Add SMB Suite apps: CRM, Billing, Products, Tickets, Forms
- CRM: Pipeline view with Lead → Opportunity → Account flow (Dynamics nomenclature) - Kanban pipeline with stages: Lead, Qualified, Proposal, Negotiation, Won, Lost - List views for Leads, Opportunities, Accounts, Contacts - Summary stats: Pipeline value, Conversion rate, Avg deal, Won this month - Billing: Invoices, Payments, Quotes management - Summary cards: Pending, Overdue, Paid this month, Revenue - Invoice list with status filters (draft, sent, paid, overdue, cancelled) - Payments tracking with method filters - Quotes with status workflow (draft → sent → accepted/rejected) - Products: Product & Service catalog - Grid and List views with category/status filters - Services tab with type filters (hourly, fixed, recurring) - Price Lists management with currency support - Tickets: AI-assisted support cases - Case management with priority/category filters - AI suggestion banner and auto-suggestions on description - List + Detail split view - Summary stats: Open, Urgent, Resolved today, AI resolved % - Forms: Redirect to Tasks with AI prompt - Quick example chips for common form types - Redirects to Tasks with 'Create a form for me about [topic]' - Menu: Added all 5 apps to dropdown menu after People - i18n: Added nav labels in English and Portuguese
This commit is contained in:
parent
80c91f6304
commit
d4082b612a
13 changed files with 5650 additions and 204 deletions
233
TODO.md
Normal file
233
TODO.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# SMB Suite Implementation TODO
|
||||
|
||||
## Overview
|
||||
Complete sovereign SMB suite with CRM, Billing, Products, Tickets, and Forms.
|
||||
Following Microsoft Dynamics nomenclature (simplified for SMB).
|
||||
|
||||
---
|
||||
|
||||
## ✅ COMPLETED
|
||||
|
||||
- [x] Create folder structure (crm, billing, products, tickets, forms)
|
||||
- [x] Create TODO.md
|
||||
- [x] Create `/suite/crm/crm.html` - Pipeline view (Kanban style)
|
||||
- [x] Create `/suite/crm/crm.css` - Styling
|
||||
- [x] Create `/suite/billing/billing.html` - Invoice list + dashboard
|
||||
- [x] Create `/suite/billing/billing.css` - Styling
|
||||
- [x] Create `/suite/products/products.html` - Product/Service catalog
|
||||
- [x] Create `/suite/products/products.css` - Styling
|
||||
- [x] Create `/suite/tickets/tickets.html` - AI-assisted support tickets
|
||||
- [x] Create `/suite/tickets/tickets.css` - Styling
|
||||
- [x] Create `/suite/forms/forms.html` - Redirect to Tasks with AI prompt
|
||||
- [x] Add CRM, Billing, Products, Tickets, Forms to dropdown menu
|
||||
- [x] Add i18n entries (en, pt-BR) for nav-crm, nav-billing, nav-products, nav-tickets, nav-forms
|
||||
|
||||
---
|
||||
|
||||
## 🔄 IN PROGRESS
|
||||
|
||||
### Phase 1: Create HTML/CSS for New Apps
|
||||
|
||||
#### 1.1 CRM (`/suite/crm/`)
|
||||
- [x] `crm.html` - Pipeline view (Kanban style)
|
||||
- [x] `crm.css` - Styling
|
||||
- [x] Entities (Dynamics nomenclature):
|
||||
- **Lead** - Unqualified prospect
|
||||
- **Opportunity** - Qualified, in sales process
|
||||
- **Account** - Company (converted customer)
|
||||
- **Contact** - Person at Account
|
||||
- **Activity** - Linked tasks/calls/emails
|
||||
|
||||
#### 1.2 Billing (`/suite/billing/`)
|
||||
- [x] `billing.html` - Invoice list + dashboard
|
||||
- [x] `billing.css` - Styling
|
||||
- [x] Entities:
|
||||
- **Invoice** - Bill to customer
|
||||
- **Payment** - Payment received
|
||||
- **Quote** - Price quotation → converts to Invoice
|
||||
|
||||
#### 1.3 Products (`/suite/products/`)
|
||||
- [x] `products.html` - Product/Service catalog
|
||||
- [x] `products.css` - Styling
|
||||
- [x] Entities:
|
||||
- **Product** - Physical/digital product
|
||||
- **Service** - Service offering
|
||||
- **PriceList** - Pricing tiers
|
||||
|
||||
#### 1.4 Tickets (`/suite/tickets/`)
|
||||
- [x] `tickets.html` - AI-assisted support tickets
|
||||
- [x] `tickets.css` - Styling
|
||||
- [x] Entities:
|
||||
- **Case** - Support ticket (Dynamics term)
|
||||
- **Resolution** - AI-suggested solutions
|
||||
|
||||
#### 1.5 Forms (`/suite/forms/`)
|
||||
- [x] `forms.html` - Redirect to Tasks with AI prompt
|
||||
- [x] Behavior: "Create a form for me about [topic]"
|
||||
|
||||
---
|
||||
|
||||
## 📋 TODO
|
||||
|
||||
### Phase 2: Menu Integration (`/suite/index.html`)
|
||||
|
||||
- [x] Add CRM to dropdown menu
|
||||
- [x] Add Billing to dropdown menu
|
||||
- [x] Add Products to dropdown menu
|
||||
- [x] Add Tickets to dropdown menu
|
||||
- [x] Add Forms to dropdown menu
|
||||
- [ ] Update header tabs (add CRM)
|
||||
- [ ] Update CSS breakpoints (`/suite/css/app.css`)
|
||||
|
||||
### Phase 3: i18n Updates
|
||||
|
||||
#### English (`/suite/js/i18n.js`)
|
||||
- [x] nav-crm, nav-billing, nav-products, nav-tickets, nav-forms
|
||||
- [ ] CRM: lead, opportunity, account, contact, pipeline, qualify, convert, won, lost
|
||||
- [ ] Billing: invoice, payment, quote, due-date, overdue, paid, pending
|
||||
- [ ] Products: product, service, price, sku, category, unit
|
||||
- [ ] Tickets: case, priority, status, assigned, resolved, escalate
|
||||
|
||||
#### Portuguese (`/suite/js/i18n.js`)
|
||||
- [x] nav-crm: "CRM"
|
||||
- [x] nav-billing: "Faturamento"
|
||||
- [x] nav-products: "Produtos"
|
||||
- [x] nav-tickets: "Chamados"
|
||||
- [x] nav-forms: "Formulários"
|
||||
- [ ] All entity labels in Portuguese
|
||||
|
||||
### Phase 4: Chat @ Mentions
|
||||
|
||||
- [ ] Add @ autocomplete in chat input
|
||||
- [ ] Entity types to reference:
|
||||
- @lead:name
|
||||
- @opportunity:name
|
||||
- @account:name
|
||||
- @contact:name
|
||||
- @invoice:number
|
||||
- @case:number
|
||||
- @product:name
|
||||
- [ ] Show entity card on hover
|
||||
- [ ] Navigate to entity on click
|
||||
|
||||
### Phase 5: Reports (in Analytics/Dashboards)
|
||||
|
||||
#### CRM Reports
|
||||
- [ ] Sales Pipeline (funnel)
|
||||
- [ ] Lead Conversion Rate
|
||||
- [ ] Opportunities by Stage
|
||||
- [ ] Won/Lost Analysis
|
||||
- [ ] Sales Forecast
|
||||
|
||||
#### Billing Reports
|
||||
- [ ] Revenue Summary
|
||||
- [ ] Aging Report (overdue invoices)
|
||||
- [ ] Payment History
|
||||
- [ ] Monthly Revenue
|
||||
|
||||
#### Support Reports
|
||||
- [ ] Open Cases by Priority
|
||||
- [ ] Resolution Time (avg)
|
||||
- [ ] Cases by Category
|
||||
- [ ] AI Resolution Rate
|
||||
|
||||
### Phase 6: BotBook Documentation
|
||||
|
||||
- [ ] Add CRM chapter
|
||||
- [ ] Add Billing chapter
|
||||
- [ ] Add Products chapter
|
||||
- [ ] Add Tickets chapter
|
||||
- [ ] Document @ mentions
|
||||
- [ ] Update SUMMARY.md
|
||||
|
||||
---
|
||||
|
||||
## File Checklist
|
||||
|
||||
### New Files to Create:
|
||||
```
|
||||
/suite/crm/crm.html
|
||||
/suite/crm/crm.css
|
||||
/suite/billing/billing.html
|
||||
/suite/billing/billing.css
|
||||
/suite/products/products.html
|
||||
/suite/products/products.css
|
||||
/suite/tickets/tickets.html
|
||||
/suite/tickets/tickets.css
|
||||
/suite/forms/forms.html
|
||||
```
|
||||
|
||||
### Files to Update:
|
||||
```
|
||||
/suite/index.html - Menu items + HTMX routes
|
||||
/suite/css/app.css - Breakpoints for new tabs
|
||||
/suite/js/i18n/en.json - English labels
|
||||
/suite/js/i18n/pt.json - Portuguese labels
|
||||
/suite/chat/chat.html - @ mention UI
|
||||
/suite/chat/chat.js - @ autocomplete logic
|
||||
```
|
||||
|
||||
### BotBook Files:
|
||||
```
|
||||
/botbook/src/SUMMARY.md - Add new chapters
|
||||
/botbook/src/XX-crm/ - CRM documentation
|
||||
/botbook/src/XX-billing/ - Billing documentation
|
||||
/botbook/src/XX-products/ - Products documentation
|
||||
/botbook/src/XX-tickets/ - Tickets documentation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entity Relationships (Dynamics Style)
|
||||
|
||||
```
|
||||
Lead ──(qualify)──► Opportunity ──(convert)──► Account + Contact
|
||||
│
|
||||
▼
|
||||
Quote ──(accept)──► Invoice ──(pay)──► Payment
|
||||
│
|
||||
└── Product/Service (line items)
|
||||
|
||||
Account ◄──► Contact (1:N)
|
||||
Account ◄──► Case/Ticket (1:N)
|
||||
Account ◄──► Invoice (1:N)
|
||||
Account ◄──► Opportunity (1:N)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTMX Patterns to Use
|
||||
|
||||
### List with selection
|
||||
```html
|
||||
<div hx-get="/api/crm/leads" hx-trigger="load" hx-target="#lead-list">
|
||||
Loading...
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pipeline drag-drop
|
||||
```html
|
||||
<div class="pipeline-column"
|
||||
hx-post="/api/crm/opportunity/{id}/stage"
|
||||
hx-trigger="drop"
|
||||
hx-vals='{"stage": "qualified"}'>
|
||||
```
|
||||
|
||||
### @ Mention autocomplete
|
||||
```html
|
||||
<input type="text"
|
||||
hx-get="/api/search/entities?q="
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#mention-dropdown">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Dynamics Nomenclature**: Lead, Opportunity, Account, Contact, Case, Quote, Invoice
|
||||
- **SMB Focus**: Simple, not enterprise complexity
|
||||
- **AI-First**: Tickets use AI suggestions, Forms use AI generation
|
||||
- **HTMX**: All interactions via HTMX
|
||||
- **Sovereign**: No external dependencies, all data local
|
||||
- **@ Mentions**: Reference any entity in chat with @type:name
|
||||
770
ui/suite/billing/billing.css
Normal file
770
ui/suite/billing/billing.css
Normal file
|
|
@ -0,0 +1,770 @@
|
|||
/* Billing Styles - Invoices, Payments & Quotes */
|
||||
|
||||
/* Container */
|
||||
.billing-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.billing-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.billing-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.billing-header h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.billing-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.billing-tab {
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.billing-tab:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.billing-tab.active {
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.billing-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.billing-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.billing-search svg {
|
||||
color: var(--text-secondary, #888);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.billing-search input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.billing-search input::placeholder {
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.billing-search-results {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 150px;
|
||||
width: 350px;
|
||||
max-height: 400px;
|
||||
background: var(--surface, #1a1a1a);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.billing-search-results:not(:empty) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Summary Cards */
|
||||
.billing-summary {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.billing-summary .summary-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.summary-icon.pending {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.summary-icon.overdue {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.summary-icon.paid {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.summary-icon.total {
|
||||
background: rgba(212, 245, 5, 0.15);
|
||||
color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.summary-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.summary-value.paid {
|
||||
color: var(--success, #22c55e);
|
||||
}
|
||||
|
||||
.summary-value.overdue {
|
||||
color: var(--error, #ef4444);
|
||||
}
|
||||
|
||||
/* Views */
|
||||
.billing-view {
|
||||
display: none;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.billing-view.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* List Header */
|
||||
.billing-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.list-filters {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.list-filters select {
|
||||
padding: 8px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list-filters select option {
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.billing-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.billing-table th,
|
||||
.billing-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.billing-table th {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #888);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.billing-table td {
|
||||
font-size: 13px;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.billing-table tbody tr {
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.billing-table tbody tr:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
/* Invoice Number Link */
|
||||
.invoice-number {
|
||||
color: var(--accent, #d4f505);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.invoice-number:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Amount styling */
|
||||
.amount {
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.amount.large {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.draft {
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.status-badge.sent {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.status-badge.paid {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.status-badge.overdue {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.status-badge.cancelled {
|
||||
background: rgba(107, 114, 128, 0.15);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.status-badge.accepted {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.status-badge.rejected {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.status-badge.expired {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: var(--accent, #d4f505);
|
||||
color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover, #c4e505);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Table row actions */
|
||||
.row-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row-actions .action-btn {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.row-actions .action-btn.danger:hover {
|
||||
border-color: var(--error, #ef4444);
|
||||
color: var(--error, #ef4444);
|
||||
}
|
||||
|
||||
/* Due date styling */
|
||||
.due-date.overdue {
|
||||
color: var(--error, #ef4444);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.due-date.soon {
|
||||
color: var(--warning, #f59e0b);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.billing-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.billing-modal.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.billing-modal-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.billing-modal-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
max-height: 90vh;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-xl);
|
||||
overflow-y: auto;
|
||||
transform: translateY(20px);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.billing-modal.open .billing-modal-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Invoice Form Styles */
|
||||
.invoice-form {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.invoice-form-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.invoice-form-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary, #888);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.form-close:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.1));
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #f8fafc);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
padding: 10px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.form-select option {
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
}
|
||||
|
||||
/* Line Items */
|
||||
.line-items {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.line-items-header {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr 1fr 1fr 40px;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
border-radius: 6px 6px 0 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #888);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.line-item {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr 1fr 1fr 40px;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-top: none;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.line-item:last-child {
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
|
||||
.line-item input {
|
||||
padding: 8px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 4px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.line-item input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.line-item-total {
|
||||
font-weight: 600;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.line-item-remove {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary, #888);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.line-item-remove:hover {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: var(--error, #ef4444);
|
||||
}
|
||||
|
||||
.add-line-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.add-line-item:hover {
|
||||
border-color: var(--accent, #d4f505);
|
||||
color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
/* Invoice Totals */
|
||||
.invoice-totals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.invoice-total-row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.invoice-total-row .label {
|
||||
color: var(--text-secondary, #888);
|
||||
min-width: 120px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.invoice-total-row .value {
|
||||
min-width: 100px;
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.invoice-total-row.grand-total {
|
||||
margin-top: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border, #2a2a2a);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.invoice-total-row.grand-total .value {
|
||||
color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
/* Form Actions */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.form-btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.form-btn.secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.form-btn.secondary:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
.form-btn.primary {
|
||||
background: var(--accent, #d4f505);
|
||||
border: 1px solid var(--accent, #d4f505);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.form-btn.primary:hover {
|
||||
background: var(--accent-hover, #c4e505);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.billing-summary {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.billing-summary .summary-card {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.billing-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.billing-header-left {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.billing-tabs {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.billing-search {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.billing-summary .summary-card {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.billing-list-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.list-filters {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.line-items-header,
|
||||
.line-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.line-items-header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
256
ui/suite/billing/billing.html
Normal file
256
ui/suite/billing/billing.html
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
<!-- Billing - Invoices, Payments & Quotes -->
|
||||
<!-- Dynamics nomenclature: Quote → Invoice → Payment -->
|
||||
|
||||
<link rel="stylesheet" href="/suite/billing/billing.css">
|
||||
|
||||
<div class="billing-container">
|
||||
<!-- Header -->
|
||||
<header class="billing-header">
|
||||
<div class="billing-header-left">
|
||||
<h1 data-i18n="billing-title">Billing</h1>
|
||||
<nav class="billing-tabs">
|
||||
<button class="billing-tab active" 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>
|
||||
</div>
|
||||
<div class="billing-header-right">
|
||||
<div class="billing-search">
|
||||
<svg 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>
|
||||
<input type="text"
|
||||
placeholder="Search invoices, quotes..."
|
||||
data-i18n-placeholder="billing-search-placeholder"
|
||||
hx-get="/api/billing/search"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#billing-search-results">
|
||||
</div>
|
||||
<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">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
<span data-i18n="billing-new-invoice">New Invoice</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Search Results -->
|
||||
<div id="billing-search-results" class="billing-search-results"></div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="billing-summary">
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon pending">
|
||||
<svg 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>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="billing-pending">Pending</span>
|
||||
<span class="summary-value" hx-get="/api/billing/stats/pending" hx-trigger="load">$0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon overdue">
|
||||
<svg width="20" 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>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="billing-overdue">Overdue</span>
|
||||
<span class="summary-value overdue" hx-get="/api/billing/stats/overdue" hx-trigger="load">$0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon paid">
|
||||
<svg 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>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="billing-paid-month">Paid This Month</span>
|
||||
<span class="summary-value paid" hx-get="/api/billing/stats/paid-month" hx-trigger="load">$0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon total">
|
||||
<svg 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>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="billing-revenue-month">Revenue This Month</span>
|
||||
<span class="summary-value" hx-get="/api/billing/stats/revenue-month" hx-trigger="load">$0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoices View (Default) -->
|
||||
<div id="billing-invoices-view" class="billing-view active">
|
||||
<div class="billing-list-header">
|
||||
<div class="list-filters">
|
||||
<select hx-get="/api/billing/invoices" hx-trigger="change" hx-target="#invoices-table-body" hx-include="this" name="status">
|
||||
<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 hx-get="/api/billing/invoices" hx-trigger="change" hx-target="#invoices-table-body" 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>
|
||||
</div>
|
||||
<div class="list-actions">
|
||||
<button class="action-btn" hx-get="/api/billing/invoices/export" data-i18n="billing-export">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="billing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="billing-col-number">Invoice #</th>
|
||||
<th data-i18n="billing-col-account">Account</th>
|
||||
<th data-i18n="billing-col-date">Date</th>
|
||||
<th data-i18n="billing-col-due">Due Date</th>
|
||||
<th data-i18n="billing-col-amount">Amount</th>
|
||||
<th data-i18n="billing-col-status">Status</th>
|
||||
<th data-i18n="billing-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="invoices-table-body" hx-get="/api/billing/invoices" hx-trigger="load">
|
||||
<!-- Invoices loaded via HTMX -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Payments View -->
|
||||
<div id="billing-payments-view" class="billing-view">
|
||||
<div class="billing-list-header">
|
||||
<div class="list-filters">
|
||||
<select hx-get="/api/billing/payments" hx-trigger="change" hx-target="#payments-table-body" hx-include="this" name="method">
|
||||
<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>
|
||||
</div>
|
||||
<button class="btn-primary" 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>
|
||||
<span data-i18n="billing-record-payment">Record Payment</span>
|
||||
</button>
|
||||
</div>
|
||||
<table class="billing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="billing-col-payment-id">Payment ID</th>
|
||||
<th data-i18n="billing-col-invoice">Invoice</th>
|
||||
<th data-i18n="billing-col-account">Account</th>
|
||||
<th data-i18n="billing-col-date">Date</th>
|
||||
<th data-i18n="billing-col-amount">Amount</th>
|
||||
<th data-i18n="billing-col-method">Method</th>
|
||||
<th data-i18n="billing-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="payments-table-body" hx-get="/api/billing/payments" hx-trigger="load">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Quotes View -->
|
||||
<div id="billing-quotes-view" class="billing-view">
|
||||
<div class="billing-list-header">
|
||||
<div class="list-filters">
|
||||
<select hx-get="/api/billing/quotes" hx-trigger="change" hx-target="#quotes-table-body" hx-include="this" name="status">
|
||||
<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>
|
||||
</div>
|
||||
<button class="btn-primary" 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>
|
||||
<span data-i18n="billing-new-quote">New Quote</span>
|
||||
</button>
|
||||
</div>
|
||||
<table class="billing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="billing-col-quote-number">Quote #</th>
|
||||
<th data-i18n="billing-col-account">Account</th>
|
||||
<th data-i18n="billing-col-opportunity">Opportunity</th>
|
||||
<th data-i18n="billing-col-date">Date</th>
|
||||
<th data-i18n="billing-col-valid-until">Valid Until</th>
|
||||
<th data-i18n="billing-col-amount">Amount</th>
|
||||
<th data-i18n="billing-col-status">Status</th>
|
||||
<th data-i18n="billing-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="quotes-table-body" hx-get="/api/billing/quotes" hx-trigger="load">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for forms -->
|
||||
<div id="billing-modal" class="billing-modal">
|
||||
<div class="billing-modal-backdrop" onclick="closeBillingModal()"></div>
|
||||
<div class="billing-modal-content" id="billing-modal-content">
|
||||
<!-- Form content loaded via HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Tab switching
|
||||
document.querySelectorAll('.billing-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
document.querySelectorAll('.billing-tab').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;
|
||||
document.getElementById(`billing-${view}-view`).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// New Invoice button
|
||||
document.getElementById('billing-new-invoice').addEventListener('click', function() {
|
||||
htmx.ajax('GET', '/suite/billing/partials/invoice-form.html', '#billing-modal-content').then(() => {
|
||||
openBillingModal();
|
||||
});
|
||||
});
|
||||
|
||||
// Modal functions
|
||||
window.openBillingModal = function() {
|
||||
document.getElementById('billing-modal').classList.add('open');
|
||||
};
|
||||
|
||||
window.closeBillingModal = function() {
|
||||
document.getElementById('billing-modal').classList.remove('open');
|
||||
};
|
||||
|
||||
// Keyboard shortcut: Escape to close modal
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeBillingModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize i18n if available
|
||||
if (window.i18n && window.i18n.translatePage) {
|
||||
window.i18n.translatePage();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
670
ui/suite/crm/crm.css
Normal file
670
ui/suite/crm/crm.css
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
/* CRM Styles - Pipeline Kanban & List Views */
|
||||
|
||||
/* Container */
|
||||
.crm-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.crm-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.crm-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.crm-header h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.crm-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.crm-tab {
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.crm-tab:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.crm-tab.active {
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.crm-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.crm-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.crm-search svg {
|
||||
color: var(--text-secondary, #888);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.crm-search input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.crm-search input::placeholder {
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.crm-search-results {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 150px;
|
||||
width: 350px;
|
||||
max-height: 400px;
|
||||
background: var(--surface, #1a1a1a);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.crm-search-results:not(:empty) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Primary Button */
|
||||
.btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover, #c4e505);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Views */
|
||||
.crm-view {
|
||||
display: none;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.crm-view.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Pipeline View */
|
||||
.pipeline-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
overflow-x: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pipeline-column {
|
||||
flex: 0 0 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 12px;
|
||||
max-height: calc(100vh - 280px);
|
||||
}
|
||||
|
||||
.pipeline-column.won {
|
||||
border-color: var(--success, #22c55e);
|
||||
background: rgba(34, 197, 94, 0.05);
|
||||
}
|
||||
|
||||
.pipeline-column.lost {
|
||||
border-color: var(--error, #ef4444);
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
|
||||
.pipeline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.pipeline-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.pipeline-count {
|
||||
padding: 2px 8px;
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.1));
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.pipeline-cards {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pipeline-cards.drag-over {
|
||||
background: rgba(212, 245, 5, 0.05);
|
||||
}
|
||||
|
||||
/* Pipeline Card */
|
||||
.pipeline-card {
|
||||
padding: 12px;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
cursor: grab;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.pipeline-card:hover {
|
||||
border-color: var(--accent, #d4f505);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.pipeline-card:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.pipeline-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pipeline-card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #f8fafc);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pipeline-card-value {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.pipeline-card-company {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #888);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pipeline-card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.pipeline-card-owner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pipeline-card-owner-avatar {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pipeline-card-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pipeline-card-date.overdue {
|
||||
color: var(--error, #ef4444);
|
||||
}
|
||||
|
||||
/* Add button in pipeline */
|
||||
.pipeline-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
margin: 12px;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.pipeline-add:hover {
|
||||
border-color: var(--accent, #d4f505);
|
||||
color: var(--accent, #d4f505);
|
||||
background: rgba(212, 245, 5, 0.05);
|
||||
}
|
||||
|
||||
/* Pipeline Summary */
|
||||
.pipeline-summary {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--border, #2a2a2a);
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #888);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.summary-value.success {
|
||||
color: var(--success, #22c55e);
|
||||
}
|
||||
|
||||
/* Table Styles (List Views) */
|
||||
.crm-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.list-filters select {
|
||||
padding: 8px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.crm-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.crm-table th,
|
||||
.crm-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.crm-table th {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #888);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.crm-table td {
|
||||
font-size: 13px;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.crm-table tbody tr {
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.crm-table tbody tr:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.new {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.status-badge.contacted {
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
.status-badge.qualified {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.status-badge.won {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.status-badge.lost {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Action buttons in table */
|
||||
.action-btn {
|
||||
padding: 6px 10px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: var(--accent, #d4f505);
|
||||
color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: var(--accent, #d4f505);
|
||||
border-color: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background: var(--accent-hover, #c4e505);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.crm-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.crm-modal.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.crm-modal-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.crm-modal-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
max-height: 90vh;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-xl);
|
||||
overflow-y: auto;
|
||||
transform: translateY(20px);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.crm-modal.open .crm-modal-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.crm-form {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.crm-form-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.crm-form-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.crm-form-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary, #888);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.crm-form-close:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.1));
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.crm-form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.crm-form-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #888);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.crm-form-input,
|
||||
.crm-form-select,
|
||||
.crm-form-textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.crm-form-input:focus,
|
||||
.crm-form-select:focus,
|
||||
.crm-form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.crm-form-textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.crm-form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.crm-form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.crm-form-btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.crm-form-btn.secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.crm-form-btn.secondary:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
.crm-form-btn.primary {
|
||||
background: var(--accent, #d4f505);
|
||||
border: 1px solid var(--accent, #d4f505);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.crm-form-btn.primary:hover {
|
||||
background: var(--accent-hover, #c4e505);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.pipeline-column {
|
||||
flex: 0 0 240px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.crm-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.crm-header-left {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.crm-tabs {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.crm-search {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.pipeline-summary {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.crm-form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
311
ui/suite/crm/crm.html
Normal file
311
ui/suite/crm/crm.html
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
<!-- CRM - Customer Relationship Management -->
|
||||
<!-- Dynamics nomenclature: Lead → Opportunity → Account/Contact -->
|
||||
|
||||
<link rel="stylesheet" href="/suite/crm/crm.css">
|
||||
|
||||
<div class="crm-container">
|
||||
<!-- Header -->
|
||||
<header class="crm-header">
|
||||
<div class="crm-header-left">
|
||||
<h1 data-i18n="crm-title">CRM</h1>
|
||||
<nav class="crm-tabs">
|
||||
<button class="crm-tab active" data-view="pipeline" data-i18n="crm-pipeline">Pipeline</button>
|
||||
<button class="crm-tab" data-view="leads" data-i18n="crm-leads">Leads</button>
|
||||
<button class="crm-tab" data-view="opportunities" data-i18n="crm-opportunities">Opportunities</button>
|
||||
<button class="crm-tab" data-view="accounts" data-i18n="crm-accounts">Accounts</button>
|
||||
<button class="crm-tab" data-view="contacts" data-i18n="crm-contacts">Contacts</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="crm-header-right">
|
||||
<div class="crm-search">
|
||||
<svg 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>
|
||||
<input type="text"
|
||||
placeholder="Search leads, opportunities, accounts..."
|
||||
data-i18n-placeholder="crm-search-placeholder"
|
||||
hx-get="/api/crm/search"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#crm-search-results">
|
||||
</div>
|
||||
<button class="btn-primary" id="crm-new-btn">
|
||||
<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>
|
||||
<span data-i18n="crm-new">New</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Search Results Dropdown -->
|
||||
<div id="crm-search-results" class="crm-search-results"></div>
|
||||
|
||||
<!-- Pipeline View (Default) -->
|
||||
<div id="crm-pipeline-view" class="crm-view active">
|
||||
<div class="pipeline-container">
|
||||
<!-- Lead Stage -->
|
||||
<div class="pipeline-column" data-stage="lead">
|
||||
<div class="pipeline-header">
|
||||
<span class="pipeline-title" data-i18n="crm-stage-lead">Lead</span>
|
||||
<span class="pipeline-count" hx-get="/api/crm/count?stage=lead" hx-trigger="load">0</span>
|
||||
</div>
|
||||
<div class="pipeline-cards"
|
||||
hx-get="/api/crm/pipeline?stage=lead"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<!-- Lead cards loaded via HTMX -->
|
||||
</div>
|
||||
<button class="pipeline-add" hx-get="/suite/crm/partials/lead-form.html" hx-target="#crm-modal-content" hx-on::after-request="openCrmModal()">
|
||||
<svg width="14" height="14" 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>
|
||||
<span data-i18n="crm-add-lead">Add Lead</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Qualified Stage -->
|
||||
<div class="pipeline-column" data-stage="qualified">
|
||||
<div class="pipeline-header">
|
||||
<span class="pipeline-title" data-i18n="crm-stage-qualified">Qualified</span>
|
||||
<span class="pipeline-count" hx-get="/api/crm/count?stage=qualified" hx-trigger="load">0</span>
|
||||
</div>
|
||||
<div class="pipeline-cards"
|
||||
hx-get="/api/crm/pipeline?stage=qualified"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proposal Stage -->
|
||||
<div class="pipeline-column" data-stage="proposal">
|
||||
<div class="pipeline-header">
|
||||
<span class="pipeline-title" data-i18n="crm-stage-proposal">Proposal</span>
|
||||
<span class="pipeline-count" hx-get="/api/crm/count?stage=proposal" hx-trigger="load">0</span>
|
||||
</div>
|
||||
<div class="pipeline-cards"
|
||||
hx-get="/api/crm/pipeline?stage=proposal"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Negotiation Stage -->
|
||||
<div class="pipeline-column" data-stage="negotiation">
|
||||
<div class="pipeline-header">
|
||||
<span class="pipeline-title" data-i18n="crm-stage-negotiation">Negotiation</span>
|
||||
<span class="pipeline-count" hx-get="/api/crm/count?stage=negotiation" hx-trigger="load">0</span>
|
||||
</div>
|
||||
<div class="pipeline-cards"
|
||||
hx-get="/api/crm/pipeline?stage=negotiation"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Won Stage -->
|
||||
<div class="pipeline-column won" data-stage="won">
|
||||
<div class="pipeline-header">
|
||||
<span class="pipeline-title" data-i18n="crm-stage-won">Won</span>
|
||||
<span class="pipeline-count" hx-get="/api/crm/count?stage=won" hx-trigger="load">0</span>
|
||||
</div>
|
||||
<div class="pipeline-cards"
|
||||
hx-get="/api/crm/pipeline?stage=won"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lost Stage -->
|
||||
<div class="pipeline-column lost" data-stage="lost">
|
||||
<div class="pipeline-header">
|
||||
<span class="pipeline-title" data-i18n="crm-stage-lost">Lost</span>
|
||||
<span class="pipeline-count" hx-get="/api/crm/count?stage=lost" hx-trigger="load">0</span>
|
||||
</div>
|
||||
<div class="pipeline-cards"
|
||||
hx-get="/api/crm/pipeline?stage=lost"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pipeline Summary -->
|
||||
<div class="pipeline-summary">
|
||||
<div class="summary-card">
|
||||
<span class="summary-label" data-i18n="crm-total-value">Total Pipeline Value</span>
|
||||
<span class="summary-value" hx-get="/api/crm/stats/pipeline-value" hx-trigger="load">$0</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label" data-i18n="crm-conversion-rate">Conversion Rate</span>
|
||||
<span class="summary-value" hx-get="/api/crm/stats/conversion-rate" hx-trigger="load">0%</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label" data-i18n="crm-avg-deal">Avg Deal Size</span>
|
||||
<span class="summary-value" hx-get="/api/crm/stats/avg-deal" hx-trigger="load">$0</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label" data-i18n="crm-this-month">Won This Month</span>
|
||||
<span class="summary-value success" hx-get="/api/crm/stats/won-month" hx-trigger="load">$0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leads List View -->
|
||||
<div id="crm-leads-view" class="crm-view">
|
||||
<div class="crm-list-header">
|
||||
<div class="list-filters">
|
||||
<select hx-get="/api/crm/leads" hx-trigger="change" hx-target="#leads-table-body" hx-include="this">
|
||||
<option value="all" data-i18n="crm-filter-all">All Leads</option>
|
||||
<option value="new" data-i18n="crm-filter-new">New</option>
|
||||
<option value="contacted" data-i18n="crm-filter-contacted">Contacted</option>
|
||||
<option value="qualified" data-i18n="crm-filter-qualified">Qualified</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<table class="crm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="crm-col-name">Name</th>
|
||||
<th data-i18n="crm-col-company">Company</th>
|
||||
<th data-i18n="crm-col-email">Email</th>
|
||||
<th data-i18n="crm-col-phone">Phone</th>
|
||||
<th data-i18n="crm-col-source">Source</th>
|
||||
<th data-i18n="crm-col-status">Status</th>
|
||||
<th data-i18n="crm-col-created">Created</th>
|
||||
<th data-i18n="crm-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="leads-table-body" hx-get="/api/crm/leads" hx-trigger="load">
|
||||
<!-- Leads loaded via HTMX -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Opportunities List View -->
|
||||
<div id="crm-opportunities-view" class="crm-view">
|
||||
<table class="crm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="crm-col-opportunity">Opportunity</th>
|
||||
<th data-i18n="crm-col-account">Account</th>
|
||||
<th data-i18n="crm-col-value">Value</th>
|
||||
<th data-i18n="crm-col-stage">Stage</th>
|
||||
<th data-i18n="crm-col-probability">Probability</th>
|
||||
<th data-i18n="crm-col-close-date">Expected Close</th>
|
||||
<th data-i18n="crm-col-owner">Owner</th>
|
||||
<th data-i18n="crm-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="opportunities-table-body" hx-get="/api/crm/opportunities" hx-trigger="load">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Accounts List View -->
|
||||
<div id="crm-accounts-view" class="crm-view">
|
||||
<table class="crm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="crm-col-account">Account</th>
|
||||
<th data-i18n="crm-col-industry">Industry</th>
|
||||
<th data-i18n="crm-col-phone">Phone</th>
|
||||
<th data-i18n="crm-col-city">City</th>
|
||||
<th data-i18n="crm-col-revenue">Annual Revenue</th>
|
||||
<th data-i18n="crm-col-contacts">Contacts</th>
|
||||
<th data-i18n="crm-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="accounts-table-body" hx-get="/api/crm/accounts" hx-trigger="load">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Contacts List View -->
|
||||
<div id="crm-contacts-view" class="crm-view">
|
||||
<table class="crm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="crm-col-name">Name</th>
|
||||
<th data-i18n="crm-col-account">Account</th>
|
||||
<th data-i18n="crm-col-title">Title</th>
|
||||
<th data-i18n="crm-col-email">Email</th>
|
||||
<th data-i18n="crm-col-phone">Phone</th>
|
||||
<th data-i18n="crm-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="contacts-table-body" hx-get="/api/crm/contacts" hx-trigger="load">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for forms -->
|
||||
<div id="crm-modal" class="crm-modal">
|
||||
<div class="crm-modal-backdrop" onclick="closeCrmModal()"></div>
|
||||
<div class="crm-modal-content" id="crm-modal-content">
|
||||
<!-- Form content loaded via HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Tab switching
|
||||
document.querySelectorAll('.crm-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
document.querySelectorAll('.crm-tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.crm-view').forEach(v => v.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
const view = this.dataset.view;
|
||||
document.getElementById(`crm-${view}-view`).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// New button dropdown
|
||||
const newBtn = document.getElementById('crm-new-btn');
|
||||
newBtn.addEventListener('click', function() {
|
||||
// Default: open lead form
|
||||
htmx.ajax('GET', '/suite/crm/partials/lead-form.html', '#crm-modal-content').then(() => {
|
||||
openCrmModal();
|
||||
});
|
||||
});
|
||||
|
||||
// Modal functions
|
||||
window.openCrmModal = function() {
|
||||
document.getElementById('crm-modal').classList.add('open');
|
||||
};
|
||||
|
||||
window.closeCrmModal = function() {
|
||||
document.getElementById('crm-modal').classList.remove('open');
|
||||
};
|
||||
|
||||
// Drag and drop for pipeline
|
||||
const pipelineCards = document.querySelectorAll('.pipeline-cards');
|
||||
pipelineCards.forEach(column => {
|
||||
column.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
column.classList.add('drag-over');
|
||||
});
|
||||
|
||||
column.addEventListener('dragleave', () => {
|
||||
column.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
column.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
column.classList.remove('drag-over');
|
||||
const cardId = e.dataTransfer.getData('text/plain');
|
||||
const newStage = column.closest('.pipeline-column').dataset.stage;
|
||||
|
||||
// Update via HTMX
|
||||
htmx.ajax('POST', `/api/crm/opportunity/${cardId}/stage`, {
|
||||
values: { stage: newStage }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize i18n if available
|
||||
if (window.i18n && window.i18n.translatePage) {
|
||||
window.i18n.translatePage();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -403,13 +403,29 @@ body {
|
|||
}
|
||||
|
||||
/* Hide header tabs progressively as screen shrinks */
|
||||
/* Header tabs: chat, mail, calendar, drive, tasks, docs, sheet, slides, social */
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.app-tab[data-section="social"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1300px) {
|
||||
.app-tab[data-section="slides"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.app-tab[data-section="sheet"],
|
||||
.app-tab[data-section="tasks"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.app-tab[data-section="docs"],
|
||||
.app-tab[data-section="calendar"] {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -427,12 +443,6 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.app-tab[data-section="paper"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.app-tab[data-section="chat"] {
|
||||
display: none;
|
||||
|
|
@ -1122,12 +1132,17 @@ body {
|
|||
}
|
||||
|
||||
/* Hide items that are visible in header tabs */
|
||||
/* Header tabs show: chat, mail, calendar, drive, tasks, docs, sheet, slides, social */
|
||||
/* These must be hidden from dropdown when visible in header */
|
||||
.app-item[data-section="chat"],
|
||||
.app-item[data-section="paper"],
|
||||
.app-item[data-section="mail"],
|
||||
.app-item[data-section="drive"],
|
||||
.app-item[data-section="calendar"],
|
||||
.app-item[data-section="tasks"] {
|
||||
.app-item[data-section="drive"],
|
||||
.app-item[data-section="tasks"],
|
||||
.app-item[data-section="docs"],
|
||||
.app-item[data-section="sheet"],
|
||||
.app-item[data-section="slides"],
|
||||
.app-item[data-section="social"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -1170,37 +1185,54 @@ body {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Show more items as screen gets smaller */
|
||||
/* Show dropdown items as their header tab counterparts get hidden */
|
||||
/* Must match the breakpoints in header-app-tabs section */
|
||||
|
||||
/* At 1400px: social tab hides */
|
||||
@media (max-width: 1400px) {
|
||||
.app-item[data-section="social"] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* At 1300px: slides tab hides */
|
||||
@media (max-width: 1300px) {
|
||||
.app-item[data-section="slides"] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* At 1200px: sheet, tasks tabs hide */
|
||||
@media (max-width: 1200px) {
|
||||
.app-item[data-section="sheet"],
|
||||
.app-item[data-section="tasks"] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* At 1100px: docs, calendar tabs hide */
|
||||
@media (max-width: 1100px) {
|
||||
.app-item[data-section="docs"],
|
||||
.app-item[data-section="calendar"] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* At 1000px: drive tab hides */
|
||||
@media (max-width: 1000px) {
|
||||
.app-item[data-section="drive"] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* At 900px: mail tab hides */
|
||||
@media (max-width: 900px) {
|
||||
.app-item[data-section="mail"] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.app-item[data-section="paper"] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* At 700px: chat tab hides */
|
||||
@media (max-width: 700px) {
|
||||
.app-item[data-section="chat"] {
|
||||
display: flex;
|
||||
|
|
|
|||
350
ui/suite/forms/forms.html
Normal file
350
ui/suite/forms/forms.html
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
<!-- Forms - Redirect to Tasks with AI Form Generation -->
|
||||
<!-- This page redirects to Tasks with a pre-filled AI prompt to create forms -->
|
||||
|
||||
<link rel="stylesheet" href="/suite/forms/forms.css" />
|
||||
|
||||
<div class="forms-container">
|
||||
<div class="forms-redirect-card">
|
||||
<div class="forms-icon">
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<line x1="10" y1="9" x2="8" y2="9" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="forms-title" data-i18n="forms-title">Create a Form</h1>
|
||||
|
||||
<p class="forms-description" data-i18n="forms-description">
|
||||
Use AI to generate custom forms, surveys, and questionnaires.
|
||||
Describe what you need and let the assistant build it for you.
|
||||
</p>
|
||||
|
||||
<div class="forms-input-group">
|
||||
<label class="forms-label" data-i18n="forms-what-form">
|
||||
What kind of form do you need?
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="forms-topic"
|
||||
class="forms-input"
|
||||
placeholder="e.g., Customer feedback survey, Event registration, Contact form..."
|
||||
data-i18n-placeholder="forms-topic-placeholder"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="forms-examples">
|
||||
<span class="forms-examples-label" data-i18n="forms-examples"
|
||||
>Quick examples:</span
|
||||
>
|
||||
<div class="forms-example-chips">
|
||||
<button
|
||||
class="forms-chip"
|
||||
data-topic="customer satisfaction survey"
|
||||
>
|
||||
Customer Survey
|
||||
</button>
|
||||
<button class="forms-chip" data-topic="event registration form">
|
||||
Event Registration
|
||||
</button>
|
||||
<button class="forms-chip" data-topic="job application form">
|
||||
Job Application
|
||||
</button>
|
||||
<button
|
||||
class="forms-chip"
|
||||
data-topic="contact form with name, email and message"
|
||||
>
|
||||
Contact Form
|
||||
</button>
|
||||
<button class="forms-chip" data-topic="product feedback form">
|
||||
Product Feedback
|
||||
</button>
|
||||
<button
|
||||
class="forms-chip"
|
||||
data-topic="newsletter subscription form"
|
||||
>
|
||||
Newsletter Signup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="forms-submit-btn" id="forms-create-btn">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"
|
||||
/>
|
||||
<circle cx="7.5" cy="14.5" r="1.5" />
|
||||
<circle cx="16.5" cy="14.5" r="1.5" />
|
||||
</svg>
|
||||
<span data-i18n="forms-create">Create Form with AI</span>
|
||||
</button>
|
||||
|
||||
<p class="forms-hint" data-i18n="forms-hint">
|
||||
You'll be redirected to Tasks where AI will generate your form
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const topicInput = document.getElementById("forms-topic");
|
||||
const createBtn = document.getElementById("forms-create-btn");
|
||||
const chips = document.querySelectorAll(".forms-chip");
|
||||
|
||||
// Handle chip clicks
|
||||
chips.forEach((chip) => {
|
||||
chip.addEventListener("click", function () {
|
||||
topicInput.value = this.dataset.topic;
|
||||
topicInput.focus();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle create button
|
||||
createBtn.addEventListener("click", function () {
|
||||
redirectToTasks();
|
||||
});
|
||||
|
||||
// Handle Enter key
|
||||
topicInput.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
redirectToTasks();
|
||||
}
|
||||
});
|
||||
|
||||
function redirectToTasks() {
|
||||
const topic = topicInput.value.trim();
|
||||
if (!topic) {
|
||||
topicInput.focus();
|
||||
topicInput.classList.add("error");
|
||||
setTimeout(() => topicInput.classList.remove("error"), 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
const prompt = `Create a form for me about: ${topic}`;
|
||||
|
||||
// Navigate to tasks with the AI prompt
|
||||
// The prompt will be passed as a query parameter or stored in sessionStorage
|
||||
sessionStorage.setItem("ai-task-prompt", prompt);
|
||||
|
||||
// Use HTMX to navigate to tasks
|
||||
window.location.hash = "#tasks";
|
||||
htmx.ajax("GET", "/suite/tasks/tasks.html", "#main-content").then(
|
||||
() => {
|
||||
// After tasks loads, trigger the AI prompt
|
||||
setTimeout(() => {
|
||||
const taskInput = document.querySelector(
|
||||
"#task-ai-input, .task-input, [data-ai-input]",
|
||||
);
|
||||
if (taskInput) {
|
||||
taskInput.value = prompt;
|
||||
taskInput.focus();
|
||||
// Trigger input event for any listeners
|
||||
taskInput.dispatchEvent(
|
||||
new Event("input", { bubbles: true }),
|
||||
);
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize i18n if available
|
||||
if (window.i18n && window.i18n.translatePage) {
|
||||
window.i18n.translatePage();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.forms-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
padding: 40px 24px;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
}
|
||||
|
||||
.forms-redirect-card {
|
||||
max-width: 560px;
|
||||
width: 100%;
|
||||
padding: 48px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.forms-icon {
|
||||
margin-bottom: 24px;
|
||||
color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.forms-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text, #f8fafc);
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.forms-description {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary, #888);
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
.forms-input-group {
|
||||
text-align: left;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.forms-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text, #f8fafc);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.forms-input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
border: 2px solid var(--border, #2a2a2a);
|
||||
border-radius: 10px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 15px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.forms-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent, #d4f505);
|
||||
box-shadow: 0 0 0 4px rgba(212, 245, 5, 0.1);
|
||||
}
|
||||
|
||||
.forms-input.error {
|
||||
border-color: var(--error, #ef4444);
|
||||
animation: shake 0.3s ease;
|
||||
}
|
||||
|
||||
.forms-input::placeholder {
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.forms-examples {
|
||||
text-align: left;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.forms-examples-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #888);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.forms-example-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.forms-chip {
|
||||
padding: 8px 14px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 20px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.forms-chip:hover {
|
||||
border-color: var(--accent, #d4f505);
|
||||
color: var(--accent, #d4f505);
|
||||
background: rgba(212, 245, 5, 0.05);
|
||||
}
|
||||
|
||||
.forms-submit-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 16px 24px;
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.forms-submit-btn:hover {
|
||||
background: var(--accent-hover, #c4e505);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(212, 245, 5, 0.2);
|
||||
}
|
||||
|
||||
.forms-submit-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.forms-hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #666);
|
||||
margin: 16px 0 0 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.forms-redirect-card {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.forms-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.forms-icon svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -292,7 +292,7 @@
|
|||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
</svg>
|
||||
<span data-i18n="nav-docs">Documentos</span>
|
||||
<span data-i18n="nav-docs">Docs</span>
|
||||
</a>
|
||||
<a
|
||||
class="app-tab"
|
||||
|
|
@ -398,15 +398,9 @@
|
|||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="5" cy="5" r="2"></circle>
|
||||
<circle cx="12" cy="5" r="2"></circle>
|
||||
<circle cx="19" cy="5" r="2"></circle>
|
||||
<circle cx="5" cy="12" r="2"></circle>
|
||||
<circle cx="12" cy="12" r="2"></circle>
|
||||
<circle cx="19" cy="12" r="2"></circle>
|
||||
<circle cx="5" cy="19" r="2"></circle>
|
||||
<circle cx="12" cy="19" r="2"></circle>
|
||||
<circle cx="19" cy="19" r="2"></circle>
|
||||
<circle cx="5" cy="12" r="2.5"></circle>
|
||||
<circle cx="12" cy="12" r="2.5"></circle>
|
||||
<circle cx="19" cy="12" r="2.5"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
|
|
@ -424,6 +418,11 @@
|
|||
All Applications
|
||||
</div>
|
||||
<div class="app-grid" role="group">
|
||||
<!-- =================================== -->
|
||||
<!-- DYNAMIC ITEMS (Header Tab Apps) -->
|
||||
<!-- These hide/show based on screen -->
|
||||
<!-- =================================== -->
|
||||
|
||||
<!-- Chat -->
|
||||
<a
|
||||
class="app-item active"
|
||||
|
|
@ -452,16 +451,16 @@
|
|||
<span data-i18n="nav-chat">Chat</span>
|
||||
</a>
|
||||
|
||||
<!-- People (Contacts) -->
|
||||
<!-- Mail -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#people"
|
||||
data-section="people"
|
||||
href="#mail"
|
||||
data-section="mail"
|
||||
role="menuitem"
|
||||
aria-label="People - Contacts"
|
||||
hx-get="/suite/people/people.html"
|
||||
aria-label="Mail application"
|
||||
hx-get="/suite/mail/mail.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#people"
|
||||
hx-push-url="/#mail"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
|
|
@ -473,26 +472,60 @@
|
|||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"
|
||||
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
|
||||
/>
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
<polyline points="22,6 12,13 2,6" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-people">People</span>
|
||||
<span data-i18n="nav-mail">E-mail</span>
|
||||
</a>
|
||||
|
||||
<!-- Paper -->
|
||||
<!-- Calendar -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#paper"
|
||||
data-section="paper"
|
||||
href="#calendar"
|
||||
data-section="calendar"
|
||||
role="menuitem"
|
||||
aria-label="Paper"
|
||||
hx-get="/suite/paper/paper.html"
|
||||
aria-label="Calendar application"
|
||||
hx-get="/suite/calendar/calendar.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#paper"
|
||||
hx-push-url="/#calendar"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="4"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-calendar">Calendário</span>
|
||||
</a>
|
||||
|
||||
<!-- Drive -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#drive"
|
||||
data-section="drive"
|
||||
role="menuitem"
|
||||
aria-label="Drive application"
|
||||
hx-get="/suite/drive/drive.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#drive"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
|
|
@ -504,15 +537,40 @@
|
|||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
|
||||
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<line x1="10" y1="9" x2="8" y2="9" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-paper">Paper</span>
|
||||
<span data-i18n="nav-drive">Arquivos</span>
|
||||
</a>
|
||||
|
||||
<!-- Tasks -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#tasks"
|
||||
data-section="tasks"
|
||||
role="menuitem"
|
||||
aria-label="Tasks application"
|
||||
hx-get="/suite/tasks/tasks.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#tasks"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M9 11l3 3L22 4" />
|
||||
<path
|
||||
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-tasks">Tarefas</span>
|
||||
</a>
|
||||
|
||||
<!-- Docs -->
|
||||
|
|
@ -543,7 +601,7 @@
|
|||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-docs">Documentos</span>
|
||||
<span data-i18n="nav-docs">Docs</span>
|
||||
</a>
|
||||
|
||||
<!-- Sheet -->
|
||||
|
|
@ -620,6 +678,270 @@
|
|||
>
|
||||
</a>
|
||||
|
||||
<!-- Social -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#social"
|
||||
data-section="social"
|
||||
role="menuitem"
|
||||
aria-label="Social Network"
|
||||
hx-get="/suite/social/social.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#social"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
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"
|
||||
/>
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-social">Social</span>
|
||||
</a>
|
||||
|
||||
<!-- =================================== -->
|
||||
<!-- STATIC ITEMS (Always visible) -->
|
||||
<!-- =================================== -->
|
||||
|
||||
<!-- People (Contacts) - First static item after dynamic -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#people"
|
||||
data-section="people"
|
||||
role="menuitem"
|
||||
aria-label="People - Contacts"
|
||||
hx-get="/suite/people/people.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#people"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
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"
|
||||
/>
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-people">People</span>
|
||||
</a>
|
||||
|
||||
<!-- CRM -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#crm"
|
||||
data-section="crm"
|
||||
role="menuitem"
|
||||
aria-label="CRM - Customer Relationship Management"
|
||||
hx-get="/suite/crm/crm.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#crm"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"
|
||||
/>
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
<line x1="19" y1="8" x2="19" y2="14" />
|
||||
<line x1="22" y1="11" x2="16" y2="11" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-crm">CRM</span>
|
||||
</a>
|
||||
|
||||
<!-- Billing -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#billing"
|
||||
data-section="billing"
|
||||
role="menuitem"
|
||||
aria-label="Billing - Invoices & Payments"
|
||||
hx-get="/suite/billing/billing.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#billing"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="1"
|
||||
y="4"
|
||||
width="22"
|
||||
height="16"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-billing">Billing</span>
|
||||
</a>
|
||||
|
||||
<!-- Products -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#products"
|
||||
data-section="products"
|
||||
role="menuitem"
|
||||
aria-label="Products - Product & Service Catalog"
|
||||
hx-get="/suite/products/products.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#products"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
|
||||
/>
|
||||
<polyline
|
||||
points="3.27 6.96 12 12.01 20.73 6.96"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
y1="22.08"
|
||||
x2="12"
|
||||
y2="12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-products">Products</span>
|
||||
</a>
|
||||
|
||||
<!-- Tickets -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#tickets"
|
||||
data-section="tickets"
|
||||
role="menuitem"
|
||||
aria-label="Tickets - Support Cases"
|
||||
hx-get="/suite/tickets/tickets.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#tickets"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-tickets">Tickets</span>
|
||||
</a>
|
||||
|
||||
<!-- Forms -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#forms"
|
||||
data-section="forms"
|
||||
role="menuitem"
|
||||
aria-label="Forms - Create forms with AI"
|
||||
hx-get="/suite/forms/forms.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#forms"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
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"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="12" y1="18" x2="12" y2="12" />
|
||||
<line x1="9" y1="15" x2="15" y2="15" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-forms">Forms</span>
|
||||
</a>
|
||||
|
||||
<!-- Paper -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#paper"
|
||||
data-section="paper"
|
||||
role="menuitem"
|
||||
aria-label="Paper"
|
||||
hx-get="/suite/paper/paper.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#paper"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<line x1="10" y1="9" x2="8" y2="9" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-paper">Paper</span>
|
||||
</a>
|
||||
|
||||
<!-- Editor -->
|
||||
<a
|
||||
class="app-item"
|
||||
|
|
@ -648,129 +970,6 @@
|
|||
<span data-i18n="nav-editor">Editor</span>
|
||||
</a>
|
||||
|
||||
<!-- Mail -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#mail"
|
||||
data-section="mail"
|
||||
role="menuitem"
|
||||
aria-label="Mail application"
|
||||
hx-get="/suite/mail/mail.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#mail"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
|
||||
/>
|
||||
<polyline points="22,6 12,13 2,6" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-mail">E-mail</span>
|
||||
</a>
|
||||
|
||||
<!-- Drive -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#drive"
|
||||
data-section="drive"
|
||||
role="menuitem"
|
||||
aria-label="Drive application"
|
||||
hx-get="/suite/drive/drive.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#drive"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-drive">Arquivos</span>
|
||||
</a>
|
||||
|
||||
<!-- Calendar -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#calendar"
|
||||
data-section="calendar"
|
||||
role="menuitem"
|
||||
aria-label="Calendar application"
|
||||
hx-get="/suite/calendar/calendar.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#calendar"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="4"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-calendar">Calendário</span>
|
||||
</a>
|
||||
|
||||
<!-- Tasks -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#tasks"
|
||||
data-section="tasks"
|
||||
role="menuitem"
|
||||
aria-label="Tasks application"
|
||||
hx-get="/suite/tasks/tasks.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#tasks"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M9 11l3 3L22 4" />
|
||||
<path
|
||||
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-tasks">Tarefas</span>
|
||||
</a>
|
||||
|
||||
<!-- Research -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#research"
|
||||
|
|
@ -917,37 +1116,6 @@
|
|||
>
|
||||
</a>
|
||||
|
||||
<!-- Social -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#social"
|
||||
data-section="social"
|
||||
role="menuitem"
|
||||
aria-label="Social Network"
|
||||
hx-get="/suite/social/social.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#social"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
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"
|
||||
/>
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-social">Social</span>
|
||||
</a>
|
||||
|
||||
<!-- Monitoring -->
|
||||
<a
|
||||
class="app-item"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@
|
|||
"nav-canvas": "Canvas",
|
||||
"nav-search": "Search...",
|
||||
"nav-all-apps": "All Applications",
|
||||
"nav-crm": "CRM",
|
||||
"nav-billing": "Billing",
|
||||
"nav-products": "Products",
|
||||
"nav-tickets": "Tickets",
|
||||
"nav-forms": "Forms",
|
||||
"dashboard-title": "Dashboard",
|
||||
"dashboard-welcome": "Welcome back!",
|
||||
"dashboard-quick-actions": "Quick Actions",
|
||||
|
|
@ -558,6 +563,11 @@
|
|||
"nav-canvas": "Canvas",
|
||||
"nav-search": "Buscar...",
|
||||
"nav-all-apps": "Todos os Aplicativos",
|
||||
"nav-crm": "CRM",
|
||||
"nav-billing": "Faturamento",
|
||||
"nav-products": "Produtos",
|
||||
"nav-tickets": "Chamados",
|
||||
"nav-forms": "Formulários",
|
||||
"dashboard-title": "Painel",
|
||||
"dashboard-welcome": "Bem-vindo de volta!",
|
||||
"dashboard-quick-actions": "Ações Rápidas",
|
||||
|
|
|
|||
851
ui/suite/products/products.css
Normal file
851
ui/suite/products/products.css
Normal file
|
|
@ -0,0 +1,851 @@
|
|||
/* Products Styles - Catalog Grid & List Views */
|
||||
|
||||
/* Container */
|
||||
.products-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.products-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.products-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.products-header h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.products-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.products-tab {
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.products-tab:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.products-tab.active {
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.products-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.products-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.products-search svg {
|
||||
color: var(--text-secondary, #888);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.products-search input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.products-search input::placeholder {
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.products-search-results {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 150px;
|
||||
width: 350px;
|
||||
max-height: 400px;
|
||||
background: var(--surface, #1a1a1a);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.products-search-results:not(:empty) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Summary Cards */
|
||||
.products-summary {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.products-summary .summary-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.summary-icon.products {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.summary-icon.services {
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
.summary-icon.active {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.summary-icon.pricelists {
|
||||
background: rgba(212, 245, 5, 0.15);
|
||||
color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.summary-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.summary-value.active {
|
||||
color: var(--success, #22c55e);
|
||||
}
|
||||
|
||||
/* Views */
|
||||
.products-view {
|
||||
display: none;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.products-view.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* List Header */
|
||||
.products-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.list-filters {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.list-filters select {
|
||||
padding: 8px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list-filters select option {
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
}
|
||||
|
||||
/* View Toggle */
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 6px 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary, #888);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Products Grid */
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.products-grid.list-view {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Product Card */
|
||||
.product-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
border-color: var(--accent, #d4f505);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.product-card-image {
|
||||
height: 140px;
|
||||
background: var(--bg-secondary, #111);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.product-card-image svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.product-card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.product-card-body {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.product-card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #f8fafc);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.product-card-sku {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-family: 'SF Mono', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.product-card-description {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary, #888);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-card-category {
|
||||
display: inline-flex;
|
||||
padding: 4px 8px;
|
||||
background: rgba(212, 245, 5, 0.1);
|
||||
color: var(--accent, #d4f505);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 12px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.product-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.product-card-price {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.product-card-price .currency {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.product-card-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.product-card-status.active {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.product-card-status.inactive {
|
||||
background: rgba(107, 114, 128, 0.15);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* List View Card Styles */
|
||||
.products-grid.list-view .product-card {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.products-grid.list-view .product-card-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: none;
|
||||
border-right: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.products-grid.list-view .product-card-image svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.products-grid.list-view .product-card-body {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.products-grid.list-view .product-card-header {
|
||||
flex: 0 0 200px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.products-grid.list-view .product-card-description {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
.products-grid.list-view .product-card-category {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.products-grid.list-view .product-card-footer {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Table Styles (Services, Price Lists) */
|
||||
.products-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.products-table th,
|
||||
.products-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.products-table th {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary, #888);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.products-table td {
|
||||
font-size: 13px;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.products-table tbody tr {
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.products-table tbody tr:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background: rgba(107, 114, 128, 0.15);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Type badges */
|
||||
.type-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.type-badge.hourly {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.type-badge.fixed {
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
.type-badge.recurring {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
/* Default indicator */
|
||||
.default-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(212, 245, 5, 0.15);
|
||||
color: var(--accent, #d4f505);
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: var(--accent, #d4f505);
|
||||
color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover, #c4e505);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.products-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.products-modal.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.products-modal-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.products-modal-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-xl);
|
||||
overflow-y: auto;
|
||||
transform: translateY(20px);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.products-modal.open .products-modal-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.product-form {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.product-form-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.product-form-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary, #888);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.form-close:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.1));
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
padding: 10px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.form-select option {
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* Image Upload */
|
||||
.image-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 32px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
border: 2px dashed var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.image-upload:hover {
|
||||
border-color: var(--accent, #d4f505);
|
||||
background: rgba(212, 245, 5, 0.02);
|
||||
}
|
||||
|
||||
.image-upload svg {
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.image-upload span {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
/* Form Actions */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.form-btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.form-btn.secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.form-btn.secondary:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
.form-btn.primary {
|
||||
background: var(--accent, #d4f505);
|
||||
border: 1px solid var(--accent, #d4f505);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.form-btn.primary:hover {
|
||||
background: var(--accent-hover, #c4e505);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.products-summary {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.products-summary .summary-card {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.products-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.products-header-left {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.products-tabs {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.products-search {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.products-summary .summary-card {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.products-list-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.list-filters {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.products-grid.list-view .product-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.products-grid.list-view .product-card-image {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.products-grid.list-view .product-card-body {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
270
ui/suite/products/products.html
Normal file
270
ui/suite/products/products.html
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
<!-- Products - Product & Service Catalog -->
|
||||
<!-- Dynamics nomenclature: Product, Service, PriceList -->
|
||||
|
||||
<link rel="stylesheet" href="/suite/products/products.css">
|
||||
|
||||
<div class="products-container">
|
||||
<!-- Header -->
|
||||
<header class="products-header">
|
||||
<div class="products-header-left">
|
||||
<h1 data-i18n="products-title">Products</h1>
|
||||
<nav class="products-tabs">
|
||||
<button class="products-tab active" data-view="catalog" data-i18n="products-catalog">Catalog</button>
|
||||
<button class="products-tab" data-view="services" data-i18n="products-services">Services</button>
|
||||
<button class="products-tab" data-view="pricelists" data-i18n="products-pricelists">Price Lists</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="products-header-right">
|
||||
<div class="products-search">
|
||||
<svg 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>
|
||||
<input type="text"
|
||||
placeholder="Search products, services..."
|
||||
data-i18n-placeholder="products-search-placeholder"
|
||||
hx-get="/api/products/search"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#products-search-results">
|
||||
</div>
|
||||
<button class="btn-primary" id="products-new-btn">
|
||||
<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>
|
||||
<span data-i18n="products-new">New Product</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Search Results -->
|
||||
<div id="products-search-results" class="products-search-results"></div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="products-summary">
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon products">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="products-total-products">Total Products</span>
|
||||
<span class="summary-value" hx-get="/api/products/stats/total-products" hx-trigger="load">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon services">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="products-total-services">Total Services</span>
|
||||
<span class="summary-value" hx-get="/api/products/stats/total-services" hx-trigger="load">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon active">
|
||||
<svg 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>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="products-active">Active Items</span>
|
||||
<span class="summary-value active" hx-get="/api/products/stats/active" hx-trigger="load">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon pricelists">
|
||||
<svg 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>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="products-price-lists">Price Lists</span>
|
||||
<span class="summary-value" hx-get="/api/products/stats/pricelists" hx-trigger="load">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Catalog View (Default) -->
|
||||
<div id="products-catalog-view" class="products-view active">
|
||||
<div class="products-list-header">
|
||||
<div class="list-filters">
|
||||
<select hx-get="/api/products/items" hx-trigger="change" hx-target="#products-grid" hx-include="this" name="category">
|
||||
<option value="all" data-i18n="products-cat-all">All Categories</option>
|
||||
<option value="software" data-i18n="products-cat-software">Software</option>
|
||||
<option value="hardware" data-i18n="products-cat-hardware">Hardware</option>
|
||||
<option value="subscription" data-i18n="products-cat-subscription">Subscription</option>
|
||||
<option value="consulting" data-i18n="products-cat-consulting">Consulting</option>
|
||||
<option value="training" data-i18n="products-cat-training">Training</option>
|
||||
<option value="support" data-i18n="products-cat-support">Support</option>
|
||||
</select>
|
||||
<select hx-get="/api/products/items" hx-trigger="change" hx-target="#products-grid" hx-include="this" name="status">
|
||||
<option value="active" data-i18n="products-status-active">Active</option>
|
||||
<option value="all" data-i18n="products-status-all">All</option>
|
||||
<option value="inactive" data-i18n="products-status-inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" data-view="grid" title="Grid view">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="view-btn" data-view="list" title="List view">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/>
|
||||
<line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/>
|
||||
<line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Products Grid -->
|
||||
<div id="products-grid" class="products-grid" hx-get="/api/products/items" hx-trigger="load">
|
||||
<!-- Products loaded via HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services View -->
|
||||
<div id="products-services-view" class="products-view">
|
||||
<div class="products-list-header">
|
||||
<div class="list-filters">
|
||||
<select hx-get="/api/products/services" hx-trigger="change" hx-target="#services-table-body" hx-include="this" name="type">
|
||||
<option value="all" data-i18n="products-type-all">All Types</option>
|
||||
<option value="hourly" data-i18n="products-type-hourly">Hourly</option>
|
||||
<option value="fixed" data-i18n="products-type-fixed">Fixed Price</option>
|
||||
<option value="recurring" data-i18n="products-type-recurring">Recurring</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary" hx-get="/suite/products/partials/service-form.html" hx-target="#products-modal-content" hx-on::after-request="openProductsModal()">
|
||||
<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>
|
||||
<span data-i18n="products-new-service">New Service</span>
|
||||
</button>
|
||||
</div>
|
||||
<table class="products-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="products-col-name">Name</th>
|
||||
<th data-i18n="products-col-description">Description</th>
|
||||
<th data-i18n="products-col-type">Type</th>
|
||||
<th data-i18n="products-col-price">Price</th>
|
||||
<th data-i18n="products-col-unit">Unit</th>
|
||||
<th data-i18n="products-col-status">Status</th>
|
||||
<th data-i18n="products-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="services-table-body" hx-get="/api/products/services" hx-trigger="load">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Price Lists View -->
|
||||
<div id="products-pricelists-view" class="products-view">
|
||||
<div class="products-list-header">
|
||||
<div class="list-filters">
|
||||
<select hx-get="/api/products/pricelists" hx-trigger="change" hx-target="#pricelists-table-body" hx-include="this" name="currency">
|
||||
<option value="all" data-i18n="products-currency-all">All Currencies</option>
|
||||
<option value="USD">USD</option>
|
||||
<option value="EUR">EUR</option>
|
||||
<option value="BRL">BRL</option>
|
||||
<option value="GBP">GBP</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary" hx-get="/suite/products/partials/pricelist-form.html" hx-target="#products-modal-content" hx-on::after-request="openProductsModal()">
|
||||
<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>
|
||||
<span data-i18n="products-new-pricelist">New Price List</span>
|
||||
</button>
|
||||
</div>
|
||||
<table class="products-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="products-col-name">Name</th>
|
||||
<th data-i18n="products-col-description">Description</th>
|
||||
<th data-i18n="products-col-currency">Currency</th>
|
||||
<th data-i18n="products-col-items">Items</th>
|
||||
<th data-i18n="products-col-valid-from">Valid From</th>
|
||||
<th data-i18n="products-col-valid-to">Valid To</th>
|
||||
<th data-i18n="products-col-default">Default</th>
|
||||
<th data-i18n="products-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pricelists-table-body" hx-get="/api/products/pricelists" hx-trigger="load">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for forms -->
|
||||
<div id="products-modal" class="products-modal">
|
||||
<div class="products-modal-backdrop" onclick="closeProductsModal()"></div>
|
||||
<div class="products-modal-content" id="products-modal-content">
|
||||
<!-- Form content loaded via HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Tab switching
|
||||
document.querySelectorAll('.products-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
document.querySelectorAll('.products-tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.products-view').forEach(v => v.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
const view = this.dataset.view;
|
||||
document.getElementById(`products-${view}-view`).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// View toggle (grid/list)
|
||||
document.querySelectorAll('.view-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
const grid = document.getElementById('products-grid');
|
||||
if (this.dataset.view === 'list') {
|
||||
grid.classList.add('list-view');
|
||||
} else {
|
||||
grid.classList.remove('list-view');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// New Product button
|
||||
document.getElementById('products-new-btn').addEventListener('click', function() {
|
||||
htmx.ajax('GET', '/suite/products/partials/product-form.html', '#products-modal-content').then(() => {
|
||||
openProductsModal();
|
||||
});
|
||||
});
|
||||
|
||||
// Modal functions
|
||||
window.openProductsModal = function() {
|
||||
document.getElementById('products-modal').classList.add('open');
|
||||
};
|
||||
|
||||
window.closeProductsModal = function() {
|
||||
document.getElementById('products-modal').classList.remove('open');
|
||||
};
|
||||
|
||||
// Keyboard shortcut: Escape to close modal
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeProductsModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize i18n if available
|
||||
if (window.i18n && window.i18n.translatePage) {
|
||||
window.i18n.translatePage();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
958
ui/suite/tickets/tickets.css
Normal file
958
ui/suite/tickets/tickets.css
Normal file
|
|
@ -0,0 +1,958 @@
|
|||
/* Tickets Styles - Support Cases with AI Assistance */
|
||||
|
||||
/* Container */
|
||||
.tickets-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.tickets-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tickets-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.tickets-header h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tickets-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tickets-tab {
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.tickets-tab:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.tickets-tab.active {
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tickets-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.tickets-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.tickets-search svg {
|
||||
color: var(--text-secondary, #888);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tickets-search input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tickets-search input::placeholder {
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.tickets-search-results {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 150px;
|
||||
width: 350px;
|
||||
max-height: 400px;
|
||||
background: var(--surface, #1a1a1a);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tickets-search-results:not(:empty) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Summary Cards */
|
||||
.tickets-summary {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.tickets-summary .summary-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 18px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.summary-icon.open {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.summary-icon.urgent {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.summary-icon.resolved {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.summary-icon.ai {
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
.summary-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.summary-value.open {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.summary-value.urgent {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.summary-value.resolved {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.summary-value.ai {
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
/* Main Content: List + Detail */
|
||||
.tickets-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Tickets List */
|
||||
.tickets-list {
|
||||
width: 400px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.tickets-list-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.list-filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.list-filters select {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list-filters select option {
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
}
|
||||
|
||||
.tickets-list-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Ticket Item */
|
||||
.ticket-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.ticket-item:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
.ticket-item.selected {
|
||||
background: var(--surface, rgba(255, 255, 255, 0.08));
|
||||
border-left: 3px solid var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.ticket-priority {
|
||||
width: 4px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ticket-priority.low {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.ticket-priority.medium {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.ticket-priority.high {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.ticket-priority.urgent {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.ticket-item-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ticket-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.ticket-item-number {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.ticket-item-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.ticket-item-subject {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text, #f8fafc);
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ticket-item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.ticket-item-account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ticket-item-category {
|
||||
padding: 2px 6px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Ticket Detail */
|
||||
.ticket-detail {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ticket-detail-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.ticket-detail-empty svg {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ticket-detail-empty p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Ticket Detail Content */
|
||||
.ticket-detail-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.ticket-detail-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ticket-detail-number {
|
||||
font-size: 12px;
|
||||
color: var(--accent, #d4f505);
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.ticket-detail-subject {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #f8fafc);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.ticket-detail-badges {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ticket-detail-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Status & Priority badges */
|
||||
.status-badge,
|
||||
.priority-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.open {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.status-badge.resolved {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.status-badge.closed {
|
||||
background: rgba(107, 114, 128, 0.15);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.priority-badge.low {
|
||||
background: rgba(107, 114, 128, 0.15);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.priority-badge.medium {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.priority-badge.high {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.priority-badge.urgent {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Ticket Info */
|
||||
.ticket-detail-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
padding: 16px 24px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 13px;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
/* Ticket Body (conversation) */
|
||||
.ticket-detail-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.ticket-message {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ticket-message-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ticket-message-avatar.ai {
|
||||
background: rgba(168, 85, 247, 0.2);
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
.ticket-message-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ticket-message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.ticket-message-author {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.ticket-message-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.ticket-message-badge {
|
||||
padding: 2px 6px;
|
||||
background: rgba(168, 85, 247, 0.15);
|
||||
color: #a855f7;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ticket-message-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
/* AI Suggestion Box */
|
||||
.ai-suggestion-box {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
border: 1px solid rgba(168, 85, 247, 0.2);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.ai-suggestion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
.ai-suggestion-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.ai-suggestion-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* Reply box */
|
||||
.ticket-reply {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--border, #2a2a2a);
|
||||
background: var(--surface, rgba(255, 255, 255, 0.02));
|
||||
}
|
||||
|
||||
.ticket-reply textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 8px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 14px;
|
||||
resize: none;
|
||||
min-height: 80px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ticket-reply textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.ticket-reply-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ticket-reply-options {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: var(--accent, #d4f505);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover, #c4e505);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary, #888);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: var(--accent, #d4f505);
|
||||
color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
border-color: var(--error, #ef4444);
|
||||
color: var(--error, #ef4444);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.tickets-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tickets-modal.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.tickets-modal-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.tickets-modal-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-xl);
|
||||
overflow-y: auto;
|
||||
transform: translateY(20px);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.tickets-modal.open .tickets-modal-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.ticket-form {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.ticket-form-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ticket-form-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary, #888);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.form-close:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.1));
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
/* AI Suggestion Banner */
|
||||
.ai-suggestion-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
border: 1px solid rgba(168, 85, 247, 0.2);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ai-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(168, 85, 247, 0.2);
|
||||
border-radius: 8px;
|
||||
color: #a855f7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-suggestion-text {
|
||||
font-size: 13px;
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.ai-suggestion-text strong {
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
/* AI Suggestions in form */
|
||||
.ai-suggestions {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ai-suggestions:not(:empty) {
|
||||
padding: 16px;
|
||||
background: rgba(168, 85, 247, 0.08);
|
||||
border: 1px solid rgba(168, 85, 247, 0.2);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.ai-suggestions-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #a855f7;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ai-suggestion-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.ai-suggestion-item:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.08));
|
||||
}
|
||||
|
||||
.ai-suggestion-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #888);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: var(--surface, rgba(255, 255, 255, 0.05));
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
border-radius: 6px;
|
||||
color: var(--text, #f8fafc);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent, #d4f505);
|
||||
}
|
||||
|
||||
.form-select option {
|
||||
background: var(--bg-primary, #0a0a0a);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.form-btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.form-btn.secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border, #2a2a2a);
|
||||
color: var(--text, #f8fafc);
|
||||
}
|
||||
|
||||
.form-btn.secondary:hover {
|
||||
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
.form-btn.primary {
|
||||
background: var(--accent, #d4f505);
|
||||
border: 1px solid var(--accent, #d4f505);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.form-btn.primary:hover {
|
||||
background: var(--accent-hover, #c4e505);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.tickets-summary {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tickets-summary .summary-card {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
min-width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.tickets-main {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tickets-list {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border, #2a2a2a);
|
||||
}
|
||||
|
||||
.ticket-detail-info {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tickets-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.tickets-header-left {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.tickets-tabs {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tickets-search {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.tickets-summary .summary-card {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ticket-detail-info {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
567
ui/suite/tickets/tickets.html
Normal file
567
ui/suite/tickets/tickets.html
Normal file
|
|
@ -0,0 +1,567 @@
|
|||
<!-- Tickets - AI-Assisted Support Cases -->
|
||||
<!-- Dynamics nomenclature: Case, Resolution, Activity -->
|
||||
|
||||
<link rel="stylesheet" href="/suite/tickets/tickets.css" />
|
||||
|
||||
<div class="tickets-container">
|
||||
<!-- Header -->
|
||||
<header class="tickets-header">
|
||||
<div class="tickets-header-left">
|
||||
<h1 data-i18n="tickets-title">Support</h1>
|
||||
<nav class="tickets-tabs">
|
||||
<button
|
||||
class="tickets-tab active"
|
||||
data-view="all"
|
||||
data-i18n="tickets-all"
|
||||
>
|
||||
All Cases
|
||||
</button>
|
||||
<button
|
||||
class="tickets-tab"
|
||||
data-view="open"
|
||||
data-i18n="tickets-open"
|
||||
>
|
||||
Open
|
||||
</button>
|
||||
<button
|
||||
class="tickets-tab"
|
||||
data-view="pending"
|
||||
data-i18n="tickets-pending"
|
||||
>
|
||||
Pending
|
||||
</button>
|
||||
<button
|
||||
class="tickets-tab"
|
||||
data-view="resolved"
|
||||
data-i18n="tickets-resolved"
|
||||
>
|
||||
Resolved
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="tickets-header-right">
|
||||
<div class="tickets-search">
|
||||
<svg
|
||||
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>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search cases..."
|
||||
data-i18n-placeholder="tickets-search-placeholder"
|
||||
hx-get="/api/tickets/search"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#tickets-search-results"
|
||||
/>
|
||||
</div>
|
||||
<button class="btn-primary" id="tickets-new-btn">
|
||||
<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>
|
||||
<span data-i18n="tickets-new">New Case</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Search Results -->
|
||||
<div id="tickets-search-results" class="tickets-search-results"></div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="tickets-summary">
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon open">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="tickets-open-cases"
|
||||
>Open Cases</span
|
||||
>
|
||||
<span
|
||||
class="summary-value open"
|
||||
hx-get="/api/tickets/stats/open"
|
||||
hx-trigger="load"
|
||||
>0</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon urgent">
|
||||
<svg
|
||||
width="20"
|
||||
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>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="tickets-urgent"
|
||||
>Urgent</span
|
||||
>
|
||||
<span
|
||||
class="summary-value urgent"
|
||||
hx-get="/api/tickets/stats/urgent"
|
||||
hx-trigger="load"
|
||||
>0</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon resolved">
|
||||
<svg
|
||||
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>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="tickets-resolved-today"
|
||||
>Resolved Today</span
|
||||
>
|
||||
<span
|
||||
class="summary-value resolved"
|
||||
hx-get="/api/tickets/stats/resolved-today"
|
||||
hx-trigger="load"
|
||||
>0</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-icon ai">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"
|
||||
/>
|
||||
<circle cx="7.5" cy="14.5" r="1.5" />
|
||||
<circle cx="16.5" cy="14.5" r="1.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-label" data-i18n="tickets-ai-resolved"
|
||||
>AI Resolved</span
|
||||
>
|
||||
<span
|
||||
class="summary-value ai"
|
||||
hx-get="/api/tickets/stats/ai-resolved"
|
||||
hx-trigger="load"
|
||||
>0%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content: List + Detail -->
|
||||
<div class="tickets-main">
|
||||
<!-- Tickets List -->
|
||||
<div class="tickets-list">
|
||||
<div class="tickets-list-header">
|
||||
<div class="list-filters">
|
||||
<select
|
||||
hx-get="/api/tickets"
|
||||
hx-trigger="change"
|
||||
hx-target="#tickets-list-body"
|
||||
hx-include="this"
|
||||
name="priority"
|
||||
>
|
||||
<option value="all" data-i18n="tickets-priority-all">
|
||||
All Priorities
|
||||
</option>
|
||||
<option
|
||||
value="urgent"
|
||||
data-i18n="tickets-priority-urgent"
|
||||
>
|
||||
Urgent
|
||||
</option>
|
||||
<option value="high" data-i18n="tickets-priority-high">
|
||||
High
|
||||
</option>
|
||||
<option
|
||||
value="medium"
|
||||
data-i18n="tickets-priority-medium"
|
||||
>
|
||||
Medium
|
||||
</option>
|
||||
<option value="low" data-i18n="tickets-priority-low">
|
||||
Low
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
hx-get="/api/tickets"
|
||||
hx-trigger="change"
|
||||
hx-target="#tickets-list-body"
|
||||
hx-include="this"
|
||||
name="category"
|
||||
>
|
||||
<option value="all" data-i18n="tickets-category-all">
|
||||
All Categories
|
||||
</option>
|
||||
<option
|
||||
value="technical"
|
||||
data-i18n="tickets-category-technical"
|
||||
>
|
||||
Technical
|
||||
</option>
|
||||
<option
|
||||
value="billing"
|
||||
data-i18n="tickets-category-billing"
|
||||
>
|
||||
Billing
|
||||
</option>
|
||||
<option
|
||||
value="general"
|
||||
data-i18n="tickets-category-general"
|
||||
>
|
||||
General
|
||||
</option>
|
||||
<option
|
||||
value="feature"
|
||||
data-i18n="tickets-category-feature"
|
||||
>
|
||||
Feature Request
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="tickets-list-body"
|
||||
class="tickets-list-body"
|
||||
hx-get="/api/tickets"
|
||||
hx-trigger="load"
|
||||
>
|
||||
<!-- Tickets loaded via HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Detail -->
|
||||
<div class="ticket-detail" id="ticket-detail">
|
||||
<div class="ticket-detail-empty">
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
</svg>
|
||||
<p data-i18n="tickets-select-case">
|
||||
Select a case to view details
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for new ticket -->
|
||||
<div id="tickets-modal" class="tickets-modal">
|
||||
<div class="tickets-modal-backdrop" onclick="closeTicketsModal()"></div>
|
||||
<div class="tickets-modal-content" id="tickets-modal-content">
|
||||
<form
|
||||
class="ticket-form"
|
||||
hx-post="/api/tickets"
|
||||
hx-target="#tickets-list-body"
|
||||
hx-on::after-request="closeTicketsModal()"
|
||||
>
|
||||
<div class="ticket-form-header">
|
||||
<h2 class="ticket-form-title" data-i18n="tickets-new-case">
|
||||
New Support Case
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="form-close"
|
||||
onclick="closeTicketsModal()"
|
||||
>
|
||||
<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="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- AI Suggestion Banner -->
|
||||
<div class="ai-suggestion-banner">
|
||||
<div class="ai-icon">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ai-suggestion-text">
|
||||
<strong data-i18n="tickets-ai-assist">AI Assistant</strong>
|
||||
<span data-i18n="tickets-ai-will-help"
|
||||
>will analyze your issue and suggest solutions</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="tickets-subject"
|
||||
>Subject</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="subject"
|
||||
class="form-input"
|
||||
required
|
||||
placeholder="Brief description of the issue"
|
||||
data-i18n-placeholder="tickets-subject-placeholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="tickets-account"
|
||||
>Account</label
|
||||
>
|
||||
<select
|
||||
name="account_id"
|
||||
class="form-select"
|
||||
hx-get="/api/crm/accounts/search"
|
||||
hx-trigger="load"
|
||||
>
|
||||
<option value="" data-i18n="tickets-select-account">
|
||||
Select account...
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="tickets-contact"
|
||||
>Contact</label
|
||||
>
|
||||
<select name="contact_id" class="form-select">
|
||||
<option value="" data-i18n="tickets-select-contact">
|
||||
Select contact...
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="tickets-priority"
|
||||
>Priority</label
|
||||
>
|
||||
<select name="priority" class="form-select" required>
|
||||
<option value="low" data-i18n="tickets-priority-low">
|
||||
Low
|
||||
</option>
|
||||
<option
|
||||
value="medium"
|
||||
selected
|
||||
data-i18n="tickets-priority-medium"
|
||||
>
|
||||
Medium
|
||||
</option>
|
||||
<option value="high" data-i18n="tickets-priority-high">
|
||||
High
|
||||
</option>
|
||||
<option
|
||||
value="urgent"
|
||||
data-i18n="tickets-priority-urgent"
|
||||
>
|
||||
Urgent
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="tickets-category"
|
||||
>Category</label
|
||||
>
|
||||
<select name="category" class="form-select" required>
|
||||
<option
|
||||
value="technical"
|
||||
data-i18n="tickets-category-technical"
|
||||
>
|
||||
Technical
|
||||
</option>
|
||||
<option
|
||||
value="billing"
|
||||
data-i18n="tickets-category-billing"
|
||||
>
|
||||
Billing
|
||||
</option>
|
||||
<option
|
||||
value="general"
|
||||
data-i18n="tickets-category-general"
|
||||
>
|
||||
General
|
||||
</option>
|
||||
<option
|
||||
value="feature"
|
||||
data-i18n="tickets-category-feature"
|
||||
>
|
||||
Feature Request
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="tickets-description"
|
||||
>Description</label
|
||||
>
|
||||
<textarea
|
||||
name="description"
|
||||
class="form-textarea"
|
||||
required
|
||||
rows="5"
|
||||
placeholder="Describe the issue in detail..."
|
||||
data-i18n-placeholder="tickets-description-placeholder"
|
||||
hx-post="/api/tickets/ai-suggest"
|
||||
hx-trigger="blur delay:500ms"
|
||||
hx-target="#ai-suggestions"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- AI Suggestions (populated when description is entered) -->
|
||||
<div id="ai-suggestions" class="ai-suggestions"></div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="form-btn secondary"
|
||||
onclick="closeTicketsModal()"
|
||||
data-i18n="common-cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="form-btn primary"
|
||||
data-i18n="tickets-create-case"
|
||||
>
|
||||
Create Case
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// Tab switching (filters list)
|
||||
document.querySelectorAll(".tickets-tab").forEach((tab) => {
|
||||
tab.addEventListener("click", function () {
|
||||
document
|
||||
.querySelectorAll(".tickets-tab")
|
||||
.forEach((t) => t.classList.remove("active"));
|
||||
this.classList.add("active");
|
||||
const status = this.dataset.view;
|
||||
htmx.ajax(
|
||||
"GET",
|
||||
`/api/tickets?status=${status}`,
|
||||
"#tickets-list-body",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// New ticket button
|
||||
document
|
||||
.getElementById("tickets-new-btn")
|
||||
.addEventListener("click", function () {
|
||||
openTicketsModal();
|
||||
});
|
||||
|
||||
// Modal functions
|
||||
window.openTicketsModal = function () {
|
||||
document.getElementById("tickets-modal").classList.add("open");
|
||||
};
|
||||
|
||||
window.closeTicketsModal = function () {
|
||||
document.getElementById("tickets-modal").classList.remove("open");
|
||||
};
|
||||
|
||||
// Select ticket to view detail
|
||||
window.selectTicket = function (ticketId) {
|
||||
// Update list selection
|
||||
document.querySelectorAll(".ticket-item").forEach((item) => {
|
||||
item.classList.remove("selected");
|
||||
});
|
||||
document
|
||||
.querySelector(`.ticket-item[data-id="${ticketId}"]`)
|
||||
?.classList.add("selected");
|
||||
|
||||
// Load detail
|
||||
htmx.ajax("GET", `/api/tickets/${ticketId}`, "#ticket-detail");
|
||||
};
|
||||
|
||||
// Keyboard shortcut: Escape to close modal
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") {
|
||||
closeTicketsModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize i18n if available
|
||||
if (window.i18n && window.i18n.translatePage) {
|
||||
window.i18n.translatePage();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
Loading…
Add table
Reference in a new issue