Add CRM to header tabs and update CSS breakpoints
- Add CRM tab to header navigation after tasks - Add CSS breakpoint at 1350px for CRM tab hiding - Add app-item breakpoint for CRM in dropdown - Delete i18n.js (translations moved to botlib .ftl files) - Update TODO.md with completed phases
This commit is contained in:
parent
d4082b612a
commit
c24ff23a07
10 changed files with 948 additions and 1423 deletions
125
TODO.md
125
TODO.md
|
|
@ -8,27 +8,9 @@ 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
|
||||
### Phase 1: Create HTML/CSS for New Apps ✅
|
||||
|
||||
---
|
||||
|
||||
## 🔄 IN PROGRESS
|
||||
|
||||
### Phase 1: Create HTML/CSS for New Apps
|
||||
|
||||
#### 1.1 CRM (`/suite/crm/`)
|
||||
#### 1.1 CRM (`/suite/crm/`) ✅
|
||||
- [x] `crm.html` - Pipeline view (Kanban style)
|
||||
- [x] `crm.css` - Styling
|
||||
- [x] Entities (Dynamics nomenclature):
|
||||
|
|
@ -38,7 +20,7 @@ Following Microsoft Dynamics nomenclature (simplified for SMB).
|
|||
- **Contact** - Person at Account
|
||||
- **Activity** - Linked tasks/calls/emails
|
||||
|
||||
#### 1.2 Billing (`/suite/billing/`)
|
||||
#### 1.2 Billing (`/suite/billing/`) ✅
|
||||
- [x] `billing.html` - Invoice list + dashboard
|
||||
- [x] `billing.css` - Styling
|
||||
- [x] Entities:
|
||||
|
|
@ -46,7 +28,7 @@ Following Microsoft Dynamics nomenclature (simplified for SMB).
|
|||
- **Payment** - Payment received
|
||||
- **Quote** - Price quotation → converts to Invoice
|
||||
|
||||
#### 1.3 Products (`/suite/products/`)
|
||||
#### 1.3 Products (`/suite/products/`) ✅
|
||||
- [x] `products.html` - Product/Service catalog
|
||||
- [x] `products.css` - Styling
|
||||
- [x] Entities:
|
||||
|
|
@ -54,47 +36,52 @@ Following Microsoft Dynamics nomenclature (simplified for SMB).
|
|||
- **Service** - Service offering
|
||||
- **PriceList** - Pricing tiers
|
||||
|
||||
#### 1.4 Tickets (`/suite/tickets/`)
|
||||
#### 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/`)
|
||||
#### 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`)
|
||||
### 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`)
|
||||
- [x] Update header tabs (add CRM)
|
||||
- [x] Update CSS breakpoints (`/suite/css/app.css`)
|
||||
|
||||
### Phase 3: i18n Updates
|
||||
### Phase 3: i18n Updates ✅
|
||||
|
||||
#### English (`/suite/js/i18n.js`)
|
||||
**NOTE:** Translations are stored in `.ftl` files in `botlib/locales/` - NOT in JS files.
|
||||
|
||||
#### English (`botlib/locales/en/ui.ftl`) ✅
|
||||
- [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
|
||||
- [x] CRM: lead, opportunity, account, contact, pipeline, qualify, convert, won, lost
|
||||
- [x] Billing: invoice, payment, quote, due-date, overdue, paid, pending
|
||||
- [x] Products: product, service, price, sku, category, unit
|
||||
- [x] Tickets: case, priority, status, assigned, resolved, escalate
|
||||
|
||||
#### Portuguese (`/suite/js/i18n.js`)
|
||||
#### Portuguese (`botlib/locales/pt-BR/ui.ftl`) ✅
|
||||
- [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
|
||||
- [x] All entity labels in Portuguese
|
||||
|
||||
#### Spanish (`botlib/locales/es/ui.ftl`) ✅
|
||||
- [x] All navigation and entity labels in Spanish
|
||||
|
||||
---
|
||||
|
||||
## 📋 TODO
|
||||
|
||||
### Phase 4: Chat @ Mentions
|
||||
|
||||
|
|
@ -142,38 +129,49 @@ Following Microsoft Dynamics nomenclature (simplified for SMB).
|
|||
|
||||
---
|
||||
|
||||
## File Checklist
|
||||
## File Structure
|
||||
|
||||
### New Files to Create:
|
||||
### i18n Files (Fluent format .ftl):
|
||||
```
|
||||
/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
|
||||
botlib/locales/
|
||||
├── en/
|
||||
│ └── ui.ftl # English translations
|
||||
├── pt-BR/
|
||||
│ └── ui.ftl # Portuguese translations
|
||||
└── es/
|
||||
└── ui.ftl # Spanish translations
|
||||
```
|
||||
|
||||
### Files to Update:
|
||||
### Suite Files:
|
||||
```
|
||||
/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
|
||||
botui/ui/suite/
|
||||
├── crm/
|
||||
│ ├── crm.html
|
||||
│ └── crm.css
|
||||
├── billing/
|
||||
│ ├── billing.html
|
||||
│ └── billing.css
|
||||
├── products/
|
||||
│ ├── products.html
|
||||
│ └── products.css
|
||||
├── tickets/
|
||||
│ ├── tickets.html
|
||||
│ └── tickets.css
|
||||
├── forms/
|
||||
│ └── forms.html
|
||||
├── index.html # Menu items + HTMX routes
|
||||
└── css/
|
||||
└── app.css # Breakpoints for tabs
|
||||
```
|
||||
|
||||
### BotBook Files:
|
||||
### BotBook Files (TODO):
|
||||
```
|
||||
/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
|
||||
botbook/src/
|
||||
├── SUMMARY.md # Add new chapters
|
||||
├── XX-crm/ # CRM documentation
|
||||
├── XX-billing/ # Billing documentation
|
||||
├── XX-products/ # Products documentation
|
||||
└── XX-tickets/ # Tickets documentation
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -225,6 +223,7 @@ Account ◄──► Opportunity (1:N)
|
|||
|
||||
## Notes
|
||||
|
||||
- **i18n Location**: All translations in `botlib/locales/{locale}/ui.ftl` files (Fluent format)
|
||||
- **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
|
||||
|
|
|
|||
197
ui/suite/billing/partials/invoice-form.html
Normal file
197
ui/suite/billing/partials/invoice-form.html
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
<!-- Invoice Form - Billing Partial -->
|
||||
<form class="invoice-form" hx-post="/api/billing/invoices" hx-target="#invoices-table-body" hx-swap="innerHTML" hx-on::after-request="closeBillingModal()">
|
||||
<div class="invoice-form-header">
|
||||
<h2 class="invoice-form-title" data-i18n="billing-new-invoice">New Invoice</h2>
|
||||
<button type="button" class="form-close" onclick="closeBillingModal()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Customer Section -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title" data-i18n="billing-customer">Customer</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-account">Account *</label>
|
||||
<select name="account_id" class="form-select" required hx-get="/api/crm/accounts/search" hx-trigger="load">
|
||||
<option value="" data-i18n="billing-select-account">Select account...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-contact">Contact</label>
|
||||
<select name="contact_id" class="form-select">
|
||||
<option value="" data-i18n="billing-select-contact">Select contact...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoice Details -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title" data-i18n="billing-details">Invoice Details</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-invoice-date">Invoice Date *</label>
|
||||
<input type="date" name="invoice_date" class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-due-date">Due Date *</label>
|
||||
<input type="date" name="due_date" class="form-input" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-payment-terms">Payment Terms</label>
|
||||
<select name="payment_terms" class="form-select">
|
||||
<option value="immediate" data-i18n="billing-terms-immediate">Due on Receipt</option>
|
||||
<option value="net15" data-i18n="billing-terms-net15">Net 15</option>
|
||||
<option value="net30" selected data-i18n="billing-terms-net30">Net 30</option>
|
||||
<option value="net45" data-i18n="billing-terms-net45">Net 45</option>
|
||||
<option value="net60" data-i18n="billing-terms-net60">Net 60</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-currency">Currency</label>
|
||||
<select name="currency" class="form-select">
|
||||
<option value="USD">USD - US Dollar</option>
|
||||
<option value="EUR">EUR - Euro</option>
|
||||
<option value="BRL">BRL - Brazilian Real</option>
|
||||
<option value="GBP">GBP - British Pound</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Line Items -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title" data-i18n="billing-line-items">Line Items</div>
|
||||
<div class="line-items">
|
||||
<div class="line-items-header">
|
||||
<span data-i18n="billing-item-description">Description</span>
|
||||
<span data-i18n="billing-item-qty">Qty</span>
|
||||
<span data-i18n="billing-item-price">Price</span>
|
||||
<span data-i18n="billing-item-total">Total</span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div id="invoice-line-items">
|
||||
<div class="line-item" data-index="0">
|
||||
<input type="text" name="items[0][description]" class="form-input" placeholder="Product or service description" required>
|
||||
<input type="number" name="items[0][quantity]" class="form-input" value="1" min="1" step="1" onchange="updateLineTotal(this)">
|
||||
<input type="number" name="items[0][unit_price]" class="form-input" placeholder="0.00" min="0" step="0.01" onchange="updateLineTotal(this)">
|
||||
<span class="line-item-total">$0.00</span>
|
||||
<button type="button" class="line-item-remove" onclick="removeLineItem(this)" title="Remove">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="add-line-item" onclick="addLineItem()">
|
||||
<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>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
<span data-i18n="billing-add-item">Add Line Item</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="invoice-totals">
|
||||
<div class="invoice-total-row">
|
||||
<span class="label" data-i18n="billing-subtotal">Subtotal</span>
|
||||
<span class="value" id="invoice-subtotal">$0.00</span>
|
||||
</div>
|
||||
<div class="invoice-total-row">
|
||||
<span class="label" data-i18n="billing-tax">Tax (0%)</span>
|
||||
<span class="value" id="invoice-tax">$0.00</span>
|
||||
</div>
|
||||
<div class="invoice-total-row grand-total">
|
||||
<span class="label" data-i18n="billing-total">Total</span>
|
||||
<span class="value" id="invoice-total">$0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-notes">Notes</label>
|
||||
<textarea name="notes" class="form-textarea" rows="2" placeholder="Payment instructions or additional notes..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="form-btn secondary" onclick="closeBillingModal()" data-i18n="common-cancel">Cancel</button>
|
||||
<button type="submit" name="action" value="draft" class="form-btn secondary" data-i18n="billing-save-draft">Save as Draft</button>
|
||||
<button type="submit" name="action" value="send" class="form-btn primary" data-i18n="billing-create-send">Create & Send</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
let lineItemIndex = 1;
|
||||
|
||||
window.addLineItem = function() {
|
||||
const container = document.getElementById('invoice-line-items');
|
||||
const newItem = document.createElement('div');
|
||||
newItem.className = 'line-item';
|
||||
newItem.dataset.index = lineItemIndex;
|
||||
newItem.innerHTML = `
|
||||
<input type="text" name="items[${lineItemIndex}][description]" class="form-input" placeholder="Product or service description" required>
|
||||
<input type="number" name="items[${lineItemIndex}][quantity]" class="form-input" value="1" min="1" step="1" onchange="updateLineTotal(this)">
|
||||
<input type="number" name="items[${lineItemIndex}][unit_price]" class="form-input" placeholder="0.00" min="0" step="0.01" onchange="updateLineTotal(this)">
|
||||
<span class="line-item-total">$0.00</span>
|
||||
<button type="button" class="line-item-remove" onclick="removeLineItem(this)" title="Remove">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(newItem);
|
||||
lineItemIndex++;
|
||||
};
|
||||
|
||||
window.removeLineItem = function(btn) {
|
||||
const items = document.querySelectorAll('.line-item');
|
||||
if (items.length > 1) {
|
||||
btn.closest('.line-item').remove();
|
||||
updateInvoiceTotals();
|
||||
}
|
||||
};
|
||||
|
||||
window.updateLineTotal = function(input) {
|
||||
const lineItem = input.closest('.line-item');
|
||||
const qty = parseFloat(lineItem.querySelector('input[name*="quantity"]').value) || 0;
|
||||
const price = parseFloat(lineItem.querySelector('input[name*="unit_price"]').value) || 0;
|
||||
const total = qty * price;
|
||||
lineItem.querySelector('.line-item-total').textContent = '$' + total.toFixed(2);
|
||||
updateInvoiceTotals();
|
||||
};
|
||||
|
||||
function updateInvoiceTotals() {
|
||||
let subtotal = 0;
|
||||
document.querySelectorAll('.line-item').forEach(item => {
|
||||
const qty = parseFloat(item.querySelector('input[name*="quantity"]').value) || 0;
|
||||
const price = parseFloat(item.querySelector('input[name*="unit_price"]').value) || 0;
|
||||
subtotal += qty * price;
|
||||
});
|
||||
const tax = 0; // Could add tax calculation
|
||||
const total = subtotal + tax;
|
||||
|
||||
document.getElementById('invoice-subtotal').textContent = '$' + subtotal.toFixed(2);
|
||||
document.getElementById('invoice-tax').textContent = '$' + tax.toFixed(2);
|
||||
document.getElementById('invoice-total').textContent = '$' + total.toFixed(2);
|
||||
}
|
||||
|
||||
// Set default dates
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const dueDate = new Date();
|
||||
dueDate.setDate(dueDate.getDate() + 30);
|
||||
|
||||
document.querySelector('input[name="invoice_date"]').value = today;
|
||||
document.querySelector('input[name="due_date"]').value = dueDate.toISOString().split('T')[0];
|
||||
})();
|
||||
</script>
|
||||
98
ui/suite/billing/partials/payment-form.html
Normal file
98
ui/suite/billing/partials/payment-form.html
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<!-- Payment Form - Billing Partial -->
|
||||
<form class="invoice-form" hx-post="/api/billing/payments" hx-target="#payments-table-body" hx-swap="innerHTML" hx-on::after-request="closeBillingModal()">
|
||||
<div class="invoice-form-header">
|
||||
<h2 class="invoice-form-title" data-i18n="billing-record-payment">Record Payment</h2>
|
||||
<button type="button" class="form-close" onclick="closeBillingModal()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Invoice Selection -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title" data-i18n="billing-invoice-details">Invoice Details</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-invoice">Invoice *</label>
|
||||
<select name="invoice_id" class="form-select" required hx-get="/api/billing/invoices/unpaid" hx-trigger="load" hx-on::after-request="updateInvoiceInfo()">
|
||||
<option value="" data-i18n="billing-select-invoice">Select invoice...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-invoice-amount">Invoice Amount</label>
|
||||
<input type="text" id="invoice-amount-display" class="form-input" readonly placeholder="$0.00">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Details -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title" data-i18n="billing-payment-details">Payment Details</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-payment-date">Payment Date *</label>
|
||||
<input type="date" name="payment_date" class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-amount">Amount *</label>
|
||||
<input type="number" name="amount" class="form-input" required placeholder="0.00" min="0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-payment-method">Payment Method *</label>
|
||||
<select name="method" class="form-select" required>
|
||||
<option value="" data-i18n="billing-select-method">Select method...</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>
|
||||
<option value="check" data-i18n="billing-method-check">Check</option>
|
||||
<option value="other" data-i18n="billing-method-other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-reference">Reference Number</label>
|
||||
<input type="text" name="reference" class="form-input" placeholder="Transaction ID, Check #, etc.">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-notes">Notes</label>
|
||||
<textarea name="notes" class="form-textarea" rows="2" placeholder="Additional payment notes..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="form-btn secondary" onclick="closeBillingModal()" data-i18n="common-cancel">Cancel</button>
|
||||
<button type="submit" class="form-btn primary" data-i18n="billing-record">Record Payment</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Set default date to today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.querySelector('input[name="payment_date"]').value = today;
|
||||
|
||||
// Update invoice info when selection changes
|
||||
window.updateInvoiceInfo = function() {
|
||||
const select = document.querySelector('select[name="invoice_id"]');
|
||||
const amountDisplay = document.getElementById('invoice-amount-display');
|
||||
const amountInput = document.querySelector('input[name="amount"]');
|
||||
|
||||
if (select.selectedOptions[0] && select.selectedOptions[0].dataset.amount) {
|
||||
const amount = select.selectedOptions[0].dataset.amount;
|
||||
amountDisplay.value = '$' + parseFloat(amount).toFixed(2);
|
||||
amountInput.value = amount;
|
||||
}
|
||||
};
|
||||
|
||||
// Add change listener
|
||||
document.querySelector('select[name="invoice_id"]').addEventListener('change', updateInvoiceInfo);
|
||||
})();
|
||||
</script>
|
||||
222
ui/suite/billing/partials/quote-form.html
Normal file
222
ui/suite/billing/partials/quote-form.html
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<!-- Quote Form - Billing Partial -->
|
||||
<form class="invoice-form" hx-post="/api/billing/quotes" hx-target="#quotes-table-body" hx-swap="innerHTML" hx-on::after-request="closeBillingModal()">
|
||||
<div class="invoice-form-header">
|
||||
<h2 class="invoice-form-title" data-i18n="billing-new-quote">New Quote</h2>
|
||||
<button type="button" class="form-close" onclick="closeBillingModal()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Customer Section -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title" data-i18n="billing-customer">Customer</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-account">Account *</label>
|
||||
<select name="account_id" class="form-select" required hx-get="/api/crm/accounts/search" hx-trigger="load">
|
||||
<option value="" data-i18n="billing-select-account">Select account...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-opportunity">Opportunity</label>
|
||||
<select name="opportunity_id" class="form-select" hx-get="/api/crm/opportunities/search" hx-trigger="load">
|
||||
<option value="" data-i18n="billing-select-opportunity">Select opportunity...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quote Details -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title" data-i18n="billing-quote-details">Quote Details</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-quote-date">Quote Date *</label>
|
||||
<input type="date" name="quote_date" class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-valid-until">Valid Until *</label>
|
||||
<input type="date" name="valid_until" class="form-input" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-quote-name">Quote Name</label>
|
||||
<input type="text" name="name" class="form-input" placeholder="e.g., Q1 Software License Proposal">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-currency">Currency</label>
|
||||
<select name="currency" class="form-select">
|
||||
<option value="USD">USD - US Dollar</option>
|
||||
<option value="EUR">EUR - Euro</option>
|
||||
<option value="BRL">BRL - Brazilian Real</option>
|
||||
<option value="GBP">GBP - British Pound</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Line Items -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title" data-i18n="billing-line-items">Line Items</div>
|
||||
<div class="line-items">
|
||||
<div class="line-items-header">
|
||||
<span data-i18n="billing-item-description">Description</span>
|
||||
<span data-i18n="billing-item-qty">Qty</span>
|
||||
<span data-i18n="billing-item-price">Price</span>
|
||||
<span data-i18n="billing-item-total">Total</span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div id="quote-line-items">
|
||||
<div class="line-item" data-index="0">
|
||||
<input type="text" name="items[0][description]" class="form-input" placeholder="Product or service description" required>
|
||||
<input type="number" name="items[0][quantity]" class="form-input" value="1" min="1" step="1" onchange="updateQuoteLineTotal(this)">
|
||||
<input type="number" name="items[0][unit_price]" class="form-input" placeholder="0.00" min="0" step="0.01" onchange="updateQuoteLineTotal(this)">
|
||||
<span class="line-item-total">$0.00</span>
|
||||
<button type="button" class="line-item-remove" onclick="removeQuoteLineItem(this)" title="Remove">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="add-line-item" onclick="addQuoteLineItem()">
|
||||
<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>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
<span data-i18n="billing-add-item">Add Line Item</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discount -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-discount-type">Discount Type</label>
|
||||
<select name="discount_type" class="form-select" onchange="updateQuoteTotals()">
|
||||
<option value="none" data-i18n="billing-discount-none">No Discount</option>
|
||||
<option value="percent" data-i18n="billing-discount-percent">Percentage (%)</option>
|
||||
<option value="fixed" data-i18n="billing-discount-fixed">Fixed Amount</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-discount-value">Discount Value</label>
|
||||
<input type="number" name="discount_value" class="form-input" placeholder="0" min="0" step="0.01" onchange="updateQuoteTotals()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="invoice-totals">
|
||||
<div class="invoice-total-row">
|
||||
<span class="label" data-i18n="billing-subtotal">Subtotal</span>
|
||||
<span class="value" id="quote-subtotal">$0.00</span>
|
||||
</div>
|
||||
<div class="invoice-total-row">
|
||||
<span class="label" data-i18n="billing-discount">Discount</span>
|
||||
<span class="value" id="quote-discount">-$0.00</span>
|
||||
</div>
|
||||
<div class="invoice-total-row grand-total">
|
||||
<span class="label" data-i18n="billing-total">Total</span>
|
||||
<span class="value" id="quote-total">$0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms & Notes -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-terms">Terms & Conditions</label>
|
||||
<textarea name="terms" class="form-textarea" rows="2" placeholder="Payment terms, delivery conditions, etc."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="billing-notes">Notes</label>
|
||||
<textarea name="notes" class="form-textarea" rows="2" placeholder="Additional notes for the customer..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="form-btn secondary" onclick="closeBillingModal()" data-i18n="common-cancel">Cancel</button>
|
||||
<button type="submit" name="action" value="draft" class="form-btn secondary" data-i18n="billing-save-draft">Save as Draft</button>
|
||||
<button type="submit" name="action" value="send" class="form-btn primary" data-i18n="billing-create-send">Create & Send</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
let quoteLineItemIndex = 1;
|
||||
|
||||
window.addQuoteLineItem = function() {
|
||||
const container = document.getElementById('quote-line-items');
|
||||
const newItem = document.createElement('div');
|
||||
newItem.className = 'line-item';
|
||||
newItem.dataset.index = quoteLineItemIndex;
|
||||
newItem.innerHTML = `
|
||||
<input type="text" name="items[${quoteLineItemIndex}][description]" class="form-input" placeholder="Product or service description" required>
|
||||
<input type="number" name="items[${quoteLineItemIndex}][quantity]" class="form-input" value="1" min="1" step="1" onchange="updateQuoteLineTotal(this)">
|
||||
<input type="number" name="items[${quoteLineItemIndex}][unit_price]" class="form-input" placeholder="0.00" min="0" step="0.01" onchange="updateQuoteLineTotal(this)">
|
||||
<span class="line-item-total">$0.00</span>
|
||||
<button type="button" class="line-item-remove" onclick="removeQuoteLineItem(this)" title="Remove">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(newItem);
|
||||
quoteLineItemIndex++;
|
||||
};
|
||||
|
||||
window.removeQuoteLineItem = function(btn) {
|
||||
const items = document.querySelectorAll('#quote-line-items .line-item');
|
||||
if (items.length > 1) {
|
||||
btn.closest('.line-item').remove();
|
||||
updateQuoteTotals();
|
||||
}
|
||||
};
|
||||
|
||||
window.updateQuoteLineTotal = function(input) {
|
||||
const lineItem = input.closest('.line-item');
|
||||
const qty = parseFloat(lineItem.querySelector('input[name*="quantity"]').value) || 0;
|
||||
const price = parseFloat(lineItem.querySelector('input[name*="unit_price"]').value) || 0;
|
||||
const total = qty * price;
|
||||
lineItem.querySelector('.line-item-total').textContent = '$' + total.toFixed(2);
|
||||
updateQuoteTotals();
|
||||
};
|
||||
|
||||
window.updateQuoteTotals = function() {
|
||||
let subtotal = 0;
|
||||
document.querySelectorAll('#quote-line-items .line-item').forEach(item => {
|
||||
const qty = parseFloat(item.querySelector('input[name*="quantity"]').value) || 0;
|
||||
const price = parseFloat(item.querySelector('input[name*="unit_price"]').value) || 0;
|
||||
subtotal += qty * price;
|
||||
});
|
||||
|
||||
const discountType = document.querySelector('select[name="discount_type"]').value;
|
||||
const discountValue = parseFloat(document.querySelector('input[name="discount_value"]').value) || 0;
|
||||
|
||||
let discount = 0;
|
||||
if (discountType === 'percent') {
|
||||
discount = subtotal * (discountValue / 100);
|
||||
} else if (discountType === 'fixed') {
|
||||
discount = discountValue;
|
||||
}
|
||||
|
||||
const total = subtotal - discount;
|
||||
|
||||
document.getElementById('quote-subtotal').textContent = '$' + subtotal.toFixed(2);
|
||||
document.getElementById('quote-discount').textContent = '-$' + discount.toFixed(2);
|
||||
document.getElementById('quote-total').textContent = '$' + total.toFixed(2);
|
||||
};
|
||||
|
||||
// Set default dates
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const validUntil = new Date();
|
||||
validUntil.setDate(validUntil.getDate() + 30);
|
||||
|
||||
document.querySelector('input[name="quote_date"]').value = today;
|
||||
document.querySelector('input[name="valid_until"]').value = validUntil.toISOString().split('T')[0];
|
||||
})();
|
||||
</script>
|
||||
76
ui/suite/crm/partials/lead-form.html
Normal file
76
ui/suite/crm/partials/lead-form.html
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<!-- Lead Form - CRM Partial -->
|
||||
<form class="crm-form" hx-post="/api/crm/leads" hx-target="#crm-pipeline-view" hx-swap="innerHTML" hx-on::after-request="closeCrmModal()">
|
||||
<div class="crm-form-header">
|
||||
<h2 class="crm-form-title" data-i18n="crm-new-lead">New Lead</h2>
|
||||
<button type="button" class="crm-form-close" onclick="closeCrmModal()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="crm-form-row">
|
||||
<div class="crm-form-group">
|
||||
<label class="crm-form-label" data-i18n="crm-first-name">First Name *</label>
|
||||
<input type="text" name="first_name" class="crm-form-input" required placeholder="John">
|
||||
</div>
|
||||
<div class="crm-form-group">
|
||||
<label class="crm-form-label" data-i18n="crm-last-name">Last Name *</label>
|
||||
<input type="text" name="last_name" class="crm-form-input" required placeholder="Doe">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="crm-form-row">
|
||||
<div class="crm-form-group">
|
||||
<label class="crm-form-label" data-i18n="crm-email">Email *</label>
|
||||
<input type="email" name="email" class="crm-form-input" required placeholder="john@company.com">
|
||||
</div>
|
||||
<div class="crm-form-group">
|
||||
<label class="crm-form-label" data-i18n="crm-phone">Phone</label>
|
||||
<input type="tel" name="phone" class="crm-form-input" placeholder="+1 (555) 123-4567">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="crm-form-row">
|
||||
<div class="crm-form-group">
|
||||
<label class="crm-form-label" data-i18n="crm-company">Company</label>
|
||||
<input type="text" name="company" class="crm-form-input" placeholder="Company Name">
|
||||
</div>
|
||||
<div class="crm-form-group">
|
||||
<label class="crm-form-label" data-i18n="crm-job-title">Job Title</label>
|
||||
<input type="text" name="job_title" class="crm-form-input" placeholder="CEO, Manager, etc.">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="crm-form-row">
|
||||
<div class="crm-form-group">
|
||||
<label class="crm-form-label" data-i18n="crm-source">Lead Source *</label>
|
||||
<select name="source" class="crm-form-select" required>
|
||||
<option value="" data-i18n="crm-select-source">Select source...</option>
|
||||
<option value="website" data-i18n="crm-source-website">Website</option>
|
||||
<option value="referral" data-i18n="crm-source-referral">Referral</option>
|
||||
<option value="linkedin" data-i18n="crm-source-linkedin">LinkedIn</option>
|
||||
<option value="email" data-i18n="crm-source-email">Email Campaign</option>
|
||||
<option value="event" data-i18n="crm-source-event">Event/Conference</option>
|
||||
<option value="cold-call" data-i18n="crm-source-cold-call">Cold Call</option>
|
||||
<option value="partner" data-i18n="crm-source-partner">Partner</option>
|
||||
<option value="other" data-i18n="crm-source-other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="crm-form-group">
|
||||
<label class="crm-form-label" data-i18n="crm-estimated-value">Estimated Value</label>
|
||||
<input type="number" name="estimated_value" class="crm-form-input" placeholder="0.00" min="0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="crm-form-group">
|
||||
<label class="crm-form-label" data-i18n="crm-notes">Notes</label>
|
||||
<textarea name="notes" class="crm-form-textarea" rows="3" placeholder="Additional information about this lead..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="crm-form-actions">
|
||||
<button type="button" class="crm-form-btn secondary" onclick="closeCrmModal()" data-i18n="common-cancel">Cancel</button>
|
||||
<button type="submit" class="crm-form-btn primary" data-i18n="crm-create-lead">Create Lead</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -403,7 +403,7 @@ body {
|
|||
}
|
||||
|
||||
/* Hide header tabs progressively as screen shrinks */
|
||||
/* Header tabs: chat, mail, calendar, drive, tasks, docs, sheet, slides, social */
|
||||
/* Header tabs: chat, mail, calendar, drive, tasks, crm, docs, sheet, slides, social */
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.app-tab[data-section="social"] {
|
||||
|
|
@ -411,6 +411,12 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1350px) {
|
||||
.app-tab[data-section="crm"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1300px) {
|
||||
.app-tab[data-section="slides"] {
|
||||
display: none;
|
||||
|
|
@ -1195,6 +1201,13 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
/* At 1350px: crm tab hides */
|
||||
@media (max-width: 1350px) {
|
||||
.app-item[data-section="crm"] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* At 1300px: slides tab hides */
|
||||
@media (max-width: 1300px) {
|
||||
.app-item[data-section="slides"] {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@
|
|||
<link rel="stylesheet" href="analytics/analytics.css" />
|
||||
<link rel="stylesheet" href="dashboards/dashboards.css" />
|
||||
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
||||
<link rel="stylesheet" href="crm/crm.css" />
|
||||
<link rel="stylesheet" href="billing/billing.css" />
|
||||
<link rel="stylesheet" href="products/products.css" />
|
||||
<link rel="stylesheet" href="tickets/tickets.css" />
|
||||
|
||||
<!-- Local Libraries (no external CDN dependencies) -->
|
||||
<script src="js/vendor/htmx.min.js"></script>
|
||||
|
|
@ -269,6 +273,31 @@
|
|||
</svg>
|
||||
<span data-i18n="nav-tasks">Tarefas</span>
|
||||
</a>
|
||||
<a
|
||||
class="app-tab"
|
||||
href="#crm"
|
||||
data-section="crm"
|
||||
hx-get="/suite/crm/crm.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#crm"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"
|
||||
/>
|
||||
<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>
|
||||
<span data-i18n="nav-crm">CRM</span>
|
||||
</a>
|
||||
<a
|
||||
class="app-tab"
|
||||
href="#docs"
|
||||
|
|
|
|||
1359
ui/suite/js/i18n.js
1359
ui/suite/js/i18n.js
File diff suppressed because it is too large
Load diff
106
ui/suite/products/partials/product-form.html
Normal file
106
ui/suite/products/partials/product-form.html
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<!-- Product Form - Products Partial -->
|
||||
<form class="product-form" hx-post="/api/products/items" hx-target="#products-grid" hx-swap="innerHTML" hx-on::after-request="closeProductsModal()">
|
||||
<div class="product-form-header">
|
||||
<h2 class="product-form-title" data-i18n="products-new-product">New Product</h2>
|
||||
<button type="button" class="form-close" onclick="closeProductsModal()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Image Upload -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-image">Product Image</label>
|
||||
<label class="image-upload" for="product-image">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
<span data-i18n="products-upload-image">Click to upload image</span>
|
||||
<input type="file" id="product-image" name="image" accept="image/*" style="display: none;">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Basic Info -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-name">Product Name *</label>
|
||||
<input type="text" name="name" class="form-input" required placeholder="Enter product name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-sku">SKU</label>
|
||||
<input type="text" name="sku" class="form-input" placeholder="PRD-001">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-description">Description</label>
|
||||
<textarea name="description" class="form-textarea" rows="3" placeholder="Product description..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Pricing -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-price">Price *</label>
|
||||
<input type="number" name="price" class="form-input" required placeholder="0.00" min="0" step="0.01">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-cost">Cost</label>
|
||||
<input type="number" name="cost" class="form-input" placeholder="0.00" min="0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category & Type -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-category">Category *</label>
|
||||
<select name="category" class="form-select" required>
|
||||
<option value="" data-i18n="products-select-category">Select category...</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>
|
||||
<option value="other" data-i18n="products-cat-other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-unit">Unit</label>
|
||||
<select name="unit" class="form-select">
|
||||
<option value="unit" data-i18n="products-unit-unit">Unit</option>
|
||||
<option value="license" data-i18n="products-unit-license">License</option>
|
||||
<option value="seat" data-i18n="products-unit-seat">Seat</option>
|
||||
<option value="hour" data-i18n="products-unit-hour">Hour</option>
|
||||
<option value="month" data-i18n="products-unit-month">Month</option>
|
||||
<option value="year" data-i18n="products-unit-year">Year</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-status">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="active" data-i18n="products-status-active">Active</option>
|
||||
<option value="inactive" data-i18n="products-status-inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-taxable">Taxable</label>
|
||||
<select name="taxable" class="form-select">
|
||||
<option value="yes" data-i18n="common-yes">Yes</option>
|
||||
<option value="no" data-i18n="common-no">No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="form-btn secondary" onclick="closeProductsModal()" data-i18n="common-cancel">Cancel</button>
|
||||
<button type="submit" class="form-btn primary" data-i18n="products-create">Create Product</button>
|
||||
</div>
|
||||
</form>
|
||||
144
ui/suite/products/partials/service-form.html
Normal file
144
ui/suite/products/partials/service-form.html
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<!-- Service Form - Products Partial -->
|
||||
<form class="product-form" hx-post="/api/products/services" hx-target="#services-table-body" hx-swap="innerHTML" hx-on::after-request="closeProductsModal()">
|
||||
<div class="product-form-header">
|
||||
<h2 class="product-form-title" data-i18n="products-new-service">New Service</h2>
|
||||
<button type="button" class="form-close" onclick="closeProductsModal()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Basic Info -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-service-name">Service Name *</label>
|
||||
<input type="text" name="name" class="form-input" required placeholder="e.g., Technical Consulting">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-description">Description</label>
|
||||
<textarea name="description" class="form-textarea" rows="3" placeholder="Describe the service offering..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Pricing Type -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-billing-type">Billing Type *</label>
|
||||
<select name="billing_type" class="form-select" required onchange="updatePricingFields(this.value)">
|
||||
<option value="" data-i18n="products-select-type">Select type...</option>
|
||||
<option value="hourly" data-i18n="products-type-hourly">Hourly Rate</option>
|
||||
<option value="fixed" data-i18n="products-type-fixed">Fixed Price</option>
|
||||
<option value="recurring" data-i18n="products-type-recurring">Recurring (Subscription)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-category">Category</label>
|
||||
<select name="category" class="form-select">
|
||||
<option value="consulting" data-i18n="products-cat-consulting">Consulting</option>
|
||||
<option value="development" data-i18n="products-cat-development">Development</option>
|
||||
<option value="training" data-i18n="products-cat-training">Training</option>
|
||||
<option value="support" data-i18n="products-cat-support">Support</option>
|
||||
<option value="maintenance" data-i18n="products-cat-maintenance">Maintenance</option>
|
||||
<option value="implementation" data-i18n="products-cat-implementation">Implementation</option>
|
||||
<option value="other" data-i18n="products-cat-other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing -->
|
||||
<div class="form-row" id="pricing-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-price">Price *</label>
|
||||
<input type="number" name="price" class="form-input" required placeholder="0.00" min="0" step="0.01">
|
||||
</div>
|
||||
<div class="form-group" id="unit-group">
|
||||
<label class="form-label" data-i18n="products-unit">Unit</label>
|
||||
<select name="unit" class="form-select">
|
||||
<option value="hour" data-i18n="products-unit-hour">Per Hour</option>
|
||||
<option value="day" data-i18n="products-unit-day">Per Day</option>
|
||||
<option value="project" data-i18n="products-unit-project">Per Project</option>
|
||||
<option value="month" data-i18n="products-unit-month">Per Month</option>
|
||||
<option value="year" data-i18n="products-unit-year">Per Year</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recurring Options (shown only for recurring type) -->
|
||||
<div class="form-row" id="recurring-options" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-billing-cycle">Billing Cycle</label>
|
||||
<select name="billing_cycle" class="form-select">
|
||||
<option value="monthly" data-i18n="products-cycle-monthly">Monthly</option>
|
||||
<option value="quarterly" data-i18n="products-cycle-quarterly">Quarterly</option>
|
||||
<option value="annually" data-i18n="products-cycle-annually">Annually</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-trial-days">Trial Period (days)</label>
|
||||
<input type="number" name="trial_days" class="form-input" placeholder="0" min="0" step="1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estimated Duration (for fixed/hourly) -->
|
||||
<div class="form-row" id="duration-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-min-hours">Min. Hours</label>
|
||||
<input type="number" name="min_hours" class="form-input" placeholder="1" min="0" step="0.5">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-max-hours">Max. Hours (estimate)</label>
|
||||
<input type="number" name="max_hours" class="form-input" placeholder="40" min="0" step="0.5">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-status">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="active" data-i18n="products-status-active">Active</option>
|
||||
<option value="inactive" data-i18n="products-status-inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="products-taxable">Taxable</label>
|
||||
<select name="taxable" class="form-select">
|
||||
<option value="yes" data-i18n="common-yes">Yes</option>
|
||||
<option value="no" data-i18n="common-no">No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="form-btn secondary" onclick="closeProductsModal()" data-i18n="common-cancel">Cancel</button>
|
||||
<button type="submit" class="form-btn primary" data-i18n="products-create-service">Create Service</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
window.updatePricingFields = function(billingType) {
|
||||
const recurringOptions = document.getElementById('recurring-options');
|
||||
const durationRow = document.getElementById('duration-row');
|
||||
const unitSelect = document.querySelector('select[name="unit"]');
|
||||
|
||||
if (billingType === 'recurring') {
|
||||
recurringOptions.style.display = 'grid';
|
||||
durationRow.style.display = 'none';
|
||||
unitSelect.value = 'month';
|
||||
} else if (billingType === 'fixed') {
|
||||
recurringOptions.style.display = 'none';
|
||||
durationRow.style.display = 'grid';
|
||||
unitSelect.value = 'project';
|
||||
} else if (billingType === 'hourly') {
|
||||
recurringOptions.style.display = 'none';
|
||||
durationRow.style.display = 'grid';
|
||||
unitSelect.value = 'hour';
|
||||
} else {
|
||||
recurringOptions.style.display = 'none';
|
||||
durationRow.style.display = 'grid';
|
||||
}
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
Loading…
Add table
Reference in a new issue