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:
Rodrigo Rodriguez (Pragmatismo) 2026-01-09 22:41:32 -03:00
parent 80c91f6304
commit d4082b612a
13 changed files with 5650 additions and 204 deletions

233
TODO.md Normal file
View 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

View 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;
}
}

View 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
View 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
View 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>

View file

@ -403,13 +403,29 @@ body {
} }
/* Hide header tabs progressively as screen shrinks */ /* 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) { @media (max-width: 1200px) {
.app-tab[data-section="sheet"],
.app-tab[data-section="tasks"] { .app-tab[data-section="tasks"] {
display: none; display: none;
} }
} }
@media (max-width: 1100px) { @media (max-width: 1100px) {
.app-tab[data-section="docs"],
.app-tab[data-section="calendar"] { .app-tab[data-section="calendar"] {
display: none; display: none;
} }
@ -427,12 +443,6 @@ body {
} }
} }
@media (max-width: 800px) {
.app-tab[data-section="paper"] {
display: none;
}
}
@media (max-width: 700px) { @media (max-width: 700px) {
.app-tab[data-section="chat"] { .app-tab[data-section="chat"] {
display: none; display: none;
@ -1122,12 +1132,17 @@ body {
} }
/* Hide items that are visible in header tabs */ /* 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="chat"],
.app-item[data-section="paper"],
.app-item[data-section="mail"], .app-item[data-section="mail"],
.app-item[data-section="drive"],
.app-item[data-section="calendar"], .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; display: none;
} }
@ -1170,37 +1185,54 @@ body {
white-space: nowrap; 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) { @media (max-width: 1200px) {
.app-item[data-section="sheet"],
.app-item[data-section="tasks"] { .app-item[data-section="tasks"] {
display: flex; display: flex;
} }
} }
/* At 1100px: docs, calendar tabs hide */
@media (max-width: 1100px) { @media (max-width: 1100px) {
.app-item[data-section="docs"],
.app-item[data-section="calendar"] { .app-item[data-section="calendar"] {
display: flex; display: flex;
} }
} }
/* At 1000px: drive tab hides */
@media (max-width: 1000px) { @media (max-width: 1000px) {
.app-item[data-section="drive"] { .app-item[data-section="drive"] {
display: flex; display: flex;
} }
} }
/* At 900px: mail tab hides */
@media (max-width: 900px) { @media (max-width: 900px) {
.app-item[data-section="mail"] { .app-item[data-section="mail"] {
display: flex; display: flex;
} }
} }
@media (max-width: 800px) { /* At 700px: chat tab hides */
.app-item[data-section="paper"] {
display: flex;
}
}
@media (max-width: 700px) { @media (max-width: 700px) {
.app-item[data-section="chat"] { .app-item[data-section="chat"] {
display: flex; display: flex;

350
ui/suite/forms/forms.html Normal file
View 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>

View file

@ -292,7 +292,7 @@
<line x1="16" y1="13" x2="8" y2="13" /> <line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" /> <line x1="16" y1="17" x2="8" y2="17" />
</svg> </svg>
<span data-i18n="nav-docs">Documentos</span> <span data-i18n="nav-docs">Docs</span>
</a> </a>
<a <a
class="app-tab" class="app-tab"
@ -398,15 +398,9 @@
fill="currentColor" fill="currentColor"
aria-hidden="true" aria-hidden="true"
> >
<circle cx="5" cy="5" r="2"></circle> <circle cx="5" cy="12" r="2.5"></circle>
<circle cx="12" cy="5" r="2"></circle> <circle cx="12" cy="12" r="2.5"></circle>
<circle cx="19" cy="5" r="2"></circle> <circle cx="19" cy="12" r="2.5"></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>
</svg> </svg>
</button> </button>
@ -424,6 +418,11 @@
All Applications All Applications
</div> </div>
<div class="app-grid" role="group"> <div class="app-grid" role="group">
<!-- =================================== -->
<!-- DYNAMIC ITEMS (Header Tab Apps) -->
<!-- These hide/show based on screen -->
<!-- =================================== -->
<!-- Chat --> <!-- Chat -->
<a <a
class="app-item active" class="app-item active"
@ -452,16 +451,16 @@
<span data-i18n="nav-chat">Chat</span> <span data-i18n="nav-chat">Chat</span>
</a> </a>
<!-- People (Contacts) --> <!-- Mail -->
<a <a
class="app-item" class="app-item"
href="#people" href="#mail"
data-section="people" data-section="mail"
role="menuitem" role="menuitem"
aria-label="People - Contacts" aria-label="Mail application"
hx-get="/suite/people/people.html" hx-get="/suite/mail/mail.html"
hx-target="#main-content" hx-target="#main-content"
hx-push-url="/#people" hx-push-url="/#mail"
> >
<div class="app-icon" aria-hidden="true"> <div class="app-icon" aria-hidden="true">
<svg <svg
@ -473,26 +472,60 @@
stroke-width="2" stroke-width="2"
> >
<path <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" /> <polyline points="22,6 12,13 2,6" />
<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> </svg>
</div> </div>
<span data-i18n="nav-people">People</span> <span data-i18n="nav-mail">E-mail</span>
</a> </a>
<!-- Paper --> <!-- Calendar -->
<a <a
class="app-item" class="app-item"
href="#paper" href="#calendar"
data-section="paper" data-section="calendar"
role="menuitem" role="menuitem"
aria-label="Paper" aria-label="Calendar application"
hx-get="/suite/paper/paper.html" hx-get="/suite/calendar/calendar.html"
hx-target="#main-content" 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"> <div class="app-icon" aria-hidden="true">
<svg <svg
@ -504,15 +537,40 @@
stroke-width="2" stroke-width="2"
> >
<path <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> </svg>
</div> </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> </a>
<!-- Docs --> <!-- Docs -->
@ -543,7 +601,7 @@
<line x1="16" y1="17" x2="8" y2="17" /> <line x1="16" y1="17" x2="8" y2="17" />
</svg> </svg>
</div> </div>
<span data-i18n="nav-docs">Documentos</span> <span data-i18n="nav-docs">Docs</span>
</a> </a>
<!-- Sheet --> <!-- Sheet -->
@ -620,6 +678,270 @@
> >
</a> </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 --> <!-- Editor -->
<a <a
class="app-item" class="app-item"
@ -648,129 +970,6 @@
<span data-i18n="nav-editor">Editor</span> <span data-i18n="nav-editor">Editor</span>
</a> </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 <a
class="app-item" class="app-item"
href="#research" href="#research"
@ -917,37 +1116,6 @@
> >
</a> </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 --> <!-- Monitoring -->
<a <a
class="app-item" class="app-item"

View file

@ -36,6 +36,11 @@
"nav-canvas": "Canvas", "nav-canvas": "Canvas",
"nav-search": "Search...", "nav-search": "Search...",
"nav-all-apps": "All Applications", "nav-all-apps": "All Applications",
"nav-crm": "CRM",
"nav-billing": "Billing",
"nav-products": "Products",
"nav-tickets": "Tickets",
"nav-forms": "Forms",
"dashboard-title": "Dashboard", "dashboard-title": "Dashboard",
"dashboard-welcome": "Welcome back!", "dashboard-welcome": "Welcome back!",
"dashboard-quick-actions": "Quick Actions", "dashboard-quick-actions": "Quick Actions",
@ -558,6 +563,11 @@
"nav-canvas": "Canvas", "nav-canvas": "Canvas",
"nav-search": "Buscar...", "nav-search": "Buscar...",
"nav-all-apps": "Todos os Aplicativos", "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-title": "Painel",
"dashboard-welcome": "Bem-vindo de volta!", "dashboard-welcome": "Bem-vindo de volta!",
"dashboard-quick-actions": "Ações Rápidas", "dashboard-quick-actions": "Ações Rápidas",

View 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;
}
}

View 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>

View 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;
}
}

View 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>