[FIX] website_sale_aplicoop: Remove redundant string= attributes and fix OCA linting warnings
- Remove redundant string= from 17 field definitions where name matches string value (W8113) - Convert @staticmethod to instance methods in selection methods for proper self.env._() access - Fix W8161 (prefer-env-translation) by using self.env._() instead of standalone _() - Fix W8301/W8115 (translation-not-lazy) by proper placement of % interpolation outside self.env._() - Remove unused imports of odoo._ from group_order.py and sale_order_extension.py - All OCA linting warnings in website_sale_aplicoop main models are now resolved Changes: - website_sale_aplicoop/models/group_order.py: 21 field definitions cleaned - website_sale_aplicoop/models/sale_order_extension.py: 5 field definitions cleaned + @staticmethod conversion - Consistent with OCA standards for addon submission
This commit is contained in:
parent
5c89795e30
commit
6fbc7b9456
73 changed files with 5386 additions and 4354 deletions
|
|
@ -1,8 +1,8 @@
|
|||
# CSS Architecture - Website Sale Aplicoop
|
||||
|
||||
**Refactoring Date**: 7 de febrero de 2026
|
||||
**Status**: ✅ Complete
|
||||
**Previous Size**: 2,986 líneas en 1 archivo
|
||||
**Refactoring Date**: 7 de febrero de 2026
|
||||
**Status**: ✅ Complete
|
||||
**Previous Size**: 2,986 líneas en 1 archivo
|
||||
**New Size**: ~400 líneas distribuidas en 15 archivos modulares
|
||||
|
||||
---
|
||||
|
|
@ -59,43 +59,43 @@ website_sale_aplicoop/static/src/css/
|
|||
## 📊 Desglose de Archivos
|
||||
|
||||
### **base/** - Fundamentos
|
||||
- **variables.css** (~80 líneas)
|
||||
- **variables.css** (~80 líneas)
|
||||
Colores, tipografía, espaciados, sombras, transiciones, z-index
|
||||
- **utilities.css** (~15 líneas)
|
||||
- **utilities.css** (~15 líneas)
|
||||
Clases utilitarias reutilizables (.sr-only, .text-muted, etc)
|
||||
|
||||
### **layout/** - Estructura Global
|
||||
- **pages.css** (~70 líneas)
|
||||
- **pages.css** (~70 líneas)
|
||||
Fondos de página, gradientes, pseudo-elementos (::before)
|
||||
- **header.css** (~100 líneas)
|
||||
- **header.css** (~100 líneas)
|
||||
Headers, navegación, títulos, información de pedidos
|
||||
- **responsive.css** (~200 líneas)
|
||||
- **responsive.css** (~200 líneas)
|
||||
Todas las media queries (4 breakpoints: 992px, 768px, 576px, etc)
|
||||
|
||||
### **components/** - Elementos Reutilizables
|
||||
- **product-card.css** (~80 líneas)
|
||||
- **product-card.css** (~80 líneas)
|
||||
Tarjetas de producto con hover, imagen, título, precio
|
||||
- **order-card.css** (~100 líneas)
|
||||
- **order-card.css** (~100 líneas)
|
||||
Tarjetas de orden (Eskaera) con metadatos, badges
|
||||
- **cart.css** (~150 líneas)
|
||||
- **cart.css** (~150 líneas)
|
||||
Carrito lateral, items, total, botones save/reload
|
||||
- **buttons.css** (~80 líneas)
|
||||
- **buttons.css** (~80 líneas)
|
||||
Botones primarios, checkout, acciones
|
||||
- **quantity-control.css** (~100 líneas)
|
||||
- **quantity-control.css** (~100 líneas)
|
||||
Control de cantidad (spinners + input numérico)
|
||||
- **forms.css** (~70 líneas)
|
||||
- **forms.css** (~70 líneas)
|
||||
Inputs, selects, checkboxes, labels
|
||||
- **alerts.css** (~50 líneas)
|
||||
- **alerts.css** (~50 líneas)
|
||||
Alertas, notificaciones, toasts
|
||||
|
||||
### **sections/** - Layouts Específicos de Página
|
||||
- **products-grid.css** (~25 líneas)
|
||||
- **products-grid.css** (~25 líneas)
|
||||
Grid de productos con responsive
|
||||
- **order-list.css** (~40 líneas)
|
||||
- **order-list.css** (~40 líneas)
|
||||
Lista de órdenes (Eskaera page)
|
||||
- **checkout.css** (~100 líneas)
|
||||
- **checkout.css** (~100 líneas)
|
||||
Tabla de checkout, totales, summary
|
||||
- **info-cards.css** (~50 líneas)
|
||||
- **info-cards.css** (~50 líneas)
|
||||
Tarjetas de información, metadatos
|
||||
|
||||
---
|
||||
|
|
@ -183,7 +183,7 @@ Permitiría mejor nesting y variables más poderosas.
|
|||
|
||||
## 📈 Cambios Visuales
|
||||
|
||||
✅ **NINGUNO** - La refactorización es solo organizacional
|
||||
✅ **NINGUNO** - La refactorización es solo organizacional
|
||||
El CSS compilado genera **exactamente el mismo output** que antes.
|
||||
|
||||
---
|
||||
|
|
@ -231,6 +231,6 @@ grep -r "components/\|sections/\|base/\|layout/" css/website_sale.css
|
|||
|
||||
---
|
||||
|
||||
**Mantenido por**: Equipo de Frontend
|
||||
**Última actualización**: 7 de febrero de 2026
|
||||
**Mantenido por**: Equipo de Frontend
|
||||
**Última actualización**: 7 de febrero de 2026
|
||||
**Licencia**: AGPL-3.0
|
||||
|
|
|
|||
|
|
@ -16,26 +16,27 @@
|
|||
--info-color: #17a2b8;
|
||||
--light-color: #f8f9fa;
|
||||
--dark-color: #2d3748;
|
||||
|
||||
|
||||
/* Text colors */
|
||||
--text-primary: #1a202c;
|
||||
--text-secondary: #4a5568;
|
||||
--text-muted: #6b7280;
|
||||
|
||||
|
||||
/* Border colors */
|
||||
--border-light: #e2e8f0;
|
||||
--border-medium: #cbd5e0;
|
||||
--border-dark: #718096;
|
||||
|
||||
|
||||
/* ========== TYPOGRAPHY ========== */
|
||||
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
|
||||
Arial, sans-serif;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-extrabold: 800;
|
||||
|
||||
|
||||
/* ========== SPACING ========== */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
|
|
@ -43,23 +44,23 @@
|
|||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
--spacing-2xl: 3rem;
|
||||
|
||||
|
||||
/* ========== BORDER RADIUS ========== */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
|
||||
|
||||
/* ========== SHADOWS ========== */
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.12);
|
||||
|
||||
|
||||
/* ========== TRANSITIONS ========== */
|
||||
--transition-fast: 200ms ease;
|
||||
--transition-normal: 320ms cubic-bezier(.2, .9, .2, 1);
|
||||
--transition-normal: 320ms cubic-bezier(0.2, 0.9, 0.2, 1);
|
||||
--transition-slow: 500ms ease;
|
||||
|
||||
|
||||
/* ========== Z-INDEX ========== */
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1020;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
border: 1px solid rgba(90, 103, 216, 0.12);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 6px 18px rgba(28, 37, 80, 0.06);
|
||||
transition: transform 320ms cubic-bezier(.2, .9, .2, 1), box-shadow 320ms, border-color 320ms, background 320ms;
|
||||
transition: transform 320ms cubic-bezier(0.2, 0.9, 0.2, 1), box-shadow 320ms, border-color 320ms,
|
||||
background 320ms;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -139,7 +140,7 @@
|
|||
}
|
||||
|
||||
.eskaera-order-card .btn::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,11 @@
|
|||
}
|
||||
|
||||
.product-card:hover .card-body {
|
||||
background: linear-gradient(135deg, rgba(108, 117, 125, 0.10) 0%, rgba(108, 117, 125, 0.08) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(108, 117, 125, 0.1) 0%,
|
||||
rgba(108, 117, 125, 0.08) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.product-card .card-title {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
.add-to-cart-form .input-group {
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/**
|
||||
* Tag Filter Badges Component
|
||||
*
|
||||
*
|
||||
* Styles for interactive tag filter badges in the product search/filter bar.
|
||||
* Badges toggle between secondary (unselected) and primary (selected) states.
|
||||
*/
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
.tag-filter-badges {
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
|
||||
.tag-filter-badge {
|
||||
padding: 0.375rem 0.625rem;
|
||||
font-size: 0.8125rem;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@
|
|||
* Page backgrounds and main layout structures
|
||||
*/
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
background-color: transparent !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
body.website_published {
|
||||
background: linear-gradient(135deg,
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--primary-color) 30%, white),
|
||||
color-mix(in srgb, var(--primary-color) 60%, black)
|
||||
) !important;
|
||||
|
|
@ -32,21 +34,24 @@ body.website_published .eskaera-checkout-page {
|
|||
|
||||
.eskaera-page,
|
||||
.eskaera-generic-page {
|
||||
background: linear-gradient(180deg,
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--primary-color) 10%, white),
|
||||
color-mix(in srgb, var(--primary-color) 70%, black)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.eskaera-shop-page {
|
||||
background: linear-gradient(135deg,
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--primary-color) 10%, white),
|
||||
color-mix(in srgb, var(--primary-color) 10%, rgb(135, 135, 135))
|
||||
) !important;
|
||||
}
|
||||
|
||||
.eskaera-checkout-page {
|
||||
background: linear-gradient(-135deg,
|
||||
background: linear-gradient(
|
||||
-135deg,
|
||||
color-mix(in srgb, var(--primary-color) 0%, white),
|
||||
color-mix(in srgb, var(--primary-color) 60%, black)
|
||||
) !important;
|
||||
|
|
@ -54,29 +59,54 @@ body.website_published .eskaera-checkout-page {
|
|||
|
||||
.eskaera-page::before,
|
||||
.eskaera-generic-page::before {
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%, color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 20%, color-mix(in srgb, var(--primary-color, white) 15%, transparent) 0%, transparent 50%);
|
||||
background-image: radial-gradient(
|
||||
circle at 20% 50%,
|
||||
color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 80%,
|
||||
color-mix(in srgb, var(--primary-color) 25%, transparent) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 40% 20%,
|
||||
color-mix(in srgb, var(--primary-color, white) 15%, transparent) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
}
|
||||
|
||||
.eskaera-shop-page::before {
|
||||
background-image:
|
||||
radial-gradient(circle at 15% 30%, color-mix(in srgb, var(--primary-color, white) 18%, transparent) 0%, transparent 50%),
|
||||
radial-gradient(circle at 85% 70%, color-mix(in srgb, var(--primary-color) 22%, transparent) 0%, transparent 50%);
|
||||
background-image: radial-gradient(
|
||||
circle at 15% 30%,
|
||||
color-mix(in srgb, var(--primary-color, white) 18%, transparent) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 85% 70%,
|
||||
color-mix(in srgb, var(--primary-color) 22%, transparent) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
}
|
||||
|
||||
.eskaera-checkout-page::before {
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%, color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, transparent 50%);
|
||||
background-image: radial-gradient(
|
||||
circle at 20% 50%,
|
||||
color-mix(in srgb, var(--primary-color, white) 20%, transparent) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 80%,
|
||||
color-mix(in srgb, var(--primary-color) 25%, transparent) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
}
|
||||
|
||||
.eskaera-page::before,
|
||||
.eskaera-shop-page::before,
|
||||
.eskaera-generic-page::before,
|
||||
.eskaera-checkout-page::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
|
|||
|
|
@ -17,20 +17,20 @@
|
|||
.cart-items {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
|
||||
#cart-items-container {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.list-group-item {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.list-group-item h6 {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
|
||||
.list-group-item strong {
|
||||
min-width: 70px;
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
.cart-header h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
|
||||
.cart-title-lg {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
|
@ -476,13 +476,13 @@
|
|||
.product-tags {
|
||||
font-size: 1.1rem !important;
|
||||
}
|
||||
|
||||
|
||||
/* Scale down quantity input for 6-column layout */
|
||||
.add-to-cart-form .product-qty {
|
||||
font-size: 0.85rem;
|
||||
max-width: 55px;
|
||||
}
|
||||
|
||||
|
||||
.add-to-cart-form .qty-decrease,
|
||||
.add-to-cart-form .qty-increase {
|
||||
font-size: 0.75rem;
|
||||
|
|
@ -495,13 +495,13 @@
|
|||
.product-tags {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
|
||||
/* Scale down quantity input for 5-column layout */
|
||||
.add-to-cart-form .product-qty {
|
||||
font-size: 0.9rem;
|
||||
max-width: 60px;
|
||||
}
|
||||
|
||||
|
||||
.add-to-cart-form .qty-decrease,
|
||||
.add-to-cart-form .qty-increase {
|
||||
font-size: 0.8rem;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
align-items: start;
|
||||
margin-top: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.card-meta-compact {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Website Sale Aplicoop - Main CSS Index File
|
||||
* This file imports all component stylesheets in the correct order
|
||||
*
|
||||
*
|
||||
* Architecture:
|
||||
* 1. Base & Variables (colors, spacing, typography)
|
||||
* 2. Layout & Pages (page backgrounds, containers)
|
||||
|
|
@ -15,36 +15,36 @@
|
|||
/* ============================================
|
||||
1. BASE & VARIABLES
|
||||
============================================ */
|
||||
@import 'base/variables.css';
|
||||
@import 'base/utilities.css';
|
||||
@import "base/variables.css";
|
||||
@import "base/utilities.css";
|
||||
|
||||
/* ============================================
|
||||
2. LAYOUT & PAGES
|
||||
============================================ */
|
||||
@import 'layout/pages.css';
|
||||
@import 'layout/header.css';
|
||||
@import "layout/pages.css";
|
||||
@import "layout/header.css";
|
||||
|
||||
/* ============================================
|
||||
3. COMPONENTS (Reusable UI elements)
|
||||
============================================ */
|
||||
@import 'components/product-card.css';
|
||||
@import 'components/order-card.css';
|
||||
@import 'components/cart.css';
|
||||
@import 'components/buttons.css';
|
||||
@import 'components/quantity-control.css';
|
||||
@import 'components/forms.css';
|
||||
@import 'components/alerts.css';
|
||||
@import 'components/tag-filter.css';
|
||||
@import "components/product-card.css";
|
||||
@import "components/order-card.css";
|
||||
@import "components/cart.css";
|
||||
@import "components/buttons.css";
|
||||
@import "components/quantity-control.css";
|
||||
@import "components/forms.css";
|
||||
@import "components/alerts.css";
|
||||
@import "components/tag-filter.css";
|
||||
|
||||
/* ============================================
|
||||
4. SECTIONS (Page-specific layouts)
|
||||
============================================ */
|
||||
@import 'sections/products-grid.css';
|
||||
@import 'sections/order-list.css';
|
||||
@import 'sections/checkout.css';
|
||||
@import 'sections/info-cards.css';
|
||||
@import "sections/products-grid.css";
|
||||
@import "sections/order-list.css";
|
||||
@import "sections/checkout.css";
|
||||
@import "sections/info-cards.css";
|
||||
|
||||
/* ============================================
|
||||
5. RESPONSIVE DESIGN (Media queries)
|
||||
============================================ */
|
||||
@import 'layout/responsive.css';
|
||||
@import "layout/responsive.css";
|
||||
|
|
|
|||
|
|
@ -5,140 +5,158 @@
|
|||
* before rendering the checkout summary.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
console.log('[CHECKOUT] Script loaded');
|
||||
console.log("[CHECKOUT] Script loaded");
|
||||
|
||||
// Get order ID from button
|
||||
var confirmBtn = document.getElementById('confirm-order-btn');
|
||||
var confirmBtn = document.getElementById("confirm-order-btn");
|
||||
if (!confirmBtn) {
|
||||
console.log('[CHECKOUT] No confirm button found');
|
||||
console.log("[CHECKOUT] No confirm button found");
|
||||
return;
|
||||
}
|
||||
|
||||
var orderId = confirmBtn.getAttribute('data-order-id');
|
||||
var orderId = confirmBtn.getAttribute("data-order-id");
|
||||
if (!orderId) {
|
||||
console.log('[CHECKOUT] No order ID found');
|
||||
console.log("[CHECKOUT] No order ID found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[CHECKOUT] Order ID:', orderId);
|
||||
console.log("[CHECKOUT] Order ID:", orderId);
|
||||
|
||||
// Get summary div
|
||||
var summaryDiv = document.getElementById('checkout-summary');
|
||||
var summaryDiv = document.getElementById("checkout-summary");
|
||||
if (!summaryDiv) {
|
||||
console.log('[CHECKOUT] No summary div found');
|
||||
console.log("[CHECKOUT] No summary div found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to fetch labels and render checkout
|
||||
var fetchLabelsAndRender = function() {
|
||||
console.log('[CHECKOUT] Fetching labels...');
|
||||
|
||||
var fetchLabelsAndRender = function () {
|
||||
console.log("[CHECKOUT] Fetching labels...");
|
||||
|
||||
// Wait for window.groupOrderShop.labels to be initialized (contains hardcoded labels)
|
||||
var waitForLabels = function(callback, maxWait = 3000, checkInterval = 50) {
|
||||
var waitForLabels = function (callback, maxWait = 3000, checkInterval = 50) {
|
||||
var startTime = Date.now();
|
||||
var checkLabels = function() {
|
||||
if (window.groupOrderShop && window.groupOrderShop.labels && Object.keys(window.groupOrderShop.labels).length > 0) {
|
||||
console.log('[CHECKOUT] ✅ Hardcoded labels found, proceeding');
|
||||
var checkLabels = function () {
|
||||
if (
|
||||
window.groupOrderShop &&
|
||||
window.groupOrderShop.labels &&
|
||||
Object.keys(window.groupOrderShop.labels).length > 0
|
||||
) {
|
||||
console.log("[CHECKOUT] ✅ Hardcoded labels found, proceeding");
|
||||
callback();
|
||||
} else if (Date.now() - startTime < maxWait) {
|
||||
setTimeout(checkLabels, checkInterval);
|
||||
} else {
|
||||
console.log('[CHECKOUT] ⚠️ Timeout waiting for labels, proceeding anyway');
|
||||
console.log("[CHECKOUT] ⚠️ Timeout waiting for labels, proceeding anyway");
|
||||
callback();
|
||||
}
|
||||
};
|
||||
checkLabels();
|
||||
};
|
||||
|
||||
waitForLabels(function() {
|
||||
|
||||
waitForLabels(function () {
|
||||
// Now fetch additional labels from server
|
||||
// Detect current language from document or navigator
|
||||
var currentLang = document.documentElement.lang ||
|
||||
document.documentElement.getAttribute('lang') ||
|
||||
navigator.language ||
|
||||
'es_ES';
|
||||
console.log('[CHECKOUT] Detected language:', currentLang);
|
||||
|
||||
fetch('/eskaera/labels', {
|
||||
method: 'POST',
|
||||
var currentLang =
|
||||
document.documentElement.lang ||
|
||||
document.documentElement.getAttribute("lang") ||
|
||||
navigator.language ||
|
||||
"es_ES";
|
||||
console.log("[CHECKOUT] Detected language:", currentLang);
|
||||
|
||||
fetch("/eskaera/labels", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
lang: currentLang
|
||||
lang: currentLang,
|
||||
}),
|
||||
})
|
||||
.then(function (response) {
|
||||
console.log("[CHECKOUT] Response status:", response.status);
|
||||
return response.json();
|
||||
})
|
||||
})
|
||||
.then(function(response) {
|
||||
console.log('[CHECKOUT] Response status:', response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
console.log('[CHECKOUT] Response data:', data);
|
||||
var serverLabels = data.result || data;
|
||||
console.log('[CHECKOUT] Server labels count:', Object.keys(serverLabels).length);
|
||||
console.log('[CHECKOUT] Sample server labels:', {
|
||||
draft_merged_success: serverLabels.draft_merged_success,
|
||||
home_delivery: serverLabels.home_delivery
|
||||
});
|
||||
|
||||
// CRITICAL: Merge server labels with existing hardcoded labels
|
||||
// Hardcoded labels MUST take precedence over server labels
|
||||
if (window.groupOrderShop && window.groupOrderShop.labels) {
|
||||
var existingLabels = window.groupOrderShop.labels;
|
||||
console.log('[CHECKOUT] Existing hardcoded labels count:', Object.keys(existingLabels).length);
|
||||
console.log('[CHECKOUT] Sample existing labels:', {
|
||||
draft_merged_success: existingLabels.draft_merged_success,
|
||||
home_delivery: existingLabels.home_delivery
|
||||
.then(function (data) {
|
||||
console.log("[CHECKOUT] Response data:", data);
|
||||
var serverLabels = data.result || data;
|
||||
console.log(
|
||||
"[CHECKOUT] Server labels count:",
|
||||
Object.keys(serverLabels).length
|
||||
);
|
||||
console.log("[CHECKOUT] Sample server labels:", {
|
||||
draft_merged_success: serverLabels.draft_merged_success,
|
||||
home_delivery: serverLabels.home_delivery,
|
||||
});
|
||||
|
||||
// Start with server labels, then overwrite with hardcoded ones
|
||||
var mergedLabels = Object.assign({}, serverLabels);
|
||||
Object.assign(mergedLabels, existingLabels);
|
||||
|
||||
window.groupOrderShop.labels = mergedLabels;
|
||||
console.log('[CHECKOUT] ✅ Merged labels - final count:', Object.keys(mergedLabels).length);
|
||||
console.log('[CHECKOUT] Verification:', {
|
||||
draft_merged_success: mergedLabels.draft_merged_success,
|
||||
home_delivery: mergedLabels.home_delivery
|
||||
});
|
||||
} else {
|
||||
// If no existing labels, use server labels as fallback
|
||||
if (window.groupOrderShop) {
|
||||
window.groupOrderShop.labels = serverLabels;
|
||||
|
||||
// CRITICAL: Merge server labels with existing hardcoded labels
|
||||
// Hardcoded labels MUST take precedence over server labels
|
||||
if (window.groupOrderShop && window.groupOrderShop.labels) {
|
||||
var existingLabels = window.groupOrderShop.labels;
|
||||
console.log(
|
||||
"[CHECKOUT] Existing hardcoded labels count:",
|
||||
Object.keys(existingLabels).length
|
||||
);
|
||||
console.log("[CHECKOUT] Sample existing labels:", {
|
||||
draft_merged_success: existingLabels.draft_merged_success,
|
||||
home_delivery: existingLabels.home_delivery,
|
||||
});
|
||||
|
||||
// Start with server labels, then overwrite with hardcoded ones
|
||||
var mergedLabels = Object.assign({}, serverLabels);
|
||||
Object.assign(mergedLabels, existingLabels);
|
||||
|
||||
window.groupOrderShop.labels = mergedLabels;
|
||||
console.log(
|
||||
"[CHECKOUT] ✅ Merged labels - final count:",
|
||||
Object.keys(mergedLabels).length
|
||||
);
|
||||
console.log("[CHECKOUT] Verification:", {
|
||||
draft_merged_success: mergedLabels.draft_merged_success,
|
||||
home_delivery: mergedLabels.home_delivery,
|
||||
});
|
||||
} else {
|
||||
// If no existing labels, use server labels as fallback
|
||||
if (window.groupOrderShop) {
|
||||
window.groupOrderShop.labels = serverLabels;
|
||||
}
|
||||
console.log("[CHECKOUT] ⚠️ No existing labels, using server labels");
|
||||
}
|
||||
console.log('[CHECKOUT] ⚠️ No existing labels, using server labels');
|
||||
}
|
||||
|
||||
window.renderCheckoutSummary(window.groupOrderShop.labels);
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('[CHECKOUT] Error:', error);
|
||||
// Fallback to translated labels
|
||||
window.renderCheckoutSummary(window.getCheckoutLabels());
|
||||
});
|
||||
|
||||
window.renderCheckoutSummary(window.groupOrderShop.labels);
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error("[CHECKOUT] Error:", error);
|
||||
// Fallback to translated labels
|
||||
window.renderCheckoutSummary(window.getCheckoutLabels());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Listen for cart ready event instead of polling
|
||||
if (window.groupOrderShop && window.groupOrderShop.orderId) {
|
||||
// Cart already initialized, render immediately
|
||||
console.log('[CHECKOUT] Cart already ready');
|
||||
console.log("[CHECKOUT] Cart already ready");
|
||||
fetchLabelsAndRender();
|
||||
} else {
|
||||
// Wait for cart initialization event
|
||||
console.log('[CHECKOUT] Waiting for cart ready event...');
|
||||
document.addEventListener('groupOrderCartReady', function() {
|
||||
console.log('[CHECKOUT] Cart ready event received');
|
||||
fetchLabelsAndRender();
|
||||
}, { once: true });
|
||||
|
||||
console.log("[CHECKOUT] Waiting for cart ready event...");
|
||||
document.addEventListener(
|
||||
"groupOrderCartReady",
|
||||
function () {
|
||||
console.log("[CHECKOUT] Cart ready event received");
|
||||
fetchLabelsAndRender();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
// Fallback timeout in case event never fires
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
if (window.groupOrderShop && window.groupOrderShop.orderId) {
|
||||
console.log('[CHECKOUT] Fallback timeout triggered');
|
||||
console.log("[CHECKOUT] Fallback timeout triggered");
|
||||
fetchLabelsAndRender();
|
||||
}
|
||||
}, 500);
|
||||
|
|
@ -148,67 +166,88 @@
|
|||
* Render order summary table or empty message
|
||||
* Exposed globally so other scripts can call it
|
||||
*/
|
||||
window.renderCheckoutSummary = function(labels) {
|
||||
window.renderCheckoutSummary = function (labels) {
|
||||
labels = labels || window.getCheckoutLabels();
|
||||
|
||||
var summaryDiv = document.getElementById('checkout-summary');
|
||||
var summaryDiv = document.getElementById("checkout-summary");
|
||||
if (!summaryDiv) return;
|
||||
|
||||
var cartKey = 'eskaera_' + (document.getElementById('confirm-order-btn') ? document.getElementById('confirm-order-btn').getAttribute('data-order-id') : '1') + '_cart';
|
||||
var cart = JSON.parse(localStorage.getItem(cartKey) || '{}');
|
||||
var cartKey =
|
||||
"eskaera_" +
|
||||
(document.getElementById("confirm-order-btn")
|
||||
? document.getElementById("confirm-order-btn").getAttribute("data-order-id")
|
||||
: "1") +
|
||||
"_cart";
|
||||
var cart = JSON.parse(localStorage.getItem(cartKey) || "{}");
|
||||
|
||||
var summaryTable = summaryDiv.querySelector('.checkout-summary-table');
|
||||
var tbody = summaryDiv.querySelector('#checkout-summary-tbody');
|
||||
var totalSection = summaryDiv.querySelector('.checkout-total-section');
|
||||
var summaryTable = summaryDiv.querySelector(".checkout-summary-table");
|
||||
var tbody = summaryDiv.querySelector("#checkout-summary-tbody");
|
||||
var totalSection = summaryDiv.querySelector(".checkout-total-section");
|
||||
|
||||
// If no table found, create it with headers (shouldn't happen, but fallback)
|
||||
if (!summaryTable) {
|
||||
var html = '<table class="table table-hover checkout-summary-table" id="checkout-summary-table" role="grid" aria-label="Purchase summary"><thead class="table-dark"><tr>' +
|
||||
'<th scope="col" class="col-name">' + escapeHtml(labels.product) + '</th>' +
|
||||
'<th scope="col" class="col-qty text-center">' + escapeHtml(labels.quantity) + '</th>' +
|
||||
'<th scope="col" class="col-price text-right">' + escapeHtml(labels.price) + '</th>' +
|
||||
'<th scope="col" class="col-subtotal text-right">' + escapeHtml(labels.subtotal) + '</th>' +
|
||||
var html =
|
||||
'<table class="table table-hover checkout-summary-table" id="checkout-summary-table" role="grid" aria-label="Purchase summary"><thead class="table-dark"><tr>' +
|
||||
'<th scope="col" class="col-name">' +
|
||||
escapeHtml(labels.product) +
|
||||
"</th>" +
|
||||
'<th scope="col" class="col-qty text-center">' +
|
||||
escapeHtml(labels.quantity) +
|
||||
"</th>" +
|
||||
'<th scope="col" class="col-price text-right">' +
|
||||
escapeHtml(labels.price) +
|
||||
"</th>" +
|
||||
'<th scope="col" class="col-subtotal text-right">' +
|
||||
escapeHtml(labels.subtotal) +
|
||||
"</th>" +
|
||||
'</tr></thead><tbody id="checkout-summary-tbody"></tbody></table>' +
|
||||
'<div class="checkout-total-section"><div class="total-row">' +
|
||||
'<span class="total-label">' + escapeHtml(labels.total) + '</span>' +
|
||||
'<span class="total-label">' +
|
||||
escapeHtml(labels.total) +
|
||||
"</span>" +
|
||||
'<span class="total-amount" id="checkout-total-amount">€0.00</span>' +
|
||||
'</div></div>';
|
||||
"</div></div>";
|
||||
summaryDiv.innerHTML = html;
|
||||
summaryTable = summaryDiv.querySelector('.checkout-summary-table');
|
||||
tbody = summaryDiv.querySelector('#checkout-summary-tbody');
|
||||
totalSection = summaryDiv.querySelector('.checkout-total-section');
|
||||
summaryTable = summaryDiv.querySelector(".checkout-summary-table");
|
||||
tbody = summaryDiv.querySelector("#checkout-summary-tbody");
|
||||
totalSection = summaryDiv.querySelector(".checkout-total-section");
|
||||
}
|
||||
|
||||
// Clear only tbody, preserve headers
|
||||
tbody.innerHTML = '';
|
||||
tbody.innerHTML = "";
|
||||
|
||||
if (Object.keys(cart).length === 0) {
|
||||
// Show empty message if cart is empty
|
||||
var emptyRow = document.createElement('tr');
|
||||
emptyRow.id = 'checkout-empty-row';
|
||||
emptyRow.className = 'empty-message';
|
||||
emptyRow.innerHTML = '<td colspan="4" class="text-center text-muted py-4">' +
|
||||
var emptyRow = document.createElement("tr");
|
||||
emptyRow.id = "checkout-empty-row";
|
||||
emptyRow.className = "empty-message";
|
||||
emptyRow.innerHTML =
|
||||
'<td colspan="4" class="text-center text-muted py-4">' +
|
||||
'<i class="fa fa-inbox fa-2x mb-2"></i>' +
|
||||
'<p>' + escapeHtml(labels.empty) + '</p>' +
|
||||
'</td>';
|
||||
"<p>" +
|
||||
escapeHtml(labels.empty) +
|
||||
"</p>" +
|
||||
"</td>";
|
||||
tbody.appendChild(emptyRow);
|
||||
|
||||
|
||||
// Hide total section
|
||||
totalSection.style.display = 'none';
|
||||
totalSection.style.display = "none";
|
||||
} else {
|
||||
// Hide empty row if visible
|
||||
var emptyRow = tbody.querySelector('#checkout-empty-row');
|
||||
var emptyRow = tbody.querySelector("#checkout-empty-row");
|
||||
if (emptyRow) emptyRow.remove();
|
||||
|
||||
// Get delivery product ID from page data
|
||||
var checkoutPage = document.querySelector('.eskaera-checkout-page');
|
||||
var deliveryProductId = checkoutPage ? checkoutPage.getAttribute('data-delivery-product-id') : null;
|
||||
var checkoutPage = document.querySelector(".eskaera-checkout-page");
|
||||
var deliveryProductId = checkoutPage
|
||||
? checkoutPage.getAttribute("data-delivery-product-id")
|
||||
: null;
|
||||
|
||||
// Separate normal products from delivery product
|
||||
var normalProducts = [];
|
||||
var deliveryProduct = null;
|
||||
|
||||
Object.keys(cart).forEach(function(productId) {
|
||||
|
||||
Object.keys(cart).forEach(function (productId) {
|
||||
if (productId === deliveryProductId) {
|
||||
deliveryProduct = { id: productId, item: cart[productId] };
|
||||
} else {
|
||||
|
|
@ -217,14 +256,14 @@
|
|||
});
|
||||
|
||||
// Sort normal products numerically
|
||||
normalProducts.sort(function(a, b) {
|
||||
normalProducts.sort(function (a, b) {
|
||||
return parseInt(a.id) - parseInt(b.id);
|
||||
});
|
||||
|
||||
var total = 0;
|
||||
|
||||
|
||||
// Render normal products first
|
||||
normalProducts.forEach(function(product) {
|
||||
normalProducts.forEach(function (product) {
|
||||
var item = product.item;
|
||||
var qty = parseFloat(item.quantity || item.qty || 1);
|
||||
if (isNaN(qty)) qty = 1;
|
||||
|
|
@ -233,11 +272,20 @@
|
|||
var subtotal = qty * price;
|
||||
total += subtotal;
|
||||
|
||||
var row = document.createElement('tr');
|
||||
row.innerHTML = '<td>' + escapeHtml(item.name) + '</td>' +
|
||||
'<td class="text-center">' + qty.toFixed(2).replace(/\.?0+$/, '') + '</td>' +
|
||||
'<td class="text-right">€' + price.toFixed(2) + '</td>' +
|
||||
'<td class="text-right">€' + subtotal.toFixed(2) + '</td>';
|
||||
var row = document.createElement("tr");
|
||||
row.innerHTML =
|
||||
"<td>" +
|
||||
escapeHtml(item.name) +
|
||||
"</td>" +
|
||||
'<td class="text-center">' +
|
||||
qty.toFixed(2).replace(/\.?0+$/, "") +
|
||||
"</td>" +
|
||||
'<td class="text-right">€' +
|
||||
price.toFixed(2) +
|
||||
"</td>" +
|
||||
'<td class="text-right">€' +
|
||||
subtotal.toFixed(2) +
|
||||
"</td>";
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
|
|
@ -251,32 +299,41 @@
|
|||
var subtotal = qty * price;
|
||||
total += subtotal;
|
||||
|
||||
var row = document.createElement('tr');
|
||||
row.innerHTML = '<td>' + escapeHtml(item.name) + '</td>' +
|
||||
'<td class="text-center">' + qty.toFixed(2).replace(/\.?0+$/, '') + '</td>' +
|
||||
'<td class="text-right">€' + price.toFixed(2) + '</td>' +
|
||||
'<td class="text-right">€' + subtotal.toFixed(2) + '</td>';
|
||||
var row = document.createElement("tr");
|
||||
row.innerHTML =
|
||||
"<td>" +
|
||||
escapeHtml(item.name) +
|
||||
"</td>" +
|
||||
'<td class="text-center">' +
|
||||
qty.toFixed(2).replace(/\.?0+$/, "") +
|
||||
"</td>" +
|
||||
'<td class="text-right">€' +
|
||||
price.toFixed(2) +
|
||||
"</td>" +
|
||||
'<td class="text-right">€' +
|
||||
subtotal.toFixed(2) +
|
||||
"</td>";
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
|
||||
// Update total
|
||||
var totalAmount = summaryDiv.querySelector('#checkout-total-amount');
|
||||
var totalAmount = summaryDiv.querySelector("#checkout-total-amount");
|
||||
if (totalAmount) {
|
||||
totalAmount.textContent = '€' + total.toFixed(2);
|
||||
totalAmount.textContent = "€" + total.toFixed(2);
|
||||
}
|
||||
|
||||
|
||||
// Show total section
|
||||
totalSection.style.display = 'block';
|
||||
totalSection.style.display = "block";
|
||||
}
|
||||
|
||||
console.log('[CHECKOUT] Summary rendered');
|
||||
console.log("[CHECKOUT] Summary rendered");
|
||||
};
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
var div = document.createElement('div');
|
||||
var div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@
|
|||
* This file is kept for backwards compatibility but is no longer needed.
|
||||
* The main renderSummary() logic is in checkout_labels.js
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
(function () {
|
||||
"use strict";
|
||||
// Checkout rendering is handled by checkout_labels.js
|
||||
})();
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,56 +3,65 @@
|
|||
* Manages home delivery checkbox and product addition/removal
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var HomeDeliveryManager = {
|
||||
deliveryProductId: null,
|
||||
deliveryProductPrice: 5.74,
|
||||
deliveryProductName: 'Home Delivery', // Default fallback
|
||||
deliveryProductName: "Home Delivery", // Default fallback
|
||||
orderId: null,
|
||||
homeDeliveryEnabled: false,
|
||||
|
||||
init: function() {
|
||||
|
||||
init: function () {
|
||||
// Get delivery product info from data attributes
|
||||
var checkoutPage = document.querySelector('.eskaera-checkout-page');
|
||||
var checkoutPage = document.querySelector(".eskaera-checkout-page");
|
||||
if (checkoutPage) {
|
||||
this.deliveryProductId = checkoutPage.getAttribute('data-delivery-product-id');
|
||||
console.log('[HomeDelivery] deliveryProductId from attribute:', this.deliveryProductId, 'type:', typeof this.deliveryProductId);
|
||||
|
||||
var price = checkoutPage.getAttribute('data-delivery-product-price');
|
||||
this.deliveryProductId = checkoutPage.getAttribute("data-delivery-product-id");
|
||||
console.log(
|
||||
"[HomeDelivery] deliveryProductId from attribute:",
|
||||
this.deliveryProductId,
|
||||
"type:",
|
||||
typeof this.deliveryProductId
|
||||
);
|
||||
|
||||
var price = checkoutPage.getAttribute("data-delivery-product-price");
|
||||
if (price) {
|
||||
this.deliveryProductPrice = parseFloat(price);
|
||||
}
|
||||
|
||||
|
||||
// Get translated product name from data attribute (auto-translated by Odoo server)
|
||||
var productName = checkoutPage.getAttribute('data-delivery-product-name');
|
||||
var productName = checkoutPage.getAttribute("data-delivery-product-name");
|
||||
if (productName) {
|
||||
this.deliveryProductName = productName;
|
||||
console.log('[HomeDelivery] Using translated product name from server:', this.deliveryProductName);
|
||||
console.log(
|
||||
"[HomeDelivery] Using translated product name from server:",
|
||||
this.deliveryProductName
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Check if home delivery is enabled for this order
|
||||
var homeDeliveryAttr = checkoutPage.getAttribute('data-home-delivery-enabled');
|
||||
this.homeDeliveryEnabled = homeDeliveryAttr === 'true' || homeDeliveryAttr === 'True';
|
||||
console.log('[HomeDelivery] Home delivery enabled:', this.homeDeliveryEnabled);
|
||||
|
||||
var homeDeliveryAttr = checkoutPage.getAttribute("data-home-delivery-enabled");
|
||||
this.homeDeliveryEnabled =
|
||||
homeDeliveryAttr === "true" || homeDeliveryAttr === "True";
|
||||
console.log("[HomeDelivery] Home delivery enabled:", this.homeDeliveryEnabled);
|
||||
|
||||
// Show/hide home delivery section based on configuration
|
||||
this.toggleHomeDeliverySection();
|
||||
}
|
||||
|
||||
|
||||
// Get order ID from confirm button
|
||||
var confirmBtn = document.getElementById('confirm-order-btn');
|
||||
var confirmBtn = document.getElementById("confirm-order-btn");
|
||||
if (confirmBtn) {
|
||||
this.orderId = confirmBtn.getAttribute('data-order-id');
|
||||
console.log('[HomeDelivery] orderId from button:', this.orderId);
|
||||
this.orderId = confirmBtn.getAttribute("data-order-id");
|
||||
console.log("[HomeDelivery] orderId from button:", this.orderId);
|
||||
}
|
||||
|
||||
var checkbox = document.getElementById('home-delivery-checkbox');
|
||||
var checkbox = document.getElementById("home-delivery-checkbox");
|
||||
if (!checkbox) return;
|
||||
|
||||
var self = this;
|
||||
checkbox.addEventListener('change', function() {
|
||||
checkbox.addEventListener("change", function () {
|
||||
if (this.checked) {
|
||||
self.addDeliveryProduct();
|
||||
self.showDeliveryInfo();
|
||||
|
|
@ -66,42 +75,44 @@
|
|||
this.checkDeliveryInCart();
|
||||
},
|
||||
|
||||
toggleHomeDeliverySection: function() {
|
||||
var homeDeliverySection = document.querySelector('[id*="home-delivery"], [class*="home-delivery"]');
|
||||
var checkbox = document.getElementById('home-delivery-checkbox');
|
||||
var homeDeliveryContainer = document.getElementById('home-delivery-container');
|
||||
|
||||
toggleHomeDeliverySection: function () {
|
||||
var homeDeliverySection = document.querySelector(
|
||||
'[id*="home-delivery"], [class*="home-delivery"]'
|
||||
);
|
||||
var checkbox = document.getElementById("home-delivery-checkbox");
|
||||
var homeDeliveryContainer = document.getElementById("home-delivery-container");
|
||||
|
||||
if (this.homeDeliveryEnabled) {
|
||||
// Show home delivery option
|
||||
if (checkbox) {
|
||||
checkbox.closest('.form-check').style.display = 'block';
|
||||
checkbox.closest(".form-check").style.display = "block";
|
||||
}
|
||||
if (homeDeliveryContainer) {
|
||||
homeDeliveryContainer.style.display = 'block';
|
||||
homeDeliveryContainer.style.display = "block";
|
||||
}
|
||||
console.log('[HomeDelivery] Home delivery option shown');
|
||||
console.log("[HomeDelivery] Home delivery option shown");
|
||||
} else {
|
||||
// Hide home delivery option and delivery info alert
|
||||
if (checkbox) {
|
||||
checkbox.closest('.form-check').style.display = 'none';
|
||||
checkbox.closest(".form-check").style.display = "none";
|
||||
checkbox.checked = false;
|
||||
}
|
||||
if (homeDeliveryContainer) {
|
||||
homeDeliveryContainer.style.display = 'none';
|
||||
homeDeliveryContainer.style.display = "none";
|
||||
}
|
||||
// Also hide the delivery info alert when home delivery is disabled
|
||||
this.hideDeliveryInfo();
|
||||
this.removeDeliveryProduct();
|
||||
console.log('[HomeDelivery] Home delivery option and delivery info hidden');
|
||||
console.log("[HomeDelivery] Home delivery option and delivery info hidden");
|
||||
}
|
||||
},
|
||||
|
||||
checkDeliveryInCart: function() {
|
||||
checkDeliveryInCart: function () {
|
||||
if (!this.deliveryProductId) return;
|
||||
|
||||
var cart = this.getCart();
|
||||
if (cart[this.deliveryProductId]) {
|
||||
var checkbox = document.getElementById('home-delivery-checkbox');
|
||||
var checkbox = document.getElementById("home-delivery-checkbox");
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
this.showDeliveryInfo();
|
||||
|
|
@ -109,93 +120,103 @@
|
|||
}
|
||||
},
|
||||
|
||||
getCart: function() {
|
||||
getCart: function () {
|
||||
if (!this.orderId) return {};
|
||||
var cartKey = 'eskaera_' + this.orderId + '_cart';
|
||||
var cartKey = "eskaera_" + this.orderId + "_cart";
|
||||
var cartStr = localStorage.getItem(cartKey);
|
||||
return cartStr ? JSON.parse(cartStr) : {};
|
||||
},
|
||||
|
||||
saveCart: function(cart) {
|
||||
saveCart: function (cart) {
|
||||
if (!this.orderId) return;
|
||||
var cartKey = 'eskaera_' + this.orderId + '_cart';
|
||||
var cartKey = "eskaera_" + this.orderId + "_cart";
|
||||
localStorage.setItem(cartKey, JSON.stringify(cart));
|
||||
|
||||
|
||||
// Re-render checkout summary without reloading
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
// Use the global function from checkout_labels.js
|
||||
if (typeof window.renderCheckoutSummary === 'function') {
|
||||
if (typeof window.renderCheckoutSummary === "function") {
|
||||
window.renderCheckoutSummary();
|
||||
}
|
||||
}, 50);
|
||||
},
|
||||
|
||||
renderCheckoutSummary: function() {
|
||||
|
||||
renderCheckoutSummary: function () {
|
||||
// Stub - now handled by global window.renderCheckoutSummary
|
||||
},
|
||||
|
||||
addDeliveryProduct: function() {
|
||||
addDeliveryProduct: function () {
|
||||
if (!this.deliveryProductId) {
|
||||
console.warn('[HomeDelivery] Delivery product ID not found');
|
||||
console.warn("[HomeDelivery] Delivery product ID not found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[HomeDelivery] Adding delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId);
|
||||
console.log(
|
||||
"[HomeDelivery] Adding delivery product - deliveryProductId:",
|
||||
this.deliveryProductId,
|
||||
"orderId:",
|
||||
this.orderId
|
||||
);
|
||||
var cart = this.getCart();
|
||||
console.log('[HomeDelivery] Current cart before adding:', cart);
|
||||
|
||||
console.log("[HomeDelivery] Current cart before adding:", cart);
|
||||
|
||||
cart[this.deliveryProductId] = {
|
||||
id: this.deliveryProductId,
|
||||
name: this.deliveryProductName,
|
||||
price: this.deliveryProductPrice,
|
||||
qty: 1
|
||||
qty: 1,
|
||||
};
|
||||
console.log('[HomeDelivery] Cart after adding delivery:', cart);
|
||||
console.log("[HomeDelivery] Cart after adding delivery:", cart);
|
||||
this.saveCart(cart);
|
||||
console.log('[HomeDelivery] Delivery product added to localStorage');
|
||||
console.log("[HomeDelivery] Delivery product added to localStorage");
|
||||
},
|
||||
|
||||
removeDeliveryProduct: function() {
|
||||
removeDeliveryProduct: function () {
|
||||
if (!this.deliveryProductId) {
|
||||
console.warn('[HomeDelivery] Delivery product ID not found');
|
||||
console.warn("[HomeDelivery] Delivery product ID not found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[HomeDelivery] Removing delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId);
|
||||
console.log(
|
||||
"[HomeDelivery] Removing delivery product - deliveryProductId:",
|
||||
this.deliveryProductId,
|
||||
"orderId:",
|
||||
this.orderId
|
||||
);
|
||||
var cart = this.getCart();
|
||||
console.log('[HomeDelivery] Current cart before removing:', cart);
|
||||
|
||||
console.log("[HomeDelivery] Current cart before removing:", cart);
|
||||
|
||||
if (cart[this.deliveryProductId]) {
|
||||
delete cart[this.deliveryProductId];
|
||||
console.log('[HomeDelivery] Cart after removing delivery:', cart);
|
||||
console.log("[HomeDelivery] Cart after removing delivery:", cart);
|
||||
}
|
||||
this.saveCart(cart);
|
||||
console.log('[HomeDelivery] Delivery product removed from localStorage');
|
||||
console.log("[HomeDelivery] Delivery product removed from localStorage");
|
||||
},
|
||||
|
||||
showDeliveryInfo: function() {
|
||||
var alert = document.getElementById('delivery-info-alert');
|
||||
showDeliveryInfo: function () {
|
||||
var alert = document.getElementById("delivery-info-alert");
|
||||
if (alert) {
|
||||
console.log('[HomeDelivery] Showing delivery info alert');
|
||||
alert.classList.remove('d-none');
|
||||
alert.style.display = 'block';
|
||||
console.log("[HomeDelivery] Showing delivery info alert");
|
||||
alert.classList.remove("d-none");
|
||||
alert.style.display = "block";
|
||||
}
|
||||
},
|
||||
|
||||
hideDeliveryInfo: function() {
|
||||
var alert = document.getElementById('delivery-info-alert');
|
||||
hideDeliveryInfo: function () {
|
||||
var alert = document.getElementById("delivery-info-alert");
|
||||
if (alert) {
|
||||
console.log('[HomeDelivery] Hiding delivery info alert');
|
||||
alert.classList.add('d-none');
|
||||
alert.style.display = 'none';
|
||||
console.log("[HomeDelivery] Hiding delivery info alert");
|
||||
alert.classList.add("d-none");
|
||||
alert.style.display = "none";
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
HomeDeliveryManager.init();
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
/**
|
||||
* DEPRECATED: Use i18n_manager.js instead
|
||||
*
|
||||
*
|
||||
* This file is kept for backwards compatibility only.
|
||||
* All translation logic has been moved to i18n_manager.js which
|
||||
* fetches translations from the server endpoint /eskaera/i18n
|
||||
*
|
||||
*
|
||||
* Migration guide:
|
||||
* OLD: window.getCheckoutLabels()
|
||||
* NEW: i18nManager.getAll()
|
||||
*
|
||||
*
|
||||
* OLD: window.formatCurrency(amount)
|
||||
* NEW: i18nManager.formatCurrency(amount)
|
||||
*
|
||||
*
|
||||
* Copyright 2025 Criptomart
|
||||
* License AGPL-3.0 or later
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Keep legacy functions as wrappers for backwards compatibility
|
||||
|
||||
|
||||
/**
|
||||
* DEPRECATED - Use i18nManager.getAll() or i18nManager.get(key) instead
|
||||
*/
|
||||
window.getCheckoutLabels = function(key) {
|
||||
window.getCheckoutLabels = function (key) {
|
||||
if (window.i18nManager && window.i18nManager.initialized) {
|
||||
if (key) {
|
||||
return window.i18nManager.get(key);
|
||||
|
|
@ -38,30 +38,29 @@
|
|||
/**
|
||||
* DEPRECATED - Use i18nManager.getAll() instead
|
||||
*/
|
||||
window.getSearchLabels = function() {
|
||||
window.getSearchLabels = function () {
|
||||
if (window.i18nManager && window.i18nManager.initialized) {
|
||||
return {
|
||||
'searchPlaceholder': window.i18nManager.get('search_products'),
|
||||
'noResults': window.i18nManager.get('no_results')
|
||||
searchPlaceholder: window.i18nManager.get("search_products"),
|
||||
noResults: window.i18nManager.get("no_results"),
|
||||
};
|
||||
}
|
||||
return {
|
||||
'searchPlaceholder': 'Search products...',
|
||||
'noResults': 'No products found'
|
||||
searchPlaceholder: "Search products...",
|
||||
noResults: "No products found",
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* DEPRECATED - Use i18nManager.formatCurrency(amount) instead
|
||||
*/
|
||||
window.formatCurrency = function(amount) {
|
||||
window.formatCurrency = function (amount) {
|
||||
if (window.i18nManager) {
|
||||
return window.i18nManager.formatCurrency(amount);
|
||||
}
|
||||
// Fallback
|
||||
return '€' + parseFloat(amount).toFixed(2);
|
||||
return "€" + parseFloat(amount).toFixed(2);
|
||||
};
|
||||
|
||||
console.log('[i18n_helpers] DEPRECATED - Use i18n_manager.js instead');
|
||||
|
||||
console.log("[i18n_helpers] DEPRECATED - Use i18n_manager.js instead");
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
/**
|
||||
* I18N Manager - Unified Translation Management
|
||||
*
|
||||
*
|
||||
* Single point of truth for all translations.
|
||||
* Fetches from server endpoint /eskaera/i18n once and caches.
|
||||
*
|
||||
*
|
||||
* Usage:
|
||||
* i18nManager.init().then(function() {
|
||||
* var translated = i18nManager.get('product'); // Returns translated string
|
||||
* var allLabels = i18nManager.getAll(); // Returns all labels
|
||||
* });
|
||||
*
|
||||
*
|
||||
* Copyright 2025 Criptomart
|
||||
* License AGPL-3.0 or later
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
window.i18nManager = {
|
||||
labels: null,
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
* Initialize by fetching translations from server
|
||||
* Returns a Promise that resolves when translations are loaded
|
||||
*/
|
||||
init: function() {
|
||||
init: function () {
|
||||
if (this.initialized) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
|
@ -38,41 +38,45 @@
|
|||
var self = this;
|
||||
|
||||
// Detect user's language from document or fallback to en_US
|
||||
var detectedLang = document.documentElement.lang || 'es_ES';
|
||||
console.log('[i18nManager] Detected language:', detectedLang);
|
||||
var detectedLang = document.documentElement.lang || "es_ES";
|
||||
console.log("[i18nManager] Detected language:", detectedLang);
|
||||
|
||||
// Fetch translations from server
|
||||
this.initPromise = fetch('/eskaera/i18n', {
|
||||
method: 'POST',
|
||||
this.initPromise = fetch("/eskaera/i18n", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ lang: detectedLang })
|
||||
body: JSON.stringify({ lang: detectedLang }),
|
||||
})
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP error, status = ' + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
// Handle JSON-RPC response format
|
||||
// Server returns: { "jsonrpc": "2.0", "id": null, "result": { labels } }
|
||||
// Extract the actual labels from the result property
|
||||
var labels = data.result || data;
|
||||
|
||||
console.log('[i18nManager] ✓ Loaded', Object.keys(labels).length, 'translation labels');
|
||||
self.labels = labels;
|
||||
self.initialized = true;
|
||||
return labels;
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('[i18nManager] Error loading translations:', error);
|
||||
// Fallback to empty object so app doesn't crash
|
||||
self.labels = {};
|
||||
self.initialized = true;
|
||||
return {};
|
||||
});
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP error, status = " + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
// Handle JSON-RPC response format
|
||||
// Server returns: { "jsonrpc": "2.0", "id": null, "result": { labels } }
|
||||
// Extract the actual labels from the result property
|
||||
var labels = data.result || data;
|
||||
|
||||
console.log(
|
||||
"[i18nManager] ✓ Loaded",
|
||||
Object.keys(labels).length,
|
||||
"translation labels"
|
||||
);
|
||||
self.labels = labels;
|
||||
self.initialized = true;
|
||||
return labels;
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error("[i18nManager] Error loading translations:", error);
|
||||
// Fallback to empty object so app doesn't crash
|
||||
self.labels = {};
|
||||
self.initialized = true;
|
||||
return {};
|
||||
});
|
||||
|
||||
return this.initPromise;
|
||||
},
|
||||
|
|
@ -81,9 +85,9 @@
|
|||
* Get a specific translation label
|
||||
* Returns the translated string or the key if not found
|
||||
*/
|
||||
get: function(key) {
|
||||
get: function (key) {
|
||||
if (!this.initialized) {
|
||||
console.warn('[i18nManager] Not yet initialized. Call init() first.');
|
||||
console.warn("[i18nManager] Not yet initialized. Call init() first.");
|
||||
return key;
|
||||
}
|
||||
return this.labels[key] || key;
|
||||
|
|
@ -92,9 +96,9 @@
|
|||
/**
|
||||
* Get all translation labels as object
|
||||
*/
|
||||
getAll: function() {
|
||||
getAll: function () {
|
||||
if (!this.initialized) {
|
||||
console.warn('[i18nManager] Not yet initialized. Call init() first.');
|
||||
console.warn("[i18nManager] Not yet initialized. Call init() first.");
|
||||
return {};
|
||||
}
|
||||
return this.labels;
|
||||
|
|
@ -103,7 +107,7 @@
|
|||
/**
|
||||
* Check if a specific label exists
|
||||
*/
|
||||
has: function(key) {
|
||||
has: function (key) {
|
||||
if (!this.initialized) return false;
|
||||
return key in this.labels;
|
||||
},
|
||||
|
|
@ -111,43 +115,42 @@
|
|||
/**
|
||||
* Format currency to Euro format
|
||||
*/
|
||||
formatCurrency: function(amount) {
|
||||
formatCurrency: function (amount) {
|
||||
try {
|
||||
return new Intl.NumberFormat(document.documentElement.lang || 'es_ES', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
return new Intl.NumberFormat(document.documentElement.lang || "es_ES", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(amount);
|
||||
} catch (e) {
|
||||
// Fallback to simple Euro format
|
||||
return '€' + parseFloat(amount).toFixed(2);
|
||||
return "€" + parseFloat(amount).toFixed(2);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
escapeHtml: function(text) {
|
||||
if (!text) return '';
|
||||
var div = document.createElement('div');
|
||||
escapeHtml: function (text) {
|
||||
if (!text) return "";
|
||||
var div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Auto-initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
i18nManager.init().catch(function(err) {
|
||||
console.error('[i18nManager] Auto-init failed:', err);
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
i18nManager.init().catch(function (err) {
|
||||
console.error("[i18nManager] Auto-init failed:", err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// DOM already loaded
|
||||
setTimeout(function() {
|
||||
i18nManager.init().catch(function(err) {
|
||||
console.error('[i18nManager] Auto-init failed:', err);
|
||||
setTimeout(function () {
|
||||
i18nManager.init().catch(function (err) {
|
||||
console.error("[i18nManager] Auto-init failed:", err);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ docker-compose exec odoo odoo -d odoo --test-enable --log-level=test --stop-afte
|
|||
odoo.define('website_sale_aplicoop.test_my_feature', function (require) {
|
||||
'use strict';
|
||||
var QUnit = window.QUnit;
|
||||
|
||||
|
||||
QUnit.module('website_sale_aplicoop.my_feature', {
|
||||
beforeEach: function() {
|
||||
// Setup code
|
||||
|
|
@ -257,6 +257,6 @@ exit $exit_code
|
|||
|
||||
---
|
||||
|
||||
**Maintainer**: Criptomart
|
||||
**License**: AGPL-3.0
|
||||
**Maintainer**: Criptomart
|
||||
**License**: AGPL-3.0
|
||||
**Last Updated**: February 3, 2026
|
||||
|
|
|
|||
|
|
@ -3,222 +3,273 @@
|
|||
* Tests core cart functionality (add, remove, update, calculate)
|
||||
*/
|
||||
|
||||
odoo.define('website_sale_aplicoop.test_cart_functions', function (require) {
|
||||
'use strict';
|
||||
odoo.define("website_sale_aplicoop.test_cart_functions", function (require) {
|
||||
"use strict";
|
||||
|
||||
var QUnit = window.QUnit;
|
||||
|
||||
QUnit.module('website_sale_aplicoop', {
|
||||
beforeEach: function() {
|
||||
// Setup: Initialize groupOrderShop object
|
||||
window.groupOrderShop = {
|
||||
orderId: '1',
|
||||
cart: {},
|
||||
labels: {
|
||||
'save_cart': 'Save Cart',
|
||||
'reload_cart': 'Reload Cart',
|
||||
'checkout': 'Checkout',
|
||||
'confirm_order': 'Confirm Order',
|
||||
'back_to_cart': 'Back to Cart'
|
||||
}
|
||||
};
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.clear();
|
||||
QUnit.module(
|
||||
"website_sale_aplicoop",
|
||||
{
|
||||
beforeEach: function () {
|
||||
// Setup: Initialize groupOrderShop object
|
||||
window.groupOrderShop = {
|
||||
orderId: "1",
|
||||
cart: {},
|
||||
labels: {
|
||||
save_cart: "Save Cart",
|
||||
reload_cart: "Reload Cart",
|
||||
checkout: "Checkout",
|
||||
confirm_order: "Confirm Order",
|
||||
back_to_cart: "Back to Cart",
|
||||
},
|
||||
};
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.clear();
|
||||
},
|
||||
afterEach: function () {
|
||||
// Cleanup
|
||||
localStorage.clear();
|
||||
delete window.groupOrderShop;
|
||||
},
|
||||
},
|
||||
afterEach: function() {
|
||||
// Cleanup
|
||||
localStorage.clear();
|
||||
delete window.groupOrderShop;
|
||||
function () {
|
||||
QUnit.test("groupOrderShop object initializes correctly", function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
assert.ok(window.groupOrderShop, "groupOrderShop object exists");
|
||||
assert.equal(window.groupOrderShop.orderId, "1", "orderId is set");
|
||||
assert.ok(typeof window.groupOrderShop.cart === "object", "cart is an object");
|
||||
});
|
||||
|
||||
QUnit.test("cart starts empty", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var cartKeys = Object.keys(window.groupOrderShop.cart);
|
||||
assert.equal(cartKeys.length, 0, "cart has no items initially");
|
||||
});
|
||||
|
||||
QUnit.test("can add item to cart", function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
// Add a product to cart
|
||||
var productId = "123";
|
||||
var productData = {
|
||||
name: "Test Product",
|
||||
price: 10.5,
|
||||
quantity: 2,
|
||||
};
|
||||
|
||||
window.groupOrderShop.cart[productId] = productData;
|
||||
|
||||
assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, "cart has 1 item");
|
||||
assert.ok(window.groupOrderShop.cart[productId], "product exists in cart");
|
||||
assert.equal(
|
||||
window.groupOrderShop.cart[productId].name,
|
||||
"Test Product",
|
||||
"product name is correct"
|
||||
);
|
||||
assert.equal(
|
||||
window.groupOrderShop.cart[productId].quantity,
|
||||
2,
|
||||
"product quantity is correct"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("can remove item from cart", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Add then remove
|
||||
var productId = "123";
|
||||
window.groupOrderShop.cart[productId] = {
|
||||
name: "Test Product",
|
||||
price: 10.5,
|
||||
quantity: 2,
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
Object.keys(window.groupOrderShop.cart).length,
|
||||
1,
|
||||
"cart has 1 item after add"
|
||||
);
|
||||
|
||||
delete window.groupOrderShop.cart[productId];
|
||||
|
||||
assert.equal(
|
||||
Object.keys(window.groupOrderShop.cart).length,
|
||||
0,
|
||||
"cart is empty after remove"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("can update item quantity", function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var productId = "123";
|
||||
window.groupOrderShop.cart[productId] = {
|
||||
name: "Test Product",
|
||||
price: 10.5,
|
||||
quantity: 2,
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
window.groupOrderShop.cart[productId].quantity,
|
||||
2,
|
||||
"initial quantity is 2"
|
||||
);
|
||||
|
||||
// Update quantity
|
||||
window.groupOrderShop.cart[productId].quantity = 5;
|
||||
|
||||
assert.equal(
|
||||
window.groupOrderShop.cart[productId].quantity,
|
||||
5,
|
||||
"quantity updated to 5"
|
||||
);
|
||||
assert.equal(
|
||||
Object.keys(window.groupOrderShop.cart).length,
|
||||
1,
|
||||
"still only 1 item in cart"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("cart total calculates correctly", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
// Add multiple products
|
||||
window.groupOrderShop.cart["123"] = {
|
||||
name: "Product 1",
|
||||
price: 10.0,
|
||||
quantity: 2,
|
||||
};
|
||||
|
||||
window.groupOrderShop.cart["456"] = {
|
||||
name: "Product 2",
|
||||
price: 5.5,
|
||||
quantity: 3,
|
||||
};
|
||||
|
||||
// Calculate total manually
|
||||
var total = 0;
|
||||
Object.keys(window.groupOrderShop.cart).forEach(function (productId) {
|
||||
var item = window.groupOrderShop.cart[productId];
|
||||
total += item.price * item.quantity;
|
||||
});
|
||||
|
||||
// Expected: (10.00 * 2) + (5.50 * 3) = 20.00 + 16.50 = 36.50
|
||||
assert.equal(total.toFixed(2), "36.50", "cart total is correct");
|
||||
});
|
||||
|
||||
QUnit.test("localStorage saves cart correctly", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var cartKey = "eskaera_1_cart";
|
||||
var testCart = {
|
||||
123: {
|
||||
name: "Test Product",
|
||||
price: 10.5,
|
||||
quantity: 2,
|
||||
},
|
||||
};
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem(cartKey, JSON.stringify(testCart));
|
||||
|
||||
// Retrieve and verify
|
||||
var savedCart = JSON.parse(localStorage.getItem(cartKey));
|
||||
|
||||
assert.ok(savedCart, "cart was saved to localStorage");
|
||||
assert.equal(savedCart["123"].name, "Test Product", "cart data is correct");
|
||||
});
|
||||
|
||||
QUnit.test("labels object is initialized", function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
assert.ok(window.groupOrderShop.labels, "labels object exists");
|
||||
assert.equal(
|
||||
window.groupOrderShop.labels["save_cart"],
|
||||
"Save Cart",
|
||||
"save_cart label exists"
|
||||
);
|
||||
assert.equal(
|
||||
window.groupOrderShop.labels["reload_cart"],
|
||||
"Reload Cart",
|
||||
"reload_cart label exists"
|
||||
);
|
||||
assert.equal(
|
||||
window.groupOrderShop.labels["checkout"],
|
||||
"Checkout",
|
||||
"checkout label exists"
|
||||
);
|
||||
assert.equal(
|
||||
window.groupOrderShop.labels["confirm_order"],
|
||||
"Confirm Order",
|
||||
"confirm_order label exists"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("cart handles decimal quantities correctly", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
window.groupOrderShop.cart["123"] = {
|
||||
name: "Weight Product",
|
||||
price: 8.99,
|
||||
quantity: 1.5,
|
||||
};
|
||||
|
||||
var item = window.groupOrderShop.cart["123"];
|
||||
var subtotal = item.price * item.quantity;
|
||||
|
||||
assert.equal(item.quantity, 1.5, "decimal quantity stored correctly");
|
||||
assert.equal(
|
||||
subtotal.toFixed(2),
|
||||
"13.49",
|
||||
"subtotal with decimal quantity is correct"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("cart handles zero quantity", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
window.groupOrderShop.cart["123"] = {
|
||||
name: "Test Product",
|
||||
price: 10.0,
|
||||
quantity: 0,
|
||||
};
|
||||
|
||||
var item = window.groupOrderShop.cart["123"];
|
||||
var subtotal = item.price * item.quantity;
|
||||
|
||||
assert.equal(subtotal, 0, "zero quantity results in zero subtotal");
|
||||
});
|
||||
|
||||
QUnit.test("cart handles multiple items with same price", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
window.groupOrderShop.cart["123"] = {
|
||||
name: "Product A",
|
||||
price: 10.0,
|
||||
quantity: 2,
|
||||
};
|
||||
|
||||
window.groupOrderShop.cart["456"] = {
|
||||
name: "Product B",
|
||||
price: 10.0,
|
||||
quantity: 3,
|
||||
};
|
||||
|
||||
var total = 0;
|
||||
Object.keys(window.groupOrderShop.cart).forEach(function (productId) {
|
||||
var item = window.groupOrderShop.cart[productId];
|
||||
total += item.price * item.quantity;
|
||||
});
|
||||
|
||||
assert.equal(Object.keys(window.groupOrderShop.cart).length, 2, "cart has 2 items");
|
||||
assert.equal(total.toFixed(2), "50.00", "total is correct with same prices");
|
||||
});
|
||||
}
|
||||
}, function() {
|
||||
|
||||
QUnit.test('groupOrderShop object initializes correctly', function(assert) {
|
||||
assert.expect(3);
|
||||
|
||||
assert.ok(window.groupOrderShop, 'groupOrderShop object exists');
|
||||
assert.equal(window.groupOrderShop.orderId, '1', 'orderId is set');
|
||||
assert.ok(typeof window.groupOrderShop.cart === 'object', 'cart is an object');
|
||||
});
|
||||
|
||||
QUnit.test('cart starts empty', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var cartKeys = Object.keys(window.groupOrderShop.cart);
|
||||
assert.equal(cartKeys.length, 0, 'cart has no items initially');
|
||||
});
|
||||
|
||||
QUnit.test('can add item to cart', function(assert) {
|
||||
assert.expect(4);
|
||||
|
||||
// Add a product to cart
|
||||
var productId = '123';
|
||||
var productData = {
|
||||
name: 'Test Product',
|
||||
price: 10.50,
|
||||
quantity: 2
|
||||
};
|
||||
|
||||
window.groupOrderShop.cart[productId] = productData;
|
||||
|
||||
assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, 'cart has 1 item');
|
||||
assert.ok(window.groupOrderShop.cart[productId], 'product exists in cart');
|
||||
assert.equal(window.groupOrderShop.cart[productId].name, 'Test Product', 'product name is correct');
|
||||
assert.equal(window.groupOrderShop.cart[productId].quantity, 2, 'product quantity is correct');
|
||||
});
|
||||
|
||||
QUnit.test('can remove item from cart', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Add then remove
|
||||
var productId = '123';
|
||||
window.groupOrderShop.cart[productId] = {
|
||||
name: 'Test Product',
|
||||
price: 10.50,
|
||||
quantity: 2
|
||||
};
|
||||
|
||||
assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, 'cart has 1 item after add');
|
||||
|
||||
delete window.groupOrderShop.cart[productId];
|
||||
|
||||
assert.equal(Object.keys(window.groupOrderShop.cart).length, 0, 'cart is empty after remove');
|
||||
});
|
||||
|
||||
QUnit.test('can update item quantity', function(assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var productId = '123';
|
||||
window.groupOrderShop.cart[productId] = {
|
||||
name: 'Test Product',
|
||||
price: 10.50,
|
||||
quantity: 2
|
||||
};
|
||||
|
||||
assert.equal(window.groupOrderShop.cart[productId].quantity, 2, 'initial quantity is 2');
|
||||
|
||||
// Update quantity
|
||||
window.groupOrderShop.cart[productId].quantity = 5;
|
||||
|
||||
assert.equal(window.groupOrderShop.cart[productId].quantity, 5, 'quantity updated to 5');
|
||||
assert.equal(Object.keys(window.groupOrderShop.cart).length, 1, 'still only 1 item in cart');
|
||||
});
|
||||
|
||||
QUnit.test('cart total calculates correctly', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
// Add multiple products
|
||||
window.groupOrderShop.cart['123'] = {
|
||||
name: 'Product 1',
|
||||
price: 10.00,
|
||||
quantity: 2
|
||||
};
|
||||
|
||||
window.groupOrderShop.cart['456'] = {
|
||||
name: 'Product 2',
|
||||
price: 5.50,
|
||||
quantity: 3
|
||||
};
|
||||
|
||||
// Calculate total manually
|
||||
var total = 0;
|
||||
Object.keys(window.groupOrderShop.cart).forEach(function(productId) {
|
||||
var item = window.groupOrderShop.cart[productId];
|
||||
total += item.price * item.quantity;
|
||||
});
|
||||
|
||||
// Expected: (10.00 * 2) + (5.50 * 3) = 20.00 + 16.50 = 36.50
|
||||
assert.equal(total.toFixed(2), '36.50', 'cart total is correct');
|
||||
});
|
||||
|
||||
QUnit.test('localStorage saves cart correctly', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var cartKey = 'eskaera_1_cart';
|
||||
var testCart = {
|
||||
'123': {
|
||||
name: 'Test Product',
|
||||
price: 10.50,
|
||||
quantity: 2
|
||||
}
|
||||
};
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem(cartKey, JSON.stringify(testCart));
|
||||
|
||||
// Retrieve and verify
|
||||
var savedCart = JSON.parse(localStorage.getItem(cartKey));
|
||||
|
||||
assert.ok(savedCart, 'cart was saved to localStorage');
|
||||
assert.equal(savedCart['123'].name, 'Test Product', 'cart data is correct');
|
||||
});
|
||||
|
||||
QUnit.test('labels object is initialized', function(assert) {
|
||||
assert.expect(5);
|
||||
|
||||
assert.ok(window.groupOrderShop.labels, 'labels object exists');
|
||||
assert.equal(window.groupOrderShop.labels['save_cart'], 'Save Cart', 'save_cart label exists');
|
||||
assert.equal(window.groupOrderShop.labels['reload_cart'], 'Reload Cart', 'reload_cart label exists');
|
||||
assert.equal(window.groupOrderShop.labels['checkout'], 'Checkout', 'checkout label exists');
|
||||
assert.equal(window.groupOrderShop.labels['confirm_order'], 'Confirm Order', 'confirm_order label exists');
|
||||
});
|
||||
|
||||
QUnit.test('cart handles decimal quantities correctly', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
window.groupOrderShop.cart['123'] = {
|
||||
name: 'Weight Product',
|
||||
price: 8.99,
|
||||
quantity: 1.5
|
||||
};
|
||||
|
||||
var item = window.groupOrderShop.cart['123'];
|
||||
var subtotal = item.price * item.quantity;
|
||||
|
||||
assert.equal(item.quantity, 1.5, 'decimal quantity stored correctly');
|
||||
assert.equal(subtotal.toFixed(2), '13.49', 'subtotal with decimal quantity is correct');
|
||||
});
|
||||
|
||||
QUnit.test('cart handles zero quantity', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
window.groupOrderShop.cart['123'] = {
|
||||
name: 'Test Product',
|
||||
price: 10.00,
|
||||
quantity: 0
|
||||
};
|
||||
|
||||
var item = window.groupOrderShop.cart['123'];
|
||||
var subtotal = item.price * item.quantity;
|
||||
|
||||
assert.equal(subtotal, 0, 'zero quantity results in zero subtotal');
|
||||
});
|
||||
|
||||
QUnit.test('cart handles multiple items with same price', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
window.groupOrderShop.cart['123'] = {
|
||||
name: 'Product A',
|
||||
price: 10.00,
|
||||
quantity: 2
|
||||
};
|
||||
|
||||
window.groupOrderShop.cart['456'] = {
|
||||
name: 'Product B',
|
||||
price: 10.00,
|
||||
quantity: 3
|
||||
};
|
||||
|
||||
var total = 0;
|
||||
Object.keys(window.groupOrderShop.cart).forEach(function(productId) {
|
||||
var item = window.groupOrderShop.cart[productId];
|
||||
total += item.price * item.quantity;
|
||||
});
|
||||
|
||||
assert.equal(Object.keys(window.groupOrderShop.cart).length, 2, 'cart has 2 items');
|
||||
assert.equal(total.toFixed(2), '50.00', 'total is correct with same prices');
|
||||
});
|
||||
});
|
||||
);
|
||||
|
||||
return {};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,239 +3,247 @@
|
|||
* Tests product filtering and search behavior
|
||||
*/
|
||||
|
||||
odoo.define('website_sale_aplicoop.test_realtime_search', function (require) {
|
||||
'use strict';
|
||||
odoo.define("website_sale_aplicoop.test_realtime_search", function (require) {
|
||||
"use strict";
|
||||
|
||||
var QUnit = window.QUnit;
|
||||
|
||||
QUnit.module('website_sale_aplicoop.realtime_search', {
|
||||
beforeEach: function() {
|
||||
// Setup: Create test DOM with product cards
|
||||
this.$fixture = $('#qunit-fixture');
|
||||
|
||||
this.$fixture.append(
|
||||
'<input type="text" id="realtime-search-input" />' +
|
||||
'<select id="realtime-category-select">' +
|
||||
'<option value="">All Categories</option>' +
|
||||
'<option value="1">Category 1</option>' +
|
||||
'<option value="2">Category 2</option>' +
|
||||
'</select>' +
|
||||
'<div class="product-card" data-product-name="Cabbage" data-category-id="1"></div>' +
|
||||
'<div class="product-card" data-product-name="Carrot" data-category-id="1"></div>' +
|
||||
'<div class="product-card" data-product-name="Apple" data-category-id="2"></div>' +
|
||||
'<div class="product-card" data-product-name="Banana" data-category-id="2"></div>'
|
||||
);
|
||||
|
||||
// Initialize search object
|
||||
window.realtimeSearch = {
|
||||
searchInput: document.getElementById('realtime-search-input'),
|
||||
categorySelect: document.getElementById('realtime-category-select'),
|
||||
productCards: document.querySelectorAll('.product-card'),
|
||||
|
||||
filterProducts: function() {
|
||||
var searchTerm = this.searchInput.value.toLowerCase().trim();
|
||||
var selectedCategory = this.categorySelect.value;
|
||||
|
||||
var visibleCount = 0;
|
||||
var hiddenCount = 0;
|
||||
|
||||
this.productCards.forEach(function(card) {
|
||||
var productName = card.getAttribute('data-product-name').toLowerCase();
|
||||
var categoryId = card.getAttribute('data-category-id');
|
||||
|
||||
var matchesSearch = !searchTerm || productName.includes(searchTerm);
|
||||
var matchesCategory = !selectedCategory || categoryId === selectedCategory;
|
||||
|
||||
if (matchesSearch && matchesCategory) {
|
||||
card.classList.remove('d-none');
|
||||
visibleCount++;
|
||||
} else {
|
||||
card.classList.add('d-none');
|
||||
hiddenCount++;
|
||||
}
|
||||
});
|
||||
|
||||
return { visible: visibleCount, hidden: hiddenCount };
|
||||
}
|
||||
};
|
||||
QUnit.module(
|
||||
"website_sale_aplicoop.realtime_search",
|
||||
{
|
||||
beforeEach: function () {
|
||||
// Setup: Create test DOM with product cards
|
||||
this.$fixture = $("#qunit-fixture");
|
||||
|
||||
this.$fixture.append(
|
||||
'<input type="text" id="realtime-search-input" />' +
|
||||
'<select id="realtime-category-select">' +
|
||||
'<option value="">All Categories</option>' +
|
||||
'<option value="1">Category 1</option>' +
|
||||
'<option value="2">Category 2</option>' +
|
||||
"</select>" +
|
||||
'<div class="product-card" data-product-name="Cabbage" data-category-id="1"></div>' +
|
||||
'<div class="product-card" data-product-name="Carrot" data-category-id="1"></div>' +
|
||||
'<div class="product-card" data-product-name="Apple" data-category-id="2"></div>' +
|
||||
'<div class="product-card" data-product-name="Banana" data-category-id="2"></div>'
|
||||
);
|
||||
|
||||
// Initialize search object
|
||||
window.realtimeSearch = {
|
||||
searchInput: document.getElementById("realtime-search-input"),
|
||||
categorySelect: document.getElementById("realtime-category-select"),
|
||||
productCards: document.querySelectorAll(".product-card"),
|
||||
|
||||
filterProducts: function () {
|
||||
var searchTerm = this.searchInput.value.toLowerCase().trim();
|
||||
var selectedCategory = this.categorySelect.value;
|
||||
|
||||
var visibleCount = 0;
|
||||
var hiddenCount = 0;
|
||||
|
||||
this.productCards.forEach(function (card) {
|
||||
var productName = card.getAttribute("data-product-name").toLowerCase();
|
||||
var categoryId = card.getAttribute("data-category-id");
|
||||
|
||||
var matchesSearch = !searchTerm || productName.includes(searchTerm);
|
||||
var matchesCategory =
|
||||
!selectedCategory || categoryId === selectedCategory;
|
||||
|
||||
if (matchesSearch && matchesCategory) {
|
||||
card.classList.remove("d-none");
|
||||
visibleCount++;
|
||||
} else {
|
||||
card.classList.add("d-none");
|
||||
hiddenCount++;
|
||||
}
|
||||
});
|
||||
|
||||
return { visible: visibleCount, hidden: hiddenCount };
|
||||
},
|
||||
};
|
||||
},
|
||||
afterEach: function () {
|
||||
// Cleanup
|
||||
this.$fixture.empty();
|
||||
delete window.realtimeSearch;
|
||||
},
|
||||
},
|
||||
afterEach: function() {
|
||||
// Cleanup
|
||||
this.$fixture.empty();
|
||||
delete window.realtimeSearch;
|
||||
function () {
|
||||
QUnit.test("search input element exists", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var searchInput = document.getElementById("realtime-search-input");
|
||||
assert.ok(searchInput, "search input element exists");
|
||||
});
|
||||
|
||||
QUnit.test("category select element exists", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var categorySelect = document.getElementById("realtime-category-select");
|
||||
assert.ok(categorySelect, "category select element exists");
|
||||
});
|
||||
|
||||
QUnit.test("product cards are found", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var productCards = document.querySelectorAll(".product-card");
|
||||
assert.equal(productCards.length, 4, "found 4 product cards");
|
||||
});
|
||||
|
||||
QUnit.test("search filters by product name", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Search for "cab"
|
||||
window.realtimeSearch.searchInput.value = "cab";
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 1, "1 product visible (Cabbage)");
|
||||
assert.equal(result.hidden, 3, "3 products hidden");
|
||||
});
|
||||
|
||||
QUnit.test("search is case insensitive", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Search for "CARROT" in uppercase
|
||||
window.realtimeSearch.searchInput.value = "CARROT";
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 1, "1 product visible (Carrot)");
|
||||
assert.equal(result.hidden, 3, "3 products hidden");
|
||||
});
|
||||
|
||||
QUnit.test("empty search shows all products", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
window.realtimeSearch.searchInput.value = "";
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 4, "all 4 products visible");
|
||||
assert.equal(result.hidden, 0, "no products hidden");
|
||||
});
|
||||
|
||||
QUnit.test("category filter works", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Select category 1
|
||||
window.realtimeSearch.categorySelect.value = "1";
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 2, "2 products visible (Cabbage, Carrot)");
|
||||
assert.equal(result.hidden, 2, "2 products hidden (Apple, Banana)");
|
||||
});
|
||||
|
||||
QUnit.test("search and category filter work together", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Search for "ca" in category 1
|
||||
window.realtimeSearch.searchInput.value = "ca";
|
||||
window.realtimeSearch.categorySelect.value = "1";
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
// Should show: Cabbage, Carrot (both in category 1 and match "ca")
|
||||
assert.equal(result.visible, 2, "2 products visible");
|
||||
assert.equal(result.hidden, 2, "2 products hidden");
|
||||
});
|
||||
|
||||
QUnit.test("search for non-existent product shows none", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
window.realtimeSearch.searchInput.value = "xyz123";
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 0, "no products visible");
|
||||
assert.equal(result.hidden, 4, "all 4 products hidden");
|
||||
});
|
||||
|
||||
QUnit.test("partial match works", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Search for "an" should match "Banana"
|
||||
window.realtimeSearch.searchInput.value = "an";
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 1, "1 product visible (Banana)");
|
||||
assert.equal(result.hidden, 3, "3 products hidden");
|
||||
});
|
||||
|
||||
QUnit.test("search trims whitespace", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Search with extra whitespace
|
||||
window.realtimeSearch.searchInput.value = " apple ";
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 1, "1 product visible (Apple)");
|
||||
assert.equal(result.hidden, 3, "3 products hidden");
|
||||
});
|
||||
|
||||
QUnit.test("d-none class is added to hidden products", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
window.realtimeSearch.searchInput.value = "cabbage";
|
||||
window.realtimeSearch.filterProducts();
|
||||
|
||||
var productCards = document.querySelectorAll(".product-card");
|
||||
var hiddenCards = Array.from(productCards).filter(function (card) {
|
||||
return card.classList.contains("d-none");
|
||||
});
|
||||
|
||||
assert.equal(hiddenCards.length, 3, "3 cards have d-none class");
|
||||
});
|
||||
|
||||
QUnit.test("d-none class is removed from visible products", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// First hide all
|
||||
window.realtimeSearch.searchInput.value = "xyz";
|
||||
window.realtimeSearch.filterProducts();
|
||||
|
||||
var allHidden = Array.from(window.realtimeSearch.productCards).every(function (
|
||||
card
|
||||
) {
|
||||
return card.classList.contains("d-none");
|
||||
});
|
||||
assert.ok(allHidden, "all cards hidden initially");
|
||||
|
||||
// Then show all
|
||||
window.realtimeSearch.searchInput.value = "";
|
||||
window.realtimeSearch.filterProducts();
|
||||
|
||||
var allVisible = Array.from(window.realtimeSearch.productCards).every(function (
|
||||
card
|
||||
) {
|
||||
return !card.classList.contains("d-none");
|
||||
});
|
||||
assert.ok(allVisible, "all cards visible after clearing search");
|
||||
});
|
||||
|
||||
QUnit.test("filterProducts returns correct counts", function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
// All visible
|
||||
window.realtimeSearch.searchInput.value = "";
|
||||
var result1 = window.realtimeSearch.filterProducts();
|
||||
assert.equal(result1.visible + result1.hidden, 4, "total count is 4");
|
||||
|
||||
// 1 visible
|
||||
window.realtimeSearch.searchInput.value = "apple";
|
||||
var result2 = window.realtimeSearch.filterProducts();
|
||||
assert.equal(result2.visible, 1, "visible count is 1");
|
||||
|
||||
// None visible
|
||||
window.realtimeSearch.searchInput.value = "xyz";
|
||||
var result3 = window.realtimeSearch.filterProducts();
|
||||
assert.equal(result3.visible, 0, "visible count is 0");
|
||||
|
||||
// Category filter
|
||||
window.realtimeSearch.searchInput.value = "";
|
||||
window.realtimeSearch.categorySelect.value = "2";
|
||||
var result4 = window.realtimeSearch.filterProducts();
|
||||
assert.equal(result4.visible, 2, "category filter shows 2 products");
|
||||
});
|
||||
}
|
||||
}, function() {
|
||||
|
||||
QUnit.test('search input element exists', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var searchInput = document.getElementById('realtime-search-input');
|
||||
assert.ok(searchInput, 'search input element exists');
|
||||
});
|
||||
|
||||
QUnit.test('category select element exists', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var categorySelect = document.getElementById('realtime-category-select');
|
||||
assert.ok(categorySelect, 'category select element exists');
|
||||
});
|
||||
|
||||
QUnit.test('product cards are found', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var productCards = document.querySelectorAll('.product-card');
|
||||
assert.equal(productCards.length, 4, 'found 4 product cards');
|
||||
});
|
||||
|
||||
QUnit.test('search filters by product name', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Search for "cab"
|
||||
window.realtimeSearch.searchInput.value = 'cab';
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 1, '1 product visible (Cabbage)');
|
||||
assert.equal(result.hidden, 3, '3 products hidden');
|
||||
});
|
||||
|
||||
QUnit.test('search is case insensitive', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Search for "CARROT" in uppercase
|
||||
window.realtimeSearch.searchInput.value = 'CARROT';
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 1, '1 product visible (Carrot)');
|
||||
assert.equal(result.hidden, 3, '3 products hidden');
|
||||
});
|
||||
|
||||
QUnit.test('empty search shows all products', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
window.realtimeSearch.searchInput.value = '';
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 4, 'all 4 products visible');
|
||||
assert.equal(result.hidden, 0, 'no products hidden');
|
||||
});
|
||||
|
||||
QUnit.test('category filter works', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Select category 1
|
||||
window.realtimeSearch.categorySelect.value = '1';
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 2, '2 products visible (Cabbage, Carrot)');
|
||||
assert.equal(result.hidden, 2, '2 products hidden (Apple, Banana)');
|
||||
});
|
||||
|
||||
QUnit.test('search and category filter work together', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Search for "ca" in category 1
|
||||
window.realtimeSearch.searchInput.value = 'ca';
|
||||
window.realtimeSearch.categorySelect.value = '1';
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
// Should show: Cabbage, Carrot (both in category 1 and match "ca")
|
||||
assert.equal(result.visible, 2, '2 products visible');
|
||||
assert.equal(result.hidden, 2, '2 products hidden');
|
||||
});
|
||||
|
||||
QUnit.test('search for non-existent product shows none', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
window.realtimeSearch.searchInput.value = 'xyz123';
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 0, 'no products visible');
|
||||
assert.equal(result.hidden, 4, 'all 4 products hidden');
|
||||
});
|
||||
|
||||
QUnit.test('partial match works', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Search for "an" should match "Banana"
|
||||
window.realtimeSearch.searchInput.value = 'an';
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 1, '1 product visible (Banana)');
|
||||
assert.equal(result.hidden, 3, '3 products hidden');
|
||||
});
|
||||
|
||||
QUnit.test('search trims whitespace', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// Search with extra whitespace
|
||||
window.realtimeSearch.searchInput.value = ' apple ';
|
||||
var result = window.realtimeSearch.filterProducts();
|
||||
|
||||
assert.equal(result.visible, 1, '1 product visible (Apple)');
|
||||
assert.equal(result.hidden, 3, '3 products hidden');
|
||||
});
|
||||
|
||||
QUnit.test('d-none class is added to hidden products', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
window.realtimeSearch.searchInput.value = 'cabbage';
|
||||
window.realtimeSearch.filterProducts();
|
||||
|
||||
var productCards = document.querySelectorAll('.product-card');
|
||||
var hiddenCards = Array.from(productCards).filter(function(card) {
|
||||
return card.classList.contains('d-none');
|
||||
});
|
||||
|
||||
assert.equal(hiddenCards.length, 3, '3 cards have d-none class');
|
||||
});
|
||||
|
||||
QUnit.test('d-none class is removed from visible products', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// First hide all
|
||||
window.realtimeSearch.searchInput.value = 'xyz';
|
||||
window.realtimeSearch.filterProducts();
|
||||
|
||||
var allHidden = Array.from(window.realtimeSearch.productCards).every(function(card) {
|
||||
return card.classList.contains('d-none');
|
||||
});
|
||||
assert.ok(allHidden, 'all cards hidden initially');
|
||||
|
||||
// Then show all
|
||||
window.realtimeSearch.searchInput.value = '';
|
||||
window.realtimeSearch.filterProducts();
|
||||
|
||||
var allVisible = Array.from(window.realtimeSearch.productCards).every(function(card) {
|
||||
return !card.classList.contains('d-none');
|
||||
});
|
||||
assert.ok(allVisible, 'all cards visible after clearing search');
|
||||
});
|
||||
|
||||
QUnit.test('filterProducts returns correct counts', function(assert) {
|
||||
assert.expect(4);
|
||||
|
||||
// All visible
|
||||
window.realtimeSearch.searchInput.value = '';
|
||||
var result1 = window.realtimeSearch.filterProducts();
|
||||
assert.equal(result1.visible + result1.hidden, 4, 'total count is 4');
|
||||
|
||||
// 1 visible
|
||||
window.realtimeSearch.searchInput.value = 'apple';
|
||||
var result2 = window.realtimeSearch.filterProducts();
|
||||
assert.equal(result2.visible, 1, 'visible count is 1');
|
||||
|
||||
// None visible
|
||||
window.realtimeSearch.searchInput.value = 'xyz';
|
||||
var result3 = window.realtimeSearch.filterProducts();
|
||||
assert.equal(result3.visible, 0, 'visible count is 0');
|
||||
|
||||
// Category filter
|
||||
window.realtimeSearch.searchInput.value = '';
|
||||
window.realtimeSearch.categorySelect.value = '2';
|
||||
var result4 = window.realtimeSearch.filterProducts();
|
||||
assert.equal(result4.visible, 2, 'category filter shows 2 products');
|
||||
});
|
||||
});
|
||||
);
|
||||
|
||||
return {};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
odoo.define('website_sale_aplicoop.test_suite', function (require) {
|
||||
'use strict';
|
||||
odoo.define("website_sale_aplicoop.test_suite", function (require) {
|
||||
"use strict";
|
||||
|
||||
// Import all test modules
|
||||
require('website_sale_aplicoop.test_cart_functions');
|
||||
require('website_sale_aplicoop.test_tooltips_labels');
|
||||
require('website_sale_aplicoop.test_realtime_search');
|
||||
require("website_sale_aplicoop.test_cart_functions");
|
||||
require("website_sale_aplicoop.test_tooltips_labels");
|
||||
require("website_sale_aplicoop.test_realtime_search");
|
||||
|
||||
// Test suite is automatically registered by importing modules
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,185 +3,214 @@
|
|||
* Tests tooltip initialization and label loading
|
||||
*/
|
||||
|
||||
odoo.define('website_sale_aplicoop.test_tooltips_labels', function (require) {
|
||||
'use strict';
|
||||
odoo.define("website_sale_aplicoop.test_tooltips_labels", function (require) {
|
||||
"use strict";
|
||||
|
||||
var QUnit = window.QUnit;
|
||||
|
||||
QUnit.module('website_sale_aplicoop.tooltips_labels', {
|
||||
beforeEach: function() {
|
||||
// Setup: Create test DOM elements
|
||||
this.$fixture = $('#qunit-fixture');
|
||||
|
||||
// Add test buttons with tooltip labels
|
||||
this.$fixture.append(
|
||||
'<button id="test-btn-1" data-tooltip-label="save_cart">Save</button>' +
|
||||
'<button id="test-btn-2" data-tooltip-label="checkout">Checkout</button>' +
|
||||
'<button id="test-btn-3" data-tooltip-label="reload_cart">Reload</button>'
|
||||
);
|
||||
|
||||
// Initialize groupOrderShop
|
||||
window.groupOrderShop = {
|
||||
orderId: '1',
|
||||
cart: {},
|
||||
labels: {
|
||||
'save_cart': 'Guardar Carrito',
|
||||
'reload_cart': 'Recargar Carrito',
|
||||
'checkout': 'Proceder al Pago',
|
||||
'confirm_order': 'Confirmar Pedido',
|
||||
'back_to_cart': 'Volver al Carrito'
|
||||
},
|
||||
_initTooltips: function() {
|
||||
var labels = window.groupOrderShop.labels || this.labels || {};
|
||||
var tooltipElements = document.querySelectorAll('[data-tooltip-label]');
|
||||
|
||||
tooltipElements.forEach(function(el) {
|
||||
var labelKey = el.getAttribute('data-tooltip-label');
|
||||
if (labelKey && labels[labelKey]) {
|
||||
el.setAttribute('title', labels[labelKey]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
QUnit.module(
|
||||
"website_sale_aplicoop.tooltips_labels",
|
||||
{
|
||||
beforeEach: function () {
|
||||
// Setup: Create test DOM elements
|
||||
this.$fixture = $("#qunit-fixture");
|
||||
|
||||
// Add test buttons with tooltip labels
|
||||
this.$fixture.append(
|
||||
'<button id="test-btn-1" data-tooltip-label="save_cart">Save</button>' +
|
||||
'<button id="test-btn-2" data-tooltip-label="checkout">Checkout</button>' +
|
||||
'<button id="test-btn-3" data-tooltip-label="reload_cart">Reload</button>'
|
||||
);
|
||||
|
||||
// Initialize groupOrderShop
|
||||
window.groupOrderShop = {
|
||||
orderId: "1",
|
||||
cart: {},
|
||||
labels: {
|
||||
save_cart: "Guardar Carrito",
|
||||
reload_cart: "Recargar Carrito",
|
||||
checkout: "Proceder al Pago",
|
||||
confirm_order: "Confirmar Pedido",
|
||||
back_to_cart: "Volver al Carrito",
|
||||
},
|
||||
_initTooltips: function () {
|
||||
var labels = window.groupOrderShop.labels || this.labels || {};
|
||||
var tooltipElements = document.querySelectorAll("[data-tooltip-label]");
|
||||
|
||||
tooltipElements.forEach(function (el) {
|
||||
var labelKey = el.getAttribute("data-tooltip-label");
|
||||
if (labelKey && labels[labelKey]) {
|
||||
el.setAttribute("title", labels[labelKey]);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
afterEach: function () {
|
||||
// Cleanup
|
||||
this.$fixture.empty();
|
||||
delete window.groupOrderShop;
|
||||
},
|
||||
},
|
||||
afterEach: function() {
|
||||
// Cleanup
|
||||
this.$fixture.empty();
|
||||
delete window.groupOrderShop;
|
||||
}
|
||||
}, function() {
|
||||
function () {
|
||||
QUnit.test("tooltips are initialized from labels", function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
QUnit.test('tooltips are initialized from labels', function(assert) {
|
||||
assert.expect(3);
|
||||
|
||||
// Initialize tooltips
|
||||
window.groupOrderShop._initTooltips();
|
||||
|
||||
var btn1 = document.getElementById('test-btn-1');
|
||||
var btn2 = document.getElementById('test-btn-2');
|
||||
var btn3 = document.getElementById('test-btn-3');
|
||||
|
||||
assert.equal(btn1.getAttribute('title'), 'Guardar Carrito', 'save_cart tooltip is correct');
|
||||
assert.equal(btn2.getAttribute('title'), 'Proceder al Pago', 'checkout tooltip is correct');
|
||||
assert.equal(btn3.getAttribute('title'), 'Recargar Carrito', 'reload_cart tooltip is correct');
|
||||
});
|
||||
|
||||
QUnit.test('tooltips handle missing labels gracefully', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
// Add button with non-existent label
|
||||
this.$fixture.append('<button id="test-btn-4" data-tooltip-label="non_existent">Test</button>');
|
||||
|
||||
window.groupOrderShop._initTooltips();
|
||||
|
||||
var btn4 = document.getElementById('test-btn-4');
|
||||
var title = btn4.getAttribute('title');
|
||||
|
||||
// Should be null or empty since label doesn't exist
|
||||
assert.ok(!title || title === '', 'missing label does not set tooltip');
|
||||
});
|
||||
|
||||
QUnit.test('labels object contains expected keys', function(assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var labels = window.groupOrderShop.labels;
|
||||
|
||||
assert.ok('save_cart' in labels, 'has save_cart label');
|
||||
assert.ok('reload_cart' in labels, 'has reload_cart label');
|
||||
assert.ok('checkout' in labels, 'has checkout label');
|
||||
assert.ok('confirm_order' in labels, 'has confirm_order label');
|
||||
assert.ok('back_to_cart' in labels, 'has back_to_cart label');
|
||||
});
|
||||
|
||||
QUnit.test('labels are strings', function(assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var labels = window.groupOrderShop.labels;
|
||||
|
||||
assert.equal(typeof labels.save_cart, 'string', 'save_cart is string');
|
||||
assert.equal(typeof labels.reload_cart, 'string', 'reload_cart is string');
|
||||
assert.equal(typeof labels.checkout, 'string', 'checkout is string');
|
||||
assert.equal(typeof labels.confirm_order, 'string', 'confirm_order is string');
|
||||
assert.equal(typeof labels.back_to_cart, 'string', 'back_to_cart is string');
|
||||
});
|
||||
|
||||
QUnit.test('_initTooltips uses window.groupOrderShop.labels', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
// Update global labels
|
||||
window.groupOrderShop.labels = {
|
||||
'save_cart': 'Updated Label',
|
||||
'checkout': 'Updated Checkout',
|
||||
'reload_cart': 'Updated Reload'
|
||||
};
|
||||
|
||||
window.groupOrderShop._initTooltips();
|
||||
|
||||
var btn1 = document.getElementById('test-btn-1');
|
||||
assert.equal(btn1.getAttribute('title'), 'Updated Label', 'uses updated global labels');
|
||||
});
|
||||
|
||||
QUnit.test('tooltips can be reinitialized', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// First initialization
|
||||
window.groupOrderShop._initTooltips();
|
||||
var btn1 = document.getElementById('test-btn-1');
|
||||
assert.equal(btn1.getAttribute('title'), 'Guardar Carrito', 'first init correct');
|
||||
|
||||
// Update labels and reinitialize
|
||||
window.groupOrderShop.labels.save_cart = 'New Translation';
|
||||
window.groupOrderShop._initTooltips();
|
||||
|
||||
assert.equal(btn1.getAttribute('title'), 'New Translation', 'reinitialized with new label');
|
||||
});
|
||||
|
||||
QUnit.test('elements without data-tooltip-label are ignored', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.$fixture.append('<button id="test-btn-no-label">No Label</button>');
|
||||
|
||||
window.groupOrderShop._initTooltips();
|
||||
|
||||
var btnNoLabel = document.getElementById('test-btn-no-label');
|
||||
var title = btnNoLabel.getAttribute('title');
|
||||
|
||||
assert.ok(!title || title === '', 'button without data-tooltip-label has no title');
|
||||
});
|
||||
|
||||
QUnit.test('querySelectorAll finds all tooltip elements', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var tooltipElements = document.querySelectorAll('[data-tooltip-label]');
|
||||
|
||||
// We have 3 buttons with data-tooltip-label
|
||||
assert.equal(tooltipElements.length, 3, 'finds all 3 elements with data-tooltip-label');
|
||||
});
|
||||
|
||||
QUnit.test('labels survive JSON serialization', function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var labels = window.groupOrderShop.labels;
|
||||
var serialized = JSON.stringify(labels);
|
||||
var deserialized = JSON.parse(serialized);
|
||||
|
||||
assert.ok(serialized, 'labels can be serialized to JSON');
|
||||
assert.deepEqual(deserialized, labels, 'deserialized labels match original');
|
||||
});
|
||||
|
||||
QUnit.test('empty labels object does not break initialization', function(assert) {
|
||||
assert.expect(1);
|
||||
|
||||
window.groupOrderShop.labels = {};
|
||||
|
||||
try {
|
||||
// Initialize tooltips
|
||||
window.groupOrderShop._initTooltips();
|
||||
assert.ok(true, 'initialization with empty labels does not throw error');
|
||||
} catch (e) {
|
||||
assert.ok(false, 'initialization threw error: ' + e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var btn1 = document.getElementById("test-btn-1");
|
||||
var btn2 = document.getElementById("test-btn-2");
|
||||
var btn3 = document.getElementById("test-btn-3");
|
||||
|
||||
assert.equal(
|
||||
btn1.getAttribute("title"),
|
||||
"Guardar Carrito",
|
||||
"save_cart tooltip is correct"
|
||||
);
|
||||
assert.equal(
|
||||
btn2.getAttribute("title"),
|
||||
"Proceder al Pago",
|
||||
"checkout tooltip is correct"
|
||||
);
|
||||
assert.equal(
|
||||
btn3.getAttribute("title"),
|
||||
"Recargar Carrito",
|
||||
"reload_cart tooltip is correct"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("tooltips handle missing labels gracefully", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
// Add button with non-existent label
|
||||
this.$fixture.append(
|
||||
'<button id="test-btn-4" data-tooltip-label="non_existent">Test</button>'
|
||||
);
|
||||
|
||||
window.groupOrderShop._initTooltips();
|
||||
|
||||
var btn4 = document.getElementById("test-btn-4");
|
||||
var title = btn4.getAttribute("title");
|
||||
|
||||
// Should be null or empty since label doesn't exist
|
||||
assert.ok(!title || title === "", "missing label does not set tooltip");
|
||||
});
|
||||
|
||||
QUnit.test("labels object contains expected keys", function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var labels = window.groupOrderShop.labels;
|
||||
|
||||
assert.ok("save_cart" in labels, "has save_cart label");
|
||||
assert.ok("reload_cart" in labels, "has reload_cart label");
|
||||
assert.ok("checkout" in labels, "has checkout label");
|
||||
assert.ok("confirm_order" in labels, "has confirm_order label");
|
||||
assert.ok("back_to_cart" in labels, "has back_to_cart label");
|
||||
});
|
||||
|
||||
QUnit.test("labels are strings", function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var labels = window.groupOrderShop.labels;
|
||||
|
||||
assert.equal(typeof labels.save_cart, "string", "save_cart is string");
|
||||
assert.equal(typeof labels.reload_cart, "string", "reload_cart is string");
|
||||
assert.equal(typeof labels.checkout, "string", "checkout is string");
|
||||
assert.equal(typeof labels.confirm_order, "string", "confirm_order is string");
|
||||
assert.equal(typeof labels.back_to_cart, "string", "back_to_cart is string");
|
||||
});
|
||||
|
||||
QUnit.test("_initTooltips uses window.groupOrderShop.labels", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
// Update global labels
|
||||
window.groupOrderShop.labels = {
|
||||
save_cart: "Updated Label",
|
||||
checkout: "Updated Checkout",
|
||||
reload_cart: "Updated Reload",
|
||||
};
|
||||
|
||||
window.groupOrderShop._initTooltips();
|
||||
|
||||
var btn1 = document.getElementById("test-btn-1");
|
||||
assert.equal(
|
||||
btn1.getAttribute("title"),
|
||||
"Updated Label",
|
||||
"uses updated global labels"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("tooltips can be reinitialized", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// First initialization
|
||||
window.groupOrderShop._initTooltips();
|
||||
var btn1 = document.getElementById("test-btn-1");
|
||||
assert.equal(btn1.getAttribute("title"), "Guardar Carrito", "first init correct");
|
||||
|
||||
// Update labels and reinitialize
|
||||
window.groupOrderShop.labels.save_cart = "New Translation";
|
||||
window.groupOrderShop._initTooltips();
|
||||
|
||||
assert.equal(
|
||||
btn1.getAttribute("title"),
|
||||
"New Translation",
|
||||
"reinitialized with new label"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("elements without data-tooltip-label are ignored", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.$fixture.append('<button id="test-btn-no-label">No Label</button>');
|
||||
|
||||
window.groupOrderShop._initTooltips();
|
||||
|
||||
var btnNoLabel = document.getElementById("test-btn-no-label");
|
||||
var title = btnNoLabel.getAttribute("title");
|
||||
|
||||
assert.ok(!title || title === "", "button without data-tooltip-label has no title");
|
||||
});
|
||||
|
||||
QUnit.test("querySelectorAll finds all tooltip elements", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var tooltipElements = document.querySelectorAll("[data-tooltip-label]");
|
||||
|
||||
// We have 3 buttons with data-tooltip-label
|
||||
assert.equal(
|
||||
tooltipElements.length,
|
||||
3,
|
||||
"finds all 3 elements with data-tooltip-label"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("labels survive JSON serialization", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var labels = window.groupOrderShop.labels;
|
||||
var serialized = JSON.stringify(labels);
|
||||
var deserialized = JSON.parse(serialized);
|
||||
|
||||
assert.ok(serialized, "labels can be serialized to JSON");
|
||||
assert.deepEqual(deserialized, labels, "deserialized labels match original");
|
||||
});
|
||||
|
||||
QUnit.test("empty labels object does not break initialization", function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
window.groupOrderShop.labels = {};
|
||||
|
||||
try {
|
||||
window.groupOrderShop._initTooltips();
|
||||
assert.ok(true, "initialization with empty labels does not throw error");
|
||||
} catch (e) {
|
||||
assert.ok(false, "initialization threw error: " + e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return {};
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue