chore: add shared components and update docs
- Add vendor selector component for admin pages - Add input macros for form handling - Add truck icon for shipping UI - Update vendor operations expansion plan - Update mkdocs configuration - Update dependencies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,50 @@
|
|||||||
[x-cloak] { display: none !important; }
|
[x-cloak] { display: none !important; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- Tom Select CSS with CDN fallback -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/tom-select@2.4.1/dist/css/tom-select.default.min.css"
|
||||||
|
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/vendor/tom-select.default.min.css') }}';"
|
||||||
|
/>
|
||||||
|
<!-- Tom Select Dark Mode Overrides -->
|
||||||
|
<style>
|
||||||
|
.dark .ts-wrapper .ts-control {
|
||||||
|
background-color: rgb(55 65 81);
|
||||||
|
border-color: rgb(75 85 99);
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
.dark .ts-wrapper .ts-control input {
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
.dark .ts-wrapper .ts-control input::placeholder {
|
||||||
|
color: rgb(156 163 175);
|
||||||
|
}
|
||||||
|
.dark .ts-dropdown {
|
||||||
|
background-color: rgb(55 65 81);
|
||||||
|
border-color: rgb(75 85 99);
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
.dark .ts-dropdown .option {
|
||||||
|
color: rgb(209 213 219);
|
||||||
|
}
|
||||||
|
.dark .ts-dropdown .option.active {
|
||||||
|
background-color: rgb(147 51 234);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.dark .ts-dropdown .option:hover {
|
||||||
|
background-color: rgb(75 85 99);
|
||||||
|
}
|
||||||
|
.dark .ts-wrapper.focus .ts-control {
|
||||||
|
border-color: rgb(147 51 234);
|
||||||
|
box-shadow: 0 0 0 1px rgb(147 51 234);
|
||||||
|
}
|
||||||
|
.dark .ts-dropdown .no-results,
|
||||||
|
.dark .ts-dropdown .loading {
|
||||||
|
color: rgb(156 163 175);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- Flatpickr CSS with CDN fallback (loaded on demand via block) -->
|
<!-- Flatpickr CSS with CDN fallback (loaded on demand via block) -->
|
||||||
{% block flatpickr_css %}{% endblock %}
|
{% block flatpickr_css %}{% endblock %}
|
||||||
|
|
||||||
@@ -65,7 +109,25 @@
|
|||||||
<!-- 5. FIFTH: API Client (depends on Utils) -->
|
<!-- 5. FIFTH: API Client (depends on Utils) -->
|
||||||
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
|
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
|
||||||
|
|
||||||
<!-- 6. SIXTH: Alpine.js v3 with CDN fallback (with defer) -->
|
<!-- 6. SIXTH: Tom Select with CDN fallback -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/tom-select@2.4.1/dist/js/tom-select.complete.min.js';
|
||||||
|
script.onerror = function() {
|
||||||
|
console.warn('Tom Select CDN failed, loading local copy...');
|
||||||
|
var fallbackScript = document.createElement('script');
|
||||||
|
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/tom-select.complete.min.js") }}';
|
||||||
|
document.head.appendChild(fallbackScript);
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 7. SEVENTH: Vendor Selector (depends on Tom Select and API Client) -->
|
||||||
|
<script src="{{ url_for('static', path='shared/js/vendor-selector.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- 8. EIGHTH: Alpine.js v3 with CDN fallback (with defer) -->
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
var script = document.createElement('script');
|
var script = document.createElement('script');
|
||||||
@@ -84,13 +146,13 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- 7. OPTIONAL: Chart.js with CDN fallback (loaded on demand via block) -->
|
<!-- 9. OPTIONAL: Chart.js with CDN fallback (loaded on demand via block) -->
|
||||||
{% block chartjs_script %}{% endblock %}
|
{% block chartjs_script %}{% endblock %}
|
||||||
|
|
||||||
<!-- 8. OPTIONAL: Flatpickr with CDN fallback (loaded on demand via block) -->
|
<!-- 10. OPTIONAL: Flatpickr with CDN fallback (loaded on demand via block) -->
|
||||||
{% block flatpickr_script %}{% endblock %}
|
{% block flatpickr_script %}{% endblock %}
|
||||||
|
|
||||||
<!-- 9. LAST: Page-specific scripts -->
|
<!-- 11. LAST: Page-specific scripts -->
|
||||||
{% block extra_scripts %}{% endblock %}
|
{% block extra_scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -231,3 +231,54 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{#
|
||||||
|
Vendor Selector (Tom Select)
|
||||||
|
============================
|
||||||
|
An async searchable vendor selector using Tom Select.
|
||||||
|
Searches vendors by name and code with autocomplete.
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
- Tom Select CSS/JS must be loaded (included in admin/base.html)
|
||||||
|
- vendor-selector.js must be loaded
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- ref_name: Alpine.js x-ref name for the select element (default: 'vendorSelect')
|
||||||
|
- id: HTML id attribute (default: 'vendor-select')
|
||||||
|
- placeholder: Placeholder text (default: 'Search vendor by name or code...')
|
||||||
|
- width: CSS width class (default: 'w-80')
|
||||||
|
- on_init: JS callback name when Tom Select is initialized (optional)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
{% from 'shared/macros/inputs.html' import vendor_selector %}
|
||||||
|
|
||||||
|
{{ vendor_selector(
|
||||||
|
ref_name='vendorSelect',
|
||||||
|
placeholder='Select a vendor...',
|
||||||
|
width='w-96'
|
||||||
|
) }}
|
||||||
|
|
||||||
|
// In your Alpine.js component init():
|
||||||
|
this.$nextTick(() => {
|
||||||
|
initVendorSelector(this.$refs.vendorSelect, {
|
||||||
|
onSelect: (vendor) => this.onVendorSelected(vendor),
|
||||||
|
onClear: () => this.onVendorCleared()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#}
|
||||||
|
{% macro vendor_selector(
|
||||||
|
ref_name='vendorSelect',
|
||||||
|
id='vendor-select',
|
||||||
|
placeholder='Search vendor by name or code...',
|
||||||
|
width='w-80'
|
||||||
|
) %}
|
||||||
|
<div class="{{ width }}">
|
||||||
|
<select
|
||||||
|
id="{{ id }}"
|
||||||
|
x-ref="{{ ref_name }}"
|
||||||
|
placeholder="{{ placeholder }}"
|
||||||
|
aria-label="Vendor selector"
|
||||||
|
></select>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ The admin sidebar has a "Vendor Operations" section (formerly "Product Catalog")
|
|||||||
| Marketplace Products | ✅ Complete | View/manage products from marketplace imports |
|
| Marketplace Products | ✅ Complete | View/manage products from marketplace imports |
|
||||||
| Vendor Products | ✅ Complete | View/manage vendor-specific products |
|
| Vendor Products | ✅ Complete | View/manage vendor-specific products |
|
||||||
| Customers | ✅ Complete | Customer management (moved from Platform Admin) |
|
| Customers | ✅ Complete | Customer management (moved from Platform Admin) |
|
||||||
| Inventory | 🔲 Planned | Stock levels, adjustments, low-stock alerts |
|
| Inventory | ✅ Complete | Admin API + UI with stock adjustments |
|
||||||
| Orders | 🔲 Planned | Order management, status updates, fulfillment |
|
| Orders | ✅ Complete | Order management, status updates, fulfillment |
|
||||||
| Shipping | 🔲 Planned | Tracking, carriers, shipping rules |
|
| Shipping | 🔲 Planned | Tracking, carriers, shipping rules |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -249,21 +249,21 @@ CREATE TABLE shipping_rules (
|
|||||||
- [x] Update Alpine.js configuration
|
- [x] Update Alpine.js configuration
|
||||||
- [x] Create migration plan documentation
|
- [x] Create migration plan documentation
|
||||||
|
|
||||||
### Phase 2: Inventory
|
### Phase 2: Inventory ✅
|
||||||
- [ ] Database migrations
|
- [x] Database migrations (inventory table exists)
|
||||||
- [ ] Create `InventoryService`
|
- [x] Create `InventoryService`
|
||||||
- [ ] API endpoints
|
- [x] API endpoints
|
||||||
- [ ] Admin inventory page
|
- [x] Admin inventory page
|
||||||
- [ ] Stock adjustment modal
|
- [x] Stock adjustment modal
|
||||||
- [ ] Low stock alerts
|
- [x] Low stock alerts (via filtering)
|
||||||
|
|
||||||
### Phase 3: Orders
|
### Phase 3: Orders ✅
|
||||||
- [ ] Database migrations
|
- [x] Database migrations (orders table exists)
|
||||||
- [ ] Create `AdminOrderService`
|
- [x] Add admin methods to `OrderService`
|
||||||
- [ ] API endpoints
|
- [x] API endpoints
|
||||||
- [ ] Admin orders page
|
- [x] Admin orders page
|
||||||
- [ ] Order detail view
|
- [x] Order detail view
|
||||||
- [ ] Status update functionality
|
- [x] Status update functionality
|
||||||
|
|
||||||
### Phase 4: Shipping
|
### Phase 4: Shipping
|
||||||
- [ ] Database migrations
|
- [ ] Database migrations
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ nav:
|
|||||||
- Multi-Marketplace Product Architecture: development/migration/multi-marketplace-product-architecture.md
|
- Multi-Marketplace Product Architecture: development/migration/multi-marketplace-product-architecture.md
|
||||||
- Product Migration Database Changes: development/migration/product-migration-database-changes.md
|
- Product Migration Database Changes: development/migration/product-migration-database-changes.md
|
||||||
- Vendor Operations Expansion: development/migration/vendor-operations-expansion.md
|
- Vendor Operations Expansion: development/migration/vendor-operations-expansion.md
|
||||||
|
- Implementation Plans:
|
||||||
|
- Admin Inventory Management: implementation/inventory-admin-migration.md
|
||||||
- Seed Scripts Audit: development/seed-scripts-audit.md
|
- Seed Scripts Audit: development/seed-scripts-audit.md
|
||||||
- Database Seeder:
|
- Database Seeder:
|
||||||
- Documentation: development/database-seeder/database-seeder-documentation.md
|
- Documentation: development/database-seeder/database-seeder-documentation.md
|
||||||
@@ -186,6 +188,7 @@ nav:
|
|||||||
- User Guides:
|
- User Guides:
|
||||||
- User Management: guides/user-management.md
|
- User Management: guides/user-management.md
|
||||||
- Product Management: guides/product-management.md
|
- Product Management: guides/product-management.md
|
||||||
|
- Inventory Management: guides/inventory-management.md
|
||||||
- Shop Setup: guides/shop-setup.md
|
- Shop Setup: guides/shop-setup.md
|
||||||
- CSV Import: guides/csv-import.md
|
- CSV Import: guides/csv-import.md
|
||||||
- Marketplace Integration: guides/marketplace-integration.md
|
- Marketplace Integration: guides/marketplace-integration.md
|
||||||
|
|||||||
@@ -142,8 +142,9 @@ markers = [
|
|||||||
"performance: marks tests as performance and load tests",
|
"performance: marks tests as performance and load tests",
|
||||||
"auth: marks tests as authentication and authorization tests",
|
"auth: marks tests as authentication and authorization tests",
|
||||||
"products: marks tests as product management functionality",
|
"products: marks tests as product management functionality",
|
||||||
"inventory: marks tests as inventory and inventory management",
|
"inventory: marks tests as inventory and stock management",
|
||||||
"vendors: marks tests as vendor management functionality",
|
"vendors: marks tests as vendor management (plural - admin context)",
|
||||||
|
"vendor: marks tests as vendor API tests (singular - vendor context)",
|
||||||
"admin: marks tests as admin functionality and permissions",
|
"admin: marks tests as admin functionality and permissions",
|
||||||
"marketplace: marks tests as marketplace import functionality",
|
"marketplace: marks tests as marketplace import functionality",
|
||||||
"stats: marks tests as statistics and reporting",
|
"stats: marks tests as statistics and reporting",
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ const Icons = {
|
|||||||
'receipt': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2zM10 8.5a.5.5 0 11-1 0 .5.5 0 011 0zm5 5a.5.5 0 11-1 0 .5.5 0 011 0z"/></svg>`,
|
'receipt': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2zM10 8.5a.5.5 0 11-1 0 .5.5 0 011 0zm5 5a.5.5 0 11-1 0 .5.5 0 011 0z"/></svg>`,
|
||||||
|
|
||||||
// Inventory & Products
|
// Inventory & Products
|
||||||
|
'archive': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"/></svg>`,
|
||||||
'cube': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/></svg>`,
|
'cube': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/></svg>`,
|
||||||
'collection': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>`,
|
'collection': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>`,
|
||||||
'photograph': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>`,
|
'photograph': `<svg class="{{classes}}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>`,
|
||||||
|
|||||||
243
static/shared/js/vendor-selector.js
Normal file
243
static/shared/js/vendor-selector.js
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
// static/shared/js/vendor-selector.js
|
||||||
|
/**
|
||||||
|
* Shared Vendor Selector Module
|
||||||
|
* =============================
|
||||||
|
* Provides a reusable Tom Select-based vendor autocomplete component.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Async search with debouncing (150ms)
|
||||||
|
* - Searches by vendor name and code
|
||||||
|
* - Dark mode support
|
||||||
|
* - Caches recent searches
|
||||||
|
* - Graceful fallback if Tom Select not available
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* // In Alpine.js component init():
|
||||||
|
* this.$nextTick(() => {
|
||||||
|
* this.vendorSelector = initVendorSelector(this.$refs.vendorSelect, {
|
||||||
|
* onSelect: (vendor) => this.handleVendorSelect(vendor),
|
||||||
|
* onClear: () => this.handleVendorClear(),
|
||||||
|
* minChars: 2,
|
||||||
|
* maxOptions: 50
|
||||||
|
* });
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // To programmatically set a value:
|
||||||
|
* this.vendorSelector.setValue(vendorId);
|
||||||
|
*
|
||||||
|
* // To clear:
|
||||||
|
* this.vendorSelector.clear();
|
||||||
|
*/
|
||||||
|
|
||||||
|
const vendorSelectorLog = window.LogConfig?.loggers?.vendorSelector ||
|
||||||
|
window.LogConfig?.createLogger?.('vendorSelector', false) ||
|
||||||
|
{ info: console.log, warn: console.warn, error: console.error };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Tom Select is available, with retry logic
|
||||||
|
* @param {Function} callback - Called when Tom Select is available
|
||||||
|
* @param {number} maxRetries - Maximum retry attempts
|
||||||
|
* @param {number} retryDelay - Delay between retries in ms
|
||||||
|
*/
|
||||||
|
function waitForTomSelect(callback, maxRetries = 20, retryDelay = 100) {
|
||||||
|
let retries = 0;
|
||||||
|
|
||||||
|
function check() {
|
||||||
|
if (typeof TomSelect !== 'undefined') {
|
||||||
|
callback();
|
||||||
|
} else if (retries < maxRetries) {
|
||||||
|
retries++;
|
||||||
|
vendorSelectorLog.info(`Waiting for TomSelect... (attempt ${retries}/${maxRetries})`);
|
||||||
|
setTimeout(check, retryDelay);
|
||||||
|
} else {
|
||||||
|
vendorSelectorLog.error('TomSelect not available after maximum retries');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a vendor selector on the given element
|
||||||
|
* @param {HTMLElement} selectElement - The select element to enhance
|
||||||
|
* @param {Object} options - Configuration options
|
||||||
|
* @param {Function} options.onSelect - Callback when vendor is selected (receives vendor object)
|
||||||
|
* @param {Function} options.onClear - Callback when selection is cleared
|
||||||
|
* @param {number} options.minChars - Minimum characters before search (default: 2)
|
||||||
|
* @param {number} options.maxOptions - Maximum options to show (default: 50)
|
||||||
|
* @param {string} options.placeholder - Placeholder text
|
||||||
|
* @param {string} options.apiEndpoint - API endpoint for search (default: '/api/v1/admin/vendors')
|
||||||
|
* @returns {Object} Controller object with setValue() and clear() methods
|
||||||
|
*/
|
||||||
|
function initVendorSelector(selectElement, options = {}) {
|
||||||
|
if (!selectElement) {
|
||||||
|
vendorSelectorLog.error('Vendor selector element not provided');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
minChars: options.minChars || 2,
|
||||||
|
maxOptions: options.maxOptions || 50,
|
||||||
|
placeholder: options.placeholder || selectElement.getAttribute('placeholder') || 'Search vendor by name or code...',
|
||||||
|
apiEndpoint: options.apiEndpoint || '/api/v1/admin/vendors',
|
||||||
|
onSelect: options.onSelect || (() => {}),
|
||||||
|
onClear: options.onClear || (() => {})
|
||||||
|
};
|
||||||
|
|
||||||
|
let tomSelectInstance = null;
|
||||||
|
|
||||||
|
// Controller object returned to caller
|
||||||
|
const controller = {
|
||||||
|
/**
|
||||||
|
* Set the selected vendor by ID
|
||||||
|
* @param {number} vendorId - Vendor ID to select
|
||||||
|
* @param {Object} vendorData - Optional vendor data to avoid API call
|
||||||
|
*/
|
||||||
|
setValue: async function(vendorId, vendorData = null) {
|
||||||
|
if (!tomSelectInstance) return;
|
||||||
|
|
||||||
|
if (vendorData) {
|
||||||
|
// Add option and set value
|
||||||
|
tomSelectInstance.addOption({
|
||||||
|
id: vendorData.id,
|
||||||
|
name: vendorData.name,
|
||||||
|
vendor_code: vendorData.vendor_code
|
||||||
|
});
|
||||||
|
tomSelectInstance.setValue(vendorData.id, true);
|
||||||
|
} else if (vendorId) {
|
||||||
|
// Fetch vendor data and set
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`${config.apiEndpoint}/${vendorId}`);
|
||||||
|
tomSelectInstance.addOption({
|
||||||
|
id: response.id,
|
||||||
|
name: response.name,
|
||||||
|
vendor_code: response.vendor_code
|
||||||
|
});
|
||||||
|
tomSelectInstance.setValue(response.id, true);
|
||||||
|
} catch (error) {
|
||||||
|
vendorSelectorLog.error('Failed to load vendor:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the selection
|
||||||
|
*/
|
||||||
|
clear: function() {
|
||||||
|
if (tomSelectInstance) {
|
||||||
|
tomSelectInstance.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Tom Select instance
|
||||||
|
*/
|
||||||
|
getInstance: function() {
|
||||||
|
return tomSelectInstance;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the Tom Select instance
|
||||||
|
*/
|
||||||
|
destroy: function() {
|
||||||
|
if (tomSelectInstance) {
|
||||||
|
tomSelectInstance.destroy();
|
||||||
|
tomSelectInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Tom Select when available
|
||||||
|
waitForTomSelect(() => {
|
||||||
|
vendorSelectorLog.info('Initializing vendor selector');
|
||||||
|
|
||||||
|
tomSelectInstance = new TomSelect(selectElement, {
|
||||||
|
valueField: 'id',
|
||||||
|
labelField: 'name',
|
||||||
|
searchField: ['name', 'vendor_code'],
|
||||||
|
maxOptions: config.maxOptions,
|
||||||
|
placeholder: config.placeholder,
|
||||||
|
|
||||||
|
// Async search with debouncing
|
||||||
|
load: async function(query, callback) {
|
||||||
|
if (query.length < config.minChars) {
|
||||||
|
callback([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`${config.apiEndpoint}?search=${encodeURIComponent(query)}&limit=${config.maxOptions}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const vendors = (response.vendors || []).map(v => ({
|
||||||
|
id: v.id,
|
||||||
|
name: v.name,
|
||||||
|
vendor_code: v.vendor_code
|
||||||
|
}));
|
||||||
|
|
||||||
|
vendorSelectorLog.info(`Found ${vendors.length} vendors for "${query}"`);
|
||||||
|
callback(vendors);
|
||||||
|
} catch (error) {
|
||||||
|
vendorSelectorLog.error('Vendor search failed:', error);
|
||||||
|
callback([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Custom rendering
|
||||||
|
render: {
|
||||||
|
option: function(data, escape) {
|
||||||
|
return `<div class="flex justify-between items-center py-1">
|
||||||
|
<span class="font-medium">${escape(data.name)}</span>
|
||||||
|
<span class="text-xs text-gray-400 dark:text-gray-500 ml-2 font-mono">${escape(data.vendor_code)}</span>
|
||||||
|
</div>`;
|
||||||
|
},
|
||||||
|
item: function(data, escape) {
|
||||||
|
return `<div class="flex items-center gap-2">
|
||||||
|
<span>${escape(data.name)}</span>
|
||||||
|
<span class="text-xs text-gray-400 font-mono">(${escape(data.vendor_code)})</span>
|
||||||
|
</div>`;
|
||||||
|
},
|
||||||
|
no_results: function() {
|
||||||
|
return '<div class="no-results py-2 px-3 text-gray-500 dark:text-gray-400">No vendors found</div>';
|
||||||
|
},
|
||||||
|
loading: function() {
|
||||||
|
return '<div class="loading py-2 px-3 text-gray-500 dark:text-gray-400">Searching...</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
onChange: function(value) {
|
||||||
|
if (value) {
|
||||||
|
const selectedOption = this.options[value];
|
||||||
|
if (selectedOption) {
|
||||||
|
vendorSelectorLog.info('Vendor selected:', selectedOption);
|
||||||
|
config.onSelect({
|
||||||
|
id: parseInt(value),
|
||||||
|
name: selectedOption.name,
|
||||||
|
vendor_code: selectedOption.vendor_code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vendorSelectorLog.info('Vendor selection cleared');
|
||||||
|
config.onClear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Performance settings
|
||||||
|
loadThrottle: 150, // Debounce search requests
|
||||||
|
closeAfterSelect: true,
|
||||||
|
hideSelected: false,
|
||||||
|
persist: true, // Cache options
|
||||||
|
createOnBlur: false,
|
||||||
|
create: false
|
||||||
|
});
|
||||||
|
|
||||||
|
vendorSelectorLog.info('Vendor selector initialized');
|
||||||
|
});
|
||||||
|
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export to window for global access
|
||||||
|
window.initVendorSelector = initVendorSelector;
|
||||||
Reference in New Issue
Block a user