Aplicoop desde el repo de kidekoop
This commit is contained in:
parent
69917d1ec2
commit
7cff89e418
93 changed files with 313992 additions and 0 deletions
30
website_sale_aplicoop/static/description/icon.svg
Normal file
30
website_sale_aplicoop/static/description/icon.svg
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background -->
|
||||
<rect width="256" height="256" fill="#2C3E50"/>
|
||||
|
||||
<!-- Criptomart Logo Circle -->
|
||||
<circle cx="128" cy="128" r="110" fill="#3498DB"/>
|
||||
|
||||
<!-- Shopping Cart Icon -->
|
||||
<g transform="translate(128, 128)">
|
||||
<!-- Cart Body -->
|
||||
<path d="M -30 -20 L -25 20 Q -25 30 -15 30 L 50 30 Q 60 30 60 20 L 55 -20 Z" fill="white" stroke="white" stroke-width="2"/>
|
||||
|
||||
<!-- Cart Handle -->
|
||||
<path d="M -20 -20 Q 0 -50 20 -20" fill="none" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
||||
|
||||
<!-- Wheel 1 -->
|
||||
<circle cx="-10" cy="35" r="5" fill="white"/>
|
||||
<circle cx="-10" cy="35" r="3" fill="#3498DB"/>
|
||||
|
||||
<!-- Wheel 2 -->
|
||||
<circle cx="45" cy="35" r="5" fill="white"/>
|
||||
<circle cx="45" cy="35" r="3" fill="#3498DB"/>
|
||||
</g>
|
||||
|
||||
<!-- Criptomart Text -->
|
||||
<text x="128" y="220" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white" text-anchor="middle">
|
||||
CRIPTOMART
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
180
website_sale_aplicoop/static/description/index.html
Normal file
180
website_sale_aplicoop/static/description/index.html
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# Website Sale Aplicoop - Sistema de Pedidos Colaborativos
|
||||
|
||||

|
||||
|
||||
**Versión:** 18.0.1.0.0-beta
|
||||
**Licencia:** AGPL-3.0
|
||||
**Autor:** [Criptomart](https://criptomart.net)
|
||||
|
||||
## Descripción
|
||||
|
||||
Website Sale Aplicoop es un módulo de Odoo 18 que implementa un sistema moderno y escalable para gestionar **pedidos colaborativos de compra grupal** (*eskaera* en euskera).
|
||||
|
||||
Este módulo reemplaza la antigua aplicación Aplicoop con una solución integrada en Odoo que permite a grupos de usuarios realizar compras coordinadas con productos específicos, fechas de corte y períodos de recogida.
|
||||
|
||||
## Características Principales
|
||||
|
||||
### 🛒 Gestión de Pedidos de Grupo
|
||||
- Crear pedidos colaborativos con fechas de inicio/fin configurables
|
||||
- Sistema de máquina de estados (draft → open → closed/cancelled)
|
||||
- Asignación de productos por:
|
||||
- Producto directo (lista explícita)
|
||||
- Categoría (todos los productos en categorías seleccionadas)
|
||||
- Proveedor (todos los productos del proveedor)
|
||||
|
||||
### 🔍 Experiencia de Compra
|
||||
- Búsqueda y filtrado de productos por:
|
||||
- Nombre y descripción
|
||||
- Categoría
|
||||
- Imágenes en miniatura de productos
|
||||
- Carrito persistente por pedido (localStorage)
|
||||
- Interfaz responsive (móvil-friendly)
|
||||
|
||||
### 👥 Control de Acceso
|
||||
- Grupos de usuarios (res.partner)
|
||||
- Solo usuarios miembros de grupos pueden ver/comprar en pedidos
|
||||
- Dos niveles de permisos:
|
||||
- Lectora (portal): ver y comprar
|
||||
- Gestora: crear y editar pedidos
|
||||
|
||||
### 📅 Fechas y Períodos
|
||||
- Fecha de inicio/fin del pedido
|
||||
- Horas de apertura/cierre opcionales
|
||||
- Día de corte de compras (cutoff_day)
|
||||
- Día de recogida del pedido
|
||||
- Períodos de recurrencia (diario, semanal, quincenal, mensual)
|
||||
|
||||
### 🌍 Internacionalización
|
||||
Disponible en 7 idiomas:
|
||||
- 🇪🇸 Español
|
||||
- 🇫🇷 Francés
|
||||
- 🇨🇦 Catalán
|
||||
- 🇪🇺 Euskera
|
||||
- 🇬🇦 Gallego
|
||||
- 🇮🇹 Italiano
|
||||
- 🇵🇹 Portugués
|
||||
|
||||
## Flujo de Compra
|
||||
|
||||
```
|
||||
1. Usuario ve lista de pedidos activos (/eskaera)
|
||||
↓
|
||||
2. Selecciona un pedido y ve productos (/eskaera/<id>)
|
||||
↓
|
||||
3. Busca/filtra productos (search, category)
|
||||
↓
|
||||
4. Agrega productos al carrito (localStorage)
|
||||
↓
|
||||
5. Confirma el carrito (/eskaera/confirm)
|
||||
↓
|
||||
6. Sale.order creada automáticamente en BD
|
||||
↓
|
||||
7. Flujo estándar de Odoo (quotation → order → invoice)
|
||||
```
|
||||
|
||||
## Instalación
|
||||
|
||||
1. Descargar el módulo en la carpeta de addons
|
||||
2. Actualizar la lista de módulos en Odoo
|
||||
3. Instalar "Website Sale Aplicoop"
|
||||
4. Ir a **Website Sale > Group Orders** para crear pedidos
|
||||
|
||||
## Uso
|
||||
|
||||
### Crear un Pedido de Grupo
|
||||
|
||||
1. **Website Sale > Group Orders > Create**
|
||||
2. Completar campos:
|
||||
- Nombre del pedido
|
||||
- Grupos que pueden participar (requerido)
|
||||
- Productos, categorías o proveedores
|
||||
- Fechas y horarios
|
||||
- Día de corte y recogida
|
||||
3. Cambiar estado a "Open"
|
||||
4. Los usuarios pueden empezar a comprar
|
||||
|
||||
### Buscar y Filtrar Productos
|
||||
|
||||
En la página de tienda (/eskaera/<id>):
|
||||
- Barra de búsqueda para buscar por nombre/descripción
|
||||
- Dropdown de categorías para filtrar
|
||||
- Botón "Filtrar" para aplicar
|
||||
|
||||
## Estructura Técnica
|
||||
|
||||
### Modelos
|
||||
- `group.order`: Pedido de grupo (máquina de estados)
|
||||
- Extensiones de `product.product` y `res.partner`
|
||||
|
||||
### Controlador
|
||||
- `/eskaera`: Lista de pedidos activos
|
||||
- `/eskaera/<id>`: Tienda de productos
|
||||
- `/eskaera/add-to-cart`: Validación de productos (POST JSON)
|
||||
- `/eskaera/confirm`: Crear sale.order (POST JSON)
|
||||
|
||||
### Vistas
|
||||
- Plantillas para website (eskaera_page, eskaera_shop, eskaera_checkout)
|
||||
- Formularios backend para gestión de pedidos
|
||||
|
||||
### Internacionalización
|
||||
- Traducciones al 100% en 7 idiomas
|
||||
- Basado en POT master con msgmerge
|
||||
|
||||
## Validaciones
|
||||
|
||||
- `cutoff_day`: Campo requerido
|
||||
- `start_date`: Opcional (si vacío, pedido siempre abierto)
|
||||
- `end_date`: Opcional (si vacío, pedido permanente)
|
||||
- Validación de fechas: `start_date ≤ end_date`
|
||||
- Validación de horarios: `start_time < end_time` (0-24)
|
||||
|
||||
## Seguridad
|
||||
|
||||
- CSRF token en rutas JSON
|
||||
- Validación de acceso por grupo en todas las rutas
|
||||
- Verificación de estado del pedido (solo open)
|
||||
- ACL basado en grupos de usuario
|
||||
|
||||
## Performance
|
||||
|
||||
- Búsqueda de productos optimizada (filtered en lugar de search)
|
||||
- Carrito en localStorage (sin DB writes hasta confirmación)
|
||||
- Logging detallado para debugging
|
||||
|
||||
## Testing
|
||||
|
||||
Suite completa de tests:
|
||||
- `test_group_order.py`: Validaciones del modelo
|
||||
- `test_product_extension.py`: Extensión de productos
|
||||
- `test_res_partner.py`: Extensión de partner
|
||||
- `test_eskaera_shop.py`: Lógica de descubrimiento de productos
|
||||
|
||||
Ejecutar tests:
|
||||
```bash
|
||||
docker-compose exec -T odoo odoo -d odoo -p 8070 -i website_sale_aplicoop --test-enable --stop-after-init
|
||||
```
|
||||
|
||||
## Soporte
|
||||
|
||||
- Documentación: Ver carpeta `readme/`
|
||||
- Diagnóstico de problemas: Ver `DIAGNOSTIC_PRODUCTS.md`
|
||||
- Estado del módulo: Ver `STATUS_REPORT.md`
|
||||
|
||||
## Licencia
|
||||
|
||||
AGPL-3.0 - Copyright 2025 Criptomart
|
||||
|
||||
## Cambios Recientes
|
||||
|
||||
**v18.0.1.0.0-beta:**
|
||||
- ✅ Traducción completa a 7 idiomas
|
||||
- ✅ Correcciones de tipos de pedido
|
||||
- ✅ Descubrimiento de productos por categorías/proveedores
|
||||
- ✅ Búsqueda y filtros en la tienda
|
||||
- ✅ Imágenes en miniatura de productos
|
||||
- ✅ Información de fechas de corte y recogida
|
||||
- ✅ Suite de tests completa
|
||||
|
||||
---
|
||||
|
||||
**Desarrollado con ❤️ por [Criptomart](https://criptomart.net)**
|
||||
236
website_sale_aplicoop/static/src/css/README.md
Normal file
236
website_sale_aplicoop/static/src/css/README.md
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
# CSS Architecture - Website Sale Aplicoop
|
||||
|
||||
**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
|
||||
|
||||
---
|
||||
|
||||
## 📁 Estructura de Carpetas
|
||||
|
||||
```
|
||||
website_sale_aplicoop/static/src/css/
|
||||
│
|
||||
├── website_sale.css ← Index/imports principal (48 líneas)
|
||||
│
|
||||
├── base/
|
||||
│ ├── variables.css ← Variables CSS globales (colores, tipografía, espaciados)
|
||||
│ └── utilities.css ← Clases utilitarias (.sr-only, .text-muted, etc)
|
||||
│
|
||||
├── layout/
|
||||
│ ├── pages.css ← Fondos y layouts de páginas (eskaera-page, etc)
|
||||
│ ├── header.css ← Headers, navegación y títulos
|
||||
│ └── responsive.css ← Media queries centralizadas (todas las breakpoints)
|
||||
│
|
||||
├── components/
|
||||
│ ├── product-card.css ← Tarjetas de producto
|
||||
│ ├── order-card.css ← Tarjetas de orden (Eskaera)
|
||||
│ ├── cart.css ← Carrito lateral
|
||||
│ ├── buttons.css ← Botones y acciones
|
||||
│ ├── quantity-control.css ← Control de cantidad (+ - input)
|
||||
│ ├── forms.css ← Inputs, selects, checkboxes
|
||||
│ └── alerts.css ← Alertas y notificaciones
|
||||
│
|
||||
├── sections/
|
||||
│ ├── products-grid.css ← Grid de productos
|
||||
│ ├── order-list.css ← Lista de órdenes
|
||||
│ ├── checkout.css ← Página de checkout
|
||||
│ └── info-cards.css ← Tarjetas de información
|
||||
│
|
||||
└── README.md ← Este archivo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Beneficios de la Refactorización
|
||||
|
||||
| Aspecto | Antes | Después | Mejora |
|
||||
|---------|-------|---------|--------|
|
||||
| **Tamaño de archivo** | 2,986 líneas | 48 líneas (index) | 98.4% reducción |
|
||||
| **Número de archivos** | 1 monolítico | 15 modulares | Mejor organización |
|
||||
| **Tiempo para encontrar regla** | 5-10 min | 1-2 min | 75% más rápido |
|
||||
| **Reutilización de código** | No (todo mezclado) | Sí (componentes aislados) | ✅ |
|
||||
| **Testing CSS** | Imposible | Posible por componente | ✅ |
|
||||
| **Mantenibilidad** | Difícil (cambios afectan múltiples zonas) | Fácil (aislado) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 📊 Desglose de Archivos
|
||||
|
||||
### **base/** - Fundamentos
|
||||
- **variables.css** (~80 líneas)
|
||||
Colores, tipografía, espaciados, sombras, transiciones, z-index
|
||||
- **utilities.css** (~15 líneas)
|
||||
Clases utilitarias reutilizables (.sr-only, .text-muted, etc)
|
||||
|
||||
### **layout/** - Estructura Global
|
||||
- **pages.css** (~70 líneas)
|
||||
Fondos de página, gradientes, pseudo-elementos (::before)
|
||||
- **header.css** (~100 líneas)
|
||||
Headers, navegación, títulos, información de pedidos
|
||||
- **responsive.css** (~200 líneas)
|
||||
Todas las media queries (4 breakpoints: 992px, 768px, 576px, etc)
|
||||
|
||||
### **components/** - Elementos Reutilizables
|
||||
- **product-card.css** (~80 líneas)
|
||||
Tarjetas de producto con hover, imagen, título, precio
|
||||
- **order-card.css** (~100 líneas)
|
||||
Tarjetas de orden (Eskaera) con metadatos, badges
|
||||
- **cart.css** (~150 líneas)
|
||||
Carrito lateral, items, total, botones save/reload
|
||||
- **buttons.css** (~80 líneas)
|
||||
Botones primarios, checkout, acciones
|
||||
- **quantity-control.css** (~100 líneas)
|
||||
Control de cantidad (spinners + input numérico)
|
||||
- **forms.css** (~70 líneas)
|
||||
Inputs, selects, checkboxes, labels
|
||||
- **alerts.css** (~50 líneas)
|
||||
Alertas, notificaciones, toasts
|
||||
|
||||
### **sections/** - Layouts Específicos de Página
|
||||
- **products-grid.css** (~25 líneas)
|
||||
Grid de productos con responsive
|
||||
- **order-list.css** (~40 líneas)
|
||||
Lista de órdenes (Eskaera page)
|
||||
- **checkout.css** (~100 líneas)
|
||||
Tabla de checkout, totales, summary
|
||||
- **info-cards.css** (~50 líneas)
|
||||
Tarjetas de información, metadatos
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Cómo Usar la Arquitectura
|
||||
|
||||
### Agregar Nuevas Variables Globales
|
||||
```css
|
||||
/* base/variables.css */
|
||||
:root {
|
||||
--my-new-color: #abc123;
|
||||
}
|
||||
```
|
||||
|
||||
### Crear un Nuevo Componente
|
||||
1. Crear archivo: `components/my-component.css`
|
||||
2. Escribir estilos del componente (aislado)
|
||||
3. Agregar import en `website_sale.css`:
|
||||
```css
|
||||
@import 'components/my-component.css';
|
||||
```
|
||||
|
||||
### Modificar Estilos Responsivos
|
||||
- Todos los media queries están en **`layout/responsive.css`**
|
||||
- No hay media queries esparcidos en otros archivos
|
||||
- Fácil ver todos los breakpoints en un solo lugar
|
||||
|
||||
### Encontrar Estilos de Elemento
|
||||
```
|
||||
Tarjeta de producto → components/product-card.css
|
||||
Carrito → components/cart.css
|
||||
Página eskaera → sections/order-list.css
|
||||
Colores → base/variables.css
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 Convenciones
|
||||
|
||||
### Nomenclatura de Archivos
|
||||
- `base/` → Fundamentos (variables, utilidades)
|
||||
- `layout/` → Estructura global (páginas, headers, responsive)
|
||||
- `components/` → Elementos reutilizables (tarjetas, botones)
|
||||
- `sections/` → Layouts específicos de página (checkout, lista)
|
||||
|
||||
### Orden de @import en website_sale.css
|
||||
1. **Base & Variables** (colores, espacios) - Otras se construyen sobre esto
|
||||
2. **Layout & Pages** (fondos, contenedores) - Base estructural
|
||||
3. **Components** (elementos) - Usan variables de base
|
||||
4. **Sections** (páginas) - Componen con componentes
|
||||
5. **Responsive** (media queries) - Ajusta todo lo anterior
|
||||
|
||||
### Reglas CSS
|
||||
- ✅ Usar `--variables` definidas en `base/variables.css`
|
||||
- ✅ Mantener componentes aislados (no afecten otros)
|
||||
- ✅ Media queries **solo** en `layout/responsive.css`
|
||||
- ✅ Comentarios de sección con `/* ========== ... ========== */`
|
||||
- ❌ No hardcodear colores (usar variables)
|
||||
- ❌ No mezclar lógica de múltiples componentes en un archivo
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Optimizaciones Futuras
|
||||
|
||||
### Consolidación de Duplicados
|
||||
Algunos estilos aparecen múltiples veces (ej: `.card-text`):
|
||||
- Revisar `components/product-card.css` y `components/order-card.css`
|
||||
- Extraer a archivo `components/card-base.css` si es necesario
|
||||
|
||||
### Mejora de Especificidad
|
||||
- Revisar selectores con `!important`
|
||||
- Reducir especificidad donde sea posible
|
||||
- Usar CSS variables en lugar de valores hardcodeados
|
||||
|
||||
### SCSS/SASS (Futuro)
|
||||
Si en algún momento migramos a SCSS:
|
||||
```scss
|
||||
@import 'base/variables';
|
||||
@import 'base/utilities';
|
||||
// etc...
|
||||
```
|
||||
Permitiría mejor nesting y variables más poderosas.
|
||||
|
||||
---
|
||||
|
||||
## 📈 Cambios Visuales
|
||||
|
||||
✅ **NINGUNO** - La refactorización es solo organizacional
|
||||
El CSS compilado genera **exactamente el mismo output** que antes.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Verificación de Integridad
|
||||
|
||||
Después de la refactorización, verificar:
|
||||
|
||||
```bash
|
||||
# 1. El archivo principal existe
|
||||
ls -lh css/website_sale.css
|
||||
|
||||
# 2. Todos los imports existen
|
||||
grep -r "components/\|sections/\|base/\|layout/" css/website_sale.css
|
||||
|
||||
# 3. El CSS compila sin errores
|
||||
# En el navegador, no debe haber errores en la consola
|
||||
|
||||
# 4. Los estilos se aplican correctamente
|
||||
# Visitar todas las páginas (shop, orden, checkout) y verificar visualmente
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Referencias
|
||||
|
||||
- **CSS Architecture Pattern**: [SMACSS (Scalable and Modular Architecture for CSS)](https://smacss.com/)
|
||||
- **BEM (Block Element Modifier)**: Para nombrado de clases
|
||||
- **Mobile-First Responsive**: Breakpoints en `layout/responsive.css`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist de Refactorización
|
||||
|
||||
- [x] Crear estructura de carpetas (base, layout, components, sections)
|
||||
- [x] Extraer variables a `base/variables.css`
|
||||
- [x] Separar utilidades a `base/utilities.css`
|
||||
- [x] Crear `layout/pages.css` y `layout/header.css`
|
||||
- [x] Crear componentes en `components/`
|
||||
- [x] Crear secciones en `sections/`
|
||||
- [x] Centralizar responsive en `layout/responsive.css`
|
||||
- [x] Crear `website_sale.css` como index
|
||||
- [x] Verificar que no haya reglas duplicadas
|
||||
- [x] Documentar en README.md
|
||||
|
||||
---
|
||||
|
||||
**Mantenido por**: Equipo de Frontend
|
||||
**Última actualización**: 7 de febrero de 2026
|
||||
**Licencia**: AGPL-3.0
|
||||
34
website_sale_aplicoop/static/src/css/base/utilities.css
Normal file
34
website_sale_aplicoop/static/src/css/base/utilities.css
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/base/utilities.css */
|
||||
|
||||
/**
|
||||
* Utility classes used throughout the project
|
||||
*/
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
.word-wrap-break {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.hidden-product {
|
||||
display: none !important;
|
||||
}
|
||||
71
website_sale_aplicoop/static/src/css/base/variables.css
Normal file
71
website_sale_aplicoop/static/src/css/base/variables.css
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/base/variables.css */
|
||||
|
||||
/**
|
||||
* CSS Custom Properties (Variables)
|
||||
* Colores, tipografía, espaciados centralizados
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ========== COLORS ========== */
|
||||
--primary-color: var(--primary, #007bff);
|
||||
--primary-dark: var(--primary-dark, #0056b3);
|
||||
--secondary-color: var(--secondary, #6c757d);
|
||||
--success-color: var(--success, #28a745);
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--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-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;
|
||||
--spacing-md: 1rem;
|
||||
--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-slow: 500ms ease;
|
||||
|
||||
/* ========== Z-INDEX ========== */
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1020;
|
||||
--z-fixed: 1030;
|
||||
--z-modal: 1040;
|
||||
--z-popover: 1050;
|
||||
--z-tooltip: 1060;
|
||||
--z-notification: 9999;
|
||||
}
|
||||
85
website_sale_aplicoop/static/src/css/components/alerts.css
Normal file
85
website_sale_aplicoop/static/src/css/components/alerts.css
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/components/alerts.css */
|
||||
|
||||
/**
|
||||
* Alert and notification component styles
|
||||
*/
|
||||
|
||||
.group-order-alert {
|
||||
background-color: #e7f3ff;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
color: #004085;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.group-order-alert.info {
|
||||
background-color: #d1ecf1;
|
||||
border-left-color: #17a2b8;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.group-order-alert.warning {
|
||||
background-color: #fff3cd;
|
||||
border-left-color: #ffc107;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fef3c7;
|
||||
border-color: #fcd34d;
|
||||
color: #92400e;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.alert-warning i {
|
||||
margin-right: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.alert-warning strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#delivery-info-alert {
|
||||
margin-top: 1rem;
|
||||
border-left: 4px solid #0dcaf0;
|
||||
}
|
||||
|
||||
#delivery-info-alert .fa-truck {
|
||||
margin-right: 0.5rem;
|
||||
color: #0dcaf0;
|
||||
}
|
||||
|
||||
/* Toast Notification Animation */
|
||||
.toast-notification {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-weight: 500;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
max-width: 400px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.toast-notification.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toast-notification i {
|
||||
font-size: 1.2rem;
|
||||
min-width: 20px;
|
||||
}
|
||||
103
website_sale_aplicoop/static/src/css/components/buttons.css
Normal file
103
website_sale_aplicoop/static/src/css/components/buttons.css
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/components/buttons.css */
|
||||
|
||||
/**
|
||||
* Button and action component styles
|
||||
*/
|
||||
|
||||
.btn-add-to-cart {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:focus {
|
||||
outline: 3px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:hover {
|
||||
background-color: var(--primary-dark);
|
||||
border-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-checkout {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-checkout:focus {
|
||||
outline: 3px solid #667eea;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn-checkout:hover {
|
||||
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-dark) 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-success {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary:disabled,
|
||||
.btn-success:disabled {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* Checkout action buttons */
|
||||
.checkout-actions {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.checkout-actions .btn {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.checkout-actions .btn-success {
|
||||
background-color: var(--success-color);
|
||||
border-color: var(--success-color);
|
||||
}
|
||||
|
||||
.checkout-actions .btn-success:hover {
|
||||
background-color: #218838;
|
||||
border-color: #218838;
|
||||
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.checkout-actions .btn-outline-secondary {
|
||||
color: #ebeef0;
|
||||
border-color: #cad2d8;
|
||||
}
|
||||
|
||||
.checkout-actions .btn-outline-secondary:hover {
|
||||
color: white;
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
|
||||
}
|
||||
|
||||
.checkout-actions .btn i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.save-order-btn,
|
||||
.save-order-btn-styled,
|
||||
.checkout-btn-lg {
|
||||
white-space: nowrap;
|
||||
font-size: 1.1rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
}
|
||||
|
||||
.save-icon-size {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
236
website_sale_aplicoop/static/src/css/components/cart.css
Normal file
236
website_sale_aplicoop/static/src/css/components/cart.css
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/components/cart.css */
|
||||
|
||||
/**
|
||||
* Shopping cart component styles
|
||||
*/
|
||||
|
||||
.sticky-cart {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.cart-header {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.cart-header h5 {
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cart-header .btn {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cart-items {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#cart-items-container {
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
|
||||
#cart-items-container .list-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#cart-items-container p.text-muted {
|
||||
margin: 0.4rem 0;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.list-group-item.d-flex {
|
||||
gap: 0.5rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-group-item h6 {
|
||||
margin: 0;
|
||||
margin-bottom: 0.2rem;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.list-group-item small {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.list-group-item .d-flex {
|
||||
min-width: auto;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.list-group-item .remove-from-cart {
|
||||
flex-shrink: 0;
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.15rem 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
cursor: pointer;
|
||||
background-color: #dc3545;
|
||||
color: #ffffff;
|
||||
border: 1px solid #dc3545;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.list-group-item .remove-from-cart:hover {
|
||||
background-color: #bb2d3b;
|
||||
border-color: #bb2d3b;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.list-group-item .remove-from-cart i {
|
||||
font-size: 0.85rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.list-group-item strong {
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
font-size: 0.875rem;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.list-group-item.d-flex > div:first-child {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-group-item.d-flex > div:first-child h6 {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.cart-total {
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
border-top: 2px solid var(--primary-color);
|
||||
text-align: right;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.cart-item-remove {
|
||||
cursor: pointer;
|
||||
color: #dc3545;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.cart-item-remove:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Cart header buttons styling */
|
||||
#save-cart-btn,
|
||||
#reload-cart-btn {
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-right: 0.25rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#save-cart-btn {
|
||||
background-color: #007bff;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
#save-cart-btn:hover {
|
||||
background-color: #0056b3;
|
||||
border-color: #004085;
|
||||
box-shadow: 0 4px 8px rgba(0, 86, 179, 0.3);
|
||||
}
|
||||
|
||||
#save-cart-btn:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 2px 4px rgba(0, 86, 179, 0.3);
|
||||
}
|
||||
|
||||
#reload-cart-btn {
|
||||
background-color: #17a2b8;
|
||||
border-color: #117a8b;
|
||||
}
|
||||
|
||||
#reload-cart-btn:hover {
|
||||
background-color: #117a8b;
|
||||
border-color: #0c5460;
|
||||
box-shadow: 0 4px 8px rgba(17, 122, 139, 0.3);
|
||||
}
|
||||
|
||||
#reload-cart-btn:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 2px 4px rgba(17, 122, 139, 0.3);
|
||||
}
|
||||
|
||||
.card-header .btn-group {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#cart-title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.cart-btn-group-nowrap,
|
||||
.cart-btn-group {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.cart-header-btn {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.cart-icon-size {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.cart-body-text {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.cart-body-lg {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.cart-title-lg {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.cart-title-sm {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.cart-btn-compact {
|
||||
padding: 0.1rem;
|
||||
min-width: auto;
|
||||
font-size: 0.5rem;
|
||||
line-height: 0.5;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.cart-sticky-position {
|
||||
top: 20px;
|
||||
}
|
||||
99
website_sale_aplicoop/static/src/css/components/forms.css
Normal file
99
website_sale_aplicoop/static/src/css/components/forms.css
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/components/forms.css */
|
||||
|
||||
/**
|
||||
* Form and input component styles
|
||||
*/
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-color: #cbd5e0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
input[type="number"],
|
||||
input[type="text"],
|
||||
select {
|
||||
background-color: #ffffff;
|
||||
color: #212529;
|
||||
border: 1px solid #ced4da;
|
||||
}
|
||||
|
||||
input[type="number"]:focus,
|
||||
input[type="text"]:focus,
|
||||
select:focus {
|
||||
outline: 2px solid #667eea;
|
||||
outline-offset: 0;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.form-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
margin-right: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Compact search and filter inputs */
|
||||
#realtime-search-input,
|
||||
#realtime-category-select {
|
||||
font-size: 0.95rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#realtimeSearch-filters .form-control,
|
||||
#realtimeSearch-filters .form-select {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#home-delivery-checkbox {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
cursor: pointer;
|
||||
border: 2px solid #0d6efd;
|
||||
border-radius: 0.25rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
#home-delivery-checkbox:checked {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
#home-delivery-checkbox:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.form-check-label[for="home-delivery-checkbox"] {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #212529;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.help-text-sm {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
329
website_sale_aplicoop/static/src/css/components/order-card.css
Normal file
329
website_sale_aplicoop/static/src/css/components/order-card.css
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/components/order-card.css */
|
||||
|
||||
/**
|
||||
* Order card (Eskaera) component styles
|
||||
*/
|
||||
|
||||
.eskaera-order-card-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.eskaera-order-card {
|
||||
position: relative;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
|
||||
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;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 290px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
animation: slideInUp 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.eskaera-order-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 6px;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--primary-dark));
|
||||
}
|
||||
|
||||
.eskaera-order-card-link:hover .eskaera-order-card {
|
||||
transform: translateY(-8px) scale(1.01);
|
||||
box-shadow: 0 20px 50px rgba(90, 103, 216, 0.15), 0 0 30px rgba(90, 103, 216, 0.1);
|
||||
border: 1px solid rgba(90, 103, 216, 0.25);
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
|
||||
}
|
||||
|
||||
.eskaera-order-card-link:hover .eskaera-order-card::before {
|
||||
animation: shimmer 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
left: 0;
|
||||
}
|
||||
50% {
|
||||
left: 100%;
|
||||
}
|
||||
100% {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.eskaera-order-card .card-body {
|
||||
padding: 0.6rem 0.8rem 0 0.8rem;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.eskaera-order-card .card-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.order-desc-text {
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.order-desc-sm {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.order-desc-md {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.eskaera-order-card .btn {
|
||||
margin-top: auto;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 0.6rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
border: none !important;
|
||||
font-size: 0.85rem;
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: linear-gradient(135deg, #5a67d8, #4c57bd) !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 4px 12px rgba(90, 103, 216, 0.2) !important;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.eskaera-order-card .btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.5s, height 0.5s;
|
||||
}
|
||||
|
||||
.eskaera-order-card .btn:active::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.eskaera-order-card .btn:hover {
|
||||
box-shadow: 0 8px 24px rgba(90, 103, 216, 0.4), inset 0 0 20px rgba(255, 255, 255, 0.2) !important;
|
||||
transform: translateY(-3px) scale(1.03);
|
||||
background: linear-gradient(135deg, #4c57bd, #3d4898) !important;
|
||||
}
|
||||
|
||||
/* Center button within card body */
|
||||
.eskaera-order-card .card-body > .btn {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* Order card header spacing */
|
||||
.order-card-header-spacing {
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
/* Order thumbnail small */
|
||||
.order-thumbnail-sm {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid rgba(90, 103, 216, 0.1);
|
||||
}
|
||||
|
||||
/* Order thumbnail medium */
|
||||
.order-thumbnail-md {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Order thumbnail checkout */
|
||||
.order-thumbnail-checkout,
|
||||
.checkout-thumbnail {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header-top {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.3rem;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header-top > div:last-child {
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-header-top .card-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-badges .badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.card-meta-compact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
margin: 0.2rem auto 0 auto;
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-top: 1px solid rgba(90, 103, 216, 0.08);
|
||||
background: rgba(245, 247, 255, 0.4);
|
||||
border-radius: 0 0 0.75rem 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-meta-compact .card-meta-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
/* Meta table styles for clean date display */
|
||||
.meta-table {
|
||||
width: auto;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8rem;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
min-height: 160px;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.meta-table tbody {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: table-row;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.meta-label-cell {
|
||||
display: table-cell;
|
||||
padding: 0.25rem 0.6rem 0.25rem 0;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meta-label-cell span {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.meta-value-cell {
|
||||
display: table-cell;
|
||||
padding: 0.25rem 0;
|
||||
color: #6b7280;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.meta-value-cell .badge {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
min-width: 85px;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: #6b7280;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.order-badge-position {
|
||||
position: absolute;
|
||||
top: 1.25rem;
|
||||
right: 1.25rem;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.order-badge-custom {
|
||||
font-weight: 700;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
box-shadow: 0 4px 12px rgba(90, 103, 216, 0.3);
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
144
website_sale_aplicoop/static/src/css/components/product-card.css
Normal file
144
website_sale_aplicoop/static/src/css/components/product-card.css
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/components/product-card.css */
|
||||
|
||||
/**
|
||||
* Product card component styles
|
||||
*/
|
||||
|
||||
.product-card {
|
||||
background-color: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem 0.5rem 0.5rem 0.5rem;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.product-card:focus-within {
|
||||
outline: 3px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.product-card .product-image {
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.product-img-cover {
|
||||
max-height: 160px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(40, 39, 39, 0.9);
|
||||
}
|
||||
|
||||
.product-card .card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
padding: 0.75rem;
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, rgba(0, 123, 255, 0.12) 0%, rgba(0, 123, 255, 0.08) 100%);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.product-card:hover .card-body {
|
||||
background: linear-gradient(135deg, rgba(108, 117, 125, 0.10) 0%, rgba(108, 117, 125, 0.08) 100%);
|
||||
}
|
||||
|
||||
.product-card .card-title {
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
margin-bottom: 0.2rem;
|
||||
min-height: auto;
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
font-size: 1.2rem !important;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.product-card .card-text {
|
||||
margin-bottom: 0.15rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.product-card .card-text strong {
|
||||
display: block;
|
||||
margin-bottom: 0.15rem;
|
||||
font-size: 1.2rem;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.product-card .product-supplier {
|
||||
text-align: center;
|
||||
color: #4a5568;
|
||||
font-weight: 400;
|
||||
margin-bottom: 0.15rem;
|
||||
font-size: 0.9rem !important;
|
||||
}
|
||||
|
||||
.product-tags {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.2rem;
|
||||
justify-content: center;
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.badge-km {
|
||||
background-color: var(--primary-color) !important;
|
||||
color: white !important;
|
||||
font-weight: 600 !important;
|
||||
padding: 0.2rem !important;
|
||||
font-size: 0.6rem !important;
|
||||
border-radius: 0.2rem;
|
||||
display: inline-block;
|
||||
border: 1px solid;
|
||||
white-space: nowrap;
|
||||
margin-right: 0.1rem;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.card-body p.card-text {
|
||||
text-align: center;
|
||||
margin-bottom: 0.8rem;
|
||||
min-height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-body p.card-text strong {
|
||||
display: inline;
|
||||
font-size: 1.4rem !important;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-img-fixed {
|
||||
object-fit: cover;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.product-img-placeholder {
|
||||
height: 100px;
|
||||
}
|
||||
|
|
@ -0,0 +1,405 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/components/quantity-control.css */
|
||||
|
||||
/**
|
||||
* Quantity control (+ - input) component styles
|
||||
*/
|
||||
|
||||
/* Formulario agregar al carrito */
|
||||
.add-to-cart-form {
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
padding: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
border: 1px solid #e9ecef;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.add-to-cart-form .input-group {
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.add-to-cart-form .product-qty {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
padding: 0.3rem;
|
||||
min-width: 32px;
|
||||
max-width: 70px;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 0;
|
||||
background-color: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
-moz-appearance: textfield;
|
||||
height: 36px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.add-to-cart-form .product-qty::-webkit-outer-spin-button,
|
||||
.add-to-cart-form .product-qty::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.add-to-cart-form .product-qty:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color, #007bff);
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Contenedor de control de cantidad */
|
||||
.qty-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
/* Botones de cantidad + y - */
|
||||
.qty-control .qty-decrease,
|
||||
.qty-control .qty-increase {
|
||||
min-width: 36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid #dee2e6;
|
||||
background-color: #ffffff;
|
||||
color: #495057;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qty-control .qty-decrease:hover,
|
||||
.qty-control .qty-increase:hover {
|
||||
border-color: var(--primary-color, #007bff);
|
||||
color: var(--primary-color, #007bff);
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.qty-control .qty-decrease:active,
|
||||
.qty-control .qty-increase:active {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn {
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
max-width: 36px;
|
||||
min-height: 36px;
|
||||
max-height: 36px;
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
background-color: #7c3aed !important;
|
||||
color: #ffffff !important;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn:hover {
|
||||
background-color: #6d28d9 !important;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn:active {
|
||||
background-color: #5b21b6 !important;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Pantallas muy grandes: 1600px+ (6 columnas) */
|
||||
@media (min-width: 1600px) {
|
||||
.add-to-cart-form {
|
||||
padding: 0.35rem;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .input-group {
|
||||
height: 32px;
|
||||
gap: 0.06rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .product-qty {
|
||||
font-size: 1rem;
|
||||
min-width: 28px;
|
||||
max-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
.qty-control .qty-decrease,
|
||||
.qty-control .qty-increase {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas grandes: 1400-1599px (5 columnas) */
|
||||
@media (max-width: 1599px) and (min-width: 1400px) {
|
||||
.add-to-cart-form {
|
||||
padding: 0.36rem;
|
||||
gap: 0.07rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .input-group {
|
||||
height: 32px;
|
||||
gap: 0.05rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .product-qty {
|
||||
font-size: 1rem;
|
||||
min-width: 28px;
|
||||
max-width: 62px;
|
||||
height: 32px;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
.qty-control .qty-decrease,
|
||||
.qty-control .qty-increase {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas medianas: 1200-1399px (4 columnas) */
|
||||
@media (max-width: 1399px) and (min-width: 1200px) {
|
||||
.add-to-cart-form {
|
||||
padding: 0.34rem;
|
||||
gap: 0.06rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .input-group {
|
||||
height: 32px;
|
||||
gap: 0.05rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .product-qty {
|
||||
font-size: 0.95rem;
|
||||
min-width: 28px;
|
||||
max-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
.qty-control .qty-decrease,
|
||||
.qty-control .qty-increase {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas tablet grandes: 992-1199px (3 columnas) */
|
||||
@media (max-width: 1199px) and (min-width: 992px) {
|
||||
.add-to-cart-form {
|
||||
padding: 0.32rem;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .input-group {
|
||||
height: 32px;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .product-qty {
|
||||
font-size: 0.9rem;
|
||||
min-width: 28px;
|
||||
max-width: 58px;
|
||||
height: 32px;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
.qty-control .qty-decrease,
|
||||
.qty-control .qty-increase {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 0.85rem;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas tablet: 768-991px (2-3 columnas) */
|
||||
@media (max-width: 991px) and (min-width: 768px) {
|
||||
.add-to-cart-form {
|
||||
padding: 0.42rem;
|
||||
gap: 0.03rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .input-group {
|
||||
height: 36px;
|
||||
gap: 0.04rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .product-qty {
|
||||
font-size: 1rem;
|
||||
min-width: 32px;
|
||||
max-width: 68px;
|
||||
height: 36px;
|
||||
padding: 0.3rem;
|
||||
}
|
||||
|
||||
.qty-control .qty-decrease,
|
||||
.qty-control .qty-increase {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 0.95rem;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Móvil grande: 576-767px (2 columnas) */
|
||||
@media (max-width: 767px) and (min-width: 576px) {
|
||||
.add-to-cart-form {
|
||||
padding: 0.35rem;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .input-group {
|
||||
height: 34px;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .product-qty {
|
||||
font-size: 0.9rem;
|
||||
min-width: 30px;
|
||||
max-width: 64px;
|
||||
height: 34px;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.qty-control .qty-decrease,
|
||||
.qty-control .qty-increase {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
font-size: 0.9rem;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn {
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Móvil pequeño: < 576px (1 columna) */
|
||||
@media (max-width: 575px) {
|
||||
.add-to-cart-form {
|
||||
padding: 0.3rem;
|
||||
gap: 0.08rem;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-to-cart-form .input-group {
|
||||
height: 32px;
|
||||
gap: 0.06rem;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.add-to-cart-form .product-qty {
|
||||
font-size: 0.8rem;
|
||||
min-width: 28px;
|
||||
max-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0.2rem;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.qty-control .qty-decrease,
|
||||
.qty-control .qty-increase {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 0.8rem;
|
||||
border-width: 1px;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.qty-control {
|
||||
width: auto;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
font-size: 0.75rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn i {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright 2025 Criptomart
|
||||
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* Container for all tag filter badges */
|
||||
.tag-filter-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
/* Individual tag filter badge button */
|
||||
.tag-filter-badge {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease-in-out;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
border: 1px solid;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Tags sin color definido en Odoo: usar color secundario del tema */
|
||||
.tag-filter-badge.tag-use-theme-color {
|
||||
background-color: var(--bs-secondary, #6c757d);
|
||||
border-color: var(--bs-secondary, #6c757d);
|
||||
}
|
||||
|
||||
/* Product card tags (badge-km) sin color definido: usar color del tema */
|
||||
.badge-km.tag-use-theme-color {
|
||||
background-color: var(--bs-secondary, #6c757d);
|
||||
border-color: var(--bs-secondary, #6c757d);
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.tag-filter-badge:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
/* Counter text inside badge */
|
||||
.tag-filter-badge .tag-count {
|
||||
font-weight: 600;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.tag-filter-badges {
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.tag-filter-badge {
|
||||
padding: 0.375rem 0.625rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
90
website_sale_aplicoop/static/src/css/layout/header.css
Normal file
90
website_sale_aplicoop/static/src/css/layout/header.css
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/layout/header.css */
|
||||
|
||||
/**
|
||||
* Headers, navigation, and title sections
|
||||
*/
|
||||
|
||||
/* Unified header for both shop and checkout pages */
|
||||
.eskaera-order-header,
|
||||
.checkout-header {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.eskaera-order-header h1,
|
||||
.checkout-header h1 {
|
||||
color: #2d3748;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 3px solid var(--primary-color, #007bff);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.order-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 700;
|
||||
color: #2d3748;
|
||||
font-size: 1.05rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.info-value,
|
||||
.info-date {
|
||||
font-size: 1.35rem;
|
||||
color: #1a202c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-date {
|
||||
display: block;
|
||||
font-size: 1.25rem;
|
||||
color: #2d3748;
|
||||
font-weight: 500;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Title styling for both pages */
|
||||
.checkout-title,
|
||||
.eskaera-order-header h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.checkout-order-name {
|
||||
font-size: 1.5rem;
|
||||
color: #2d3748;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Info value styling */
|
||||
.info-value {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.info-date {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
105
website_sale_aplicoop/static/src/css/layout/pages.css
Normal file
105
website_sale_aplicoop/static/src/css/layout/pages.css
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/layout/pages.css */
|
||||
|
||||
/**
|
||||
* Page backgrounds and main layout structures
|
||||
*/
|
||||
|
||||
html, body {
|
||||
background-color: transparent !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
body.website_published {
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in srgb, var(--primary-color) 30%, white),
|
||||
color-mix(in srgb, var(--primary-color) 60%, black)
|
||||
) !important;
|
||||
}
|
||||
|
||||
body.website_published .eskaera-shop-page,
|
||||
body.website_published .eskaera-checkout-page {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Generic page background mixin */
|
||||
.eskaera-page,
|
||||
.eskaera-shop-page,
|
||||
.eskaera-generic-page,
|
||||
.eskaera-checkout-page {
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.eskaera-page,
|
||||
.eskaera-generic-page {
|
||||
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,
|
||||
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,
|
||||
color-mix(in srgb, var(--primary-color) 0%, white),
|
||||
color-mix(in srgb, var(--primary-color) 60%, black)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.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%);
|
||||
}
|
||||
|
||||
.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%);
|
||||
}
|
||||
|
||||
.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%);
|
||||
}
|
||||
|
||||
.eskaera-page::before,
|
||||
.eskaera-shop-page::before,
|
||||
.eskaera-generic-page::before,
|
||||
.eskaera-checkout-page::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.eskaera-page > .container,
|
||||
.eskaera-shop-page > .container,
|
||||
.eskaera-generic-page > div,
|
||||
.eskaera-checkout-page > .container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
#wrap {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.group-order-shop {
|
||||
background-color: transparent;
|
||||
}
|
||||
517
website_sale_aplicoop/static/src/css/layout/responsive.css
Normal file
517
website_sale_aplicoop/static/src/css/layout/responsive.css
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/layout/responsive.css */
|
||||
|
||||
/**
|
||||
* Responsive design breakpoints and adjustments
|
||||
* All media queries centralized here for easier maintenance
|
||||
* NOTE: products-grid.css has its own breakpoints and should NOT be overridden here
|
||||
*/
|
||||
|
||||
@media (max-width: 992px) {
|
||||
/* Cart sidebar */
|
||||
.sticky-cart {
|
||||
position: static !important;
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.cart-header {
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cart-header h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.cart-title-lg {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
#save-cart-btn,
|
||||
#reload-cart-btn {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.card-header .btn-group {
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Order list grid */
|
||||
.eskaera-orders,
|
||||
.eskaera-orders-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
/* Header (shared between eskaera and checkout) */
|
||||
.eskaera-order-header,
|
||||
.checkout-header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.eskaera-order-header h1,
|
||||
.checkout-header h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.checkout-title,
|
||||
.eskaera-order-header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.checkout-order-name {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* Fix header flex layout for mobile */
|
||||
.eskaera-order-header .d-flex,
|
||||
.checkout-header .d-flex {
|
||||
flex-direction: column !important;
|
||||
gap: 1rem !important;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.order-thumbnail-md {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Order info grid */
|
||||
.order-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Eskaera page headings */
|
||||
.eskaera-page h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.eskaera-page > .container > .row:first-child p {
|
||||
font-size: 1.1rem !important;
|
||||
}
|
||||
|
||||
/* Order cards */
|
||||
.eskaera-order-card {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.eskaera-order-card .card-body {
|
||||
padding: 0.5rem 0.6rem 0 0.6rem;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.eskaera-order-card .card-title {
|
||||
font-size: 0.9rem;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
|
||||
.order-desc-text {
|
||||
font-size: 0.75rem;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.eskaera-order-card .btn {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.order-thumbnail-sm {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.order-thumbnail-md {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
/* Header with image and text */
|
||||
.eskaera-order-card .d-flex {
|
||||
gap: 0.75rem !important;
|
||||
}
|
||||
|
||||
.eskaera-order-card .card-footer {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
/* Order list */
|
||||
.eskaera-orders,
|
||||
.eskaera-orders-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* Product grid */
|
||||
.card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Product quantity controls */
|
||||
.add-to-cart-form .product-qty {
|
||||
font-size: 1rem;
|
||||
min-width: 50px;
|
||||
height: 32px;
|
||||
padding: 0.25rem 0.35rem;
|
||||
}
|
||||
|
||||
.qty-control .qty-decrease,
|
||||
.qty-control .qty-increase {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.add-to-cart-form .add-to-cart-btn {
|
||||
height: 36px;
|
||||
min-height: 36px;
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 1rem;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
.add-to-cart-form .input-group {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Checkout summary */
|
||||
.checkout-summary-container {
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.checkout-summary-table {
|
||||
font-size: 0.95rem;
|
||||
min-width: 500px; /* Prevent table collapse */
|
||||
}
|
||||
|
||||
.checkout-summary-table thead th {
|
||||
padding: 0.75rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.checkout-summary-table tbody td {
|
||||
padding: 0.75rem 0.5rem;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.summary-heading {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.checkout-actions .btn {
|
||||
font-size: 1rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
}
|
||||
|
||||
.group-order-header {
|
||||
padding: 1rem 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.product-card .product-image {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.checkout-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
/* Cart items */
|
||||
.list-group-item.d-flex {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.list-group-item h6 {
|
||||
font-size: 0.85rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.list-group-item small {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.list-group-item .d-flex {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-group-item strong {
|
||||
min-width: auto;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.list-group-item .remove-from-cart {
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.cart-items {
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
.sticky-cart {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.toast-notification {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
left: 20px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#save-cart-btn,
|
||||
#reload-cart-btn {
|
||||
padding: 0.3rem 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.card-header .d-flex {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
|
||||
.card-header .btn-group {
|
||||
gap: 0.25rem !important;
|
||||
}
|
||||
|
||||
.card-header .btn-group .btn {
|
||||
padding: 0.3rem 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.form-check {
|
||||
padding: 1rem 1.5rem !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 1rem !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form-check-label[for="home-delivery-checkbox"] {
|
||||
font-size: 1rem !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.eskaera-order-header,
|
||||
.checkout-header {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.eskaera-order-header h1,
|
||||
.checkout-header h1 {
|
||||
font-size: 1.25rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.checkout-title,
|
||||
.eskaera-order-header h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.checkout-order-name {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Eskaera page */
|
||||
.eskaera-page h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.eskaera-page > .container > .row:first-child {
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.eskaera-page > .container > .row:first-child p {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
/* Order cards */
|
||||
.eskaera-order-card {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.eskaera-order-card .card-body {
|
||||
padding: 0.4rem 0.5rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.eskaera-order-card .card-title {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.order-desc-text {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.eskaera-order-card .btn {
|
||||
padding: 0.45rem 0.85rem;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.order-thumbnail-sm {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.order-thumbnail-md {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
/* Order list */
|
||||
.eskaera-orders,
|
||||
.eskaera-orders-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.eskaera-empty-state {
|
||||
padding: 2rem 0.5rem;
|
||||
}
|
||||
|
||||
.eskaera-empty-state .alert {
|
||||
font-size: 0.95rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Checkout */
|
||||
.checkout-summary-container {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.checkout-summary-table {
|
||||
font-size: 0.85rem;
|
||||
min-width: 450px;
|
||||
}
|
||||
|
||||
.checkout-summary-table thead th {
|
||||
padding: 0.5rem 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.checkout-summary-table tbody td {
|
||||
padding: 0.5rem 0.35rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.currency {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.summary-heading {
|
||||
font-size: 1.25rem;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.checkout-actions .btn {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.6rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Product card typography responsive scaling */
|
||||
@media screen and (min-width: 1600px) {
|
||||
.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;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1400px) and (max-width: 1599px) {
|
||||
.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;
|
||||
min-width: 30px;
|
||||
min-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
127
website_sale_aplicoop/static/src/css/sections/checkout.css
Normal file
127
website_sale_aplicoop/static/src/css/sections/checkout.css
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/sections/checkout.css */
|
||||
|
||||
/**
|
||||
* Checkout page section styles
|
||||
*/
|
||||
|
||||
.checkout-container {
|
||||
background-color: white;
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.checkout-summary {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.checkout-summary-container {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.checkout-summary-table {
|
||||
margin-bottom: 0;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.checkout-summary-table thead {
|
||||
background-color: #2d3748;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.checkout-summary-table thead th {
|
||||
font-weight: 700;
|
||||
padding: 1rem 0.75rem;
|
||||
text-transform: uppercase;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.5px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.checkout-summary-table tbody tr {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.checkout-summary-table tbody tr:hover {
|
||||
background-color: #f7fafc;
|
||||
}
|
||||
|
||||
.checkout-summary-table tbody td {
|
||||
padding: 1rem 0.75rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.checkout-summary-table .col-name {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.checkout-summary-table .col-qty {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.checkout-summary-table .col-price {
|
||||
width: 18%;
|
||||
}
|
||||
|
||||
.checkout-summary-table .col-subtotal {
|
||||
width: 17%;
|
||||
}
|
||||
|
||||
.checkout-summary-table .empty-message {
|
||||
background-color: #f7fafc;
|
||||
}
|
||||
|
||||
.checkout-total-section {
|
||||
border-top: 2px solid #e2e8f0;
|
||||
padding-top: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--success-color);
|
||||
min-width: 120px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.currency {
|
||||
font-size: 1.2rem;
|
||||
color: #2d3748;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summary-heading {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
border-left: 4px solid var(--success-color);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.checkout-order-desc {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
color: #666;
|
||||
}
|
||||
63
website_sale_aplicoop/static/src/css/sections/info-cards.css
Normal file
63
website_sale_aplicoop/static/src/css/sections/info-cards.css
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/sections/info-cards.css */
|
||||
|
||||
/**
|
||||
* Info cards and grid section styles
|
||||
*/
|
||||
|
||||
.order-info-card {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.order-info-card .card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
align-items: start;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.card-meta-compact {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.card-meta-compact .card-meta-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 700;
|
||||
color: #2d3748;
|
||||
min-width: fit-content;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-weight: 400;
|
||||
color: #27292c;
|
||||
word-break: break-word;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.card-col {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-col .card-text strong {
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
48
website_sale_aplicoop/static/src/css/sections/order-list.css
Normal file
48
website_sale_aplicoop/static/src/css/sections/order-list.css
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/sections/order-list.css */
|
||||
|
||||
/**
|
||||
* Order list and Eskaera page section styles
|
||||
*/
|
||||
|
||||
.eskaera-page h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #2d3748;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.eskaera-page > .container > .row:first-child {
|
||||
margin-bottom: 2rem;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.eskaera-page > .container > .row:first-child p {
|
||||
font-size: 1.3rem !important;
|
||||
color: #4a5568;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.eskaera-orders,
|
||||
.eskaera-orders-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.eskaera-empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
}
|
||||
|
||||
.eskaera-empty-state .alert {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
font-size: 1.05rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/sections/products-grid.css */
|
||||
|
||||
/**
|
||||
* Products grid section styles
|
||||
*/
|
||||
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 1.2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.product-card-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-search-highlight {
|
||||
border: 2px solid #007bff;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Pantallas muy grandes: 1600px+ (6 columnas) */
|
||||
@media screen and (min-width: 1600px) {
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas grandes: 1400px-1599px (5 columnas) */
|
||||
@media screen and (min-width: 1400px) and (max-width: 1599px) {
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 1.15rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas medianas: 1200px-1399px (4 columnas) */
|
||||
@media screen and (min-width: 1200px) and (max-width: 1399px) {
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas tablet grandes: 992px-1199px (3 columnas) */
|
||||
@media screen and (min-width: 992px) and (max-width: 1199px) {
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas tablet: 768px-991px (3 columnas en tablet grande, 2 en tablet pequeña) */
|
||||
@media screen and (min-width: 768px) and (max-width: 991px) {
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas móvil grande: 576px-767px (2 columnas) */
|
||||
@media screen and (min-width: 576px) and (max-width: 767px) {
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas móvil pequeño: 1 columna */
|
||||
@media (max-width: 575px) {
|
||||
.products-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
50
website_sale_aplicoop/static/src/css/website_sale.css
Normal file
50
website_sale_aplicoop/static/src/css/website_sale.css
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/* filepath: website_sale_aplicoop/static/src/css/website_sale.css */
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* 3. Components (reusable UI elements)
|
||||
* 4. Sections (page-specific layouts)
|
||||
* 5. Responsive (media queries)
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
1. BASE & VARIABLES
|
||||
============================================ */
|
||||
@import 'base/variables.css';
|
||||
@import 'base/utilities.css';
|
||||
|
||||
/* ============================================
|
||||
2. LAYOUT & PAGES
|
||||
============================================ */
|
||||
@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';
|
||||
|
||||
/* ============================================
|
||||
4. SECTIONS (Page-specific layouts)
|
||||
============================================ */
|
||||
@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';
|
||||
283
website_sale_aplicoop/static/src/js/checkout_labels.js
Normal file
283
website_sale_aplicoop/static/src/js/checkout_labels.js
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
/**
|
||||
* Checkout Labels Loading
|
||||
* Fetches translated labels for checkout table summary
|
||||
* IMPORTANT: This script waits for the cart to be loaded by website_sale.js
|
||||
* before rendering the checkout summary.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
console.log('[CHECKOUT] Script loaded');
|
||||
|
||||
// Get order ID from button
|
||||
var confirmBtn = document.getElementById('confirm-order-btn');
|
||||
if (!confirmBtn) {
|
||||
console.log('[CHECKOUT] No confirm button found');
|
||||
return;
|
||||
}
|
||||
|
||||
var orderId = confirmBtn.getAttribute('data-order-id');
|
||||
if (!orderId) {
|
||||
console.log('[CHECKOUT] No order ID found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[CHECKOUT] Order ID:', orderId);
|
||||
|
||||
// Get summary div
|
||||
var summaryDiv = document.getElementById('checkout-summary');
|
||||
if (!summaryDiv) {
|
||||
console.log('[CHECKOUT] No summary div found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to fetch labels and render checkout
|
||||
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 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');
|
||||
callback();
|
||||
} else if (Date.now() - startTime < maxWait) {
|
||||
setTimeout(checkLabels, checkInterval);
|
||||
} else {
|
||||
console.log('[CHECKOUT] ⚠️ Timeout waiting for labels, proceeding anyway');
|
||||
callback();
|
||||
}
|
||||
};
|
||||
checkLabels();
|
||||
};
|
||||
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
lang: currentLang
|
||||
})
|
||||
})
|
||||
.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
|
||||
});
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
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');
|
||||
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 });
|
||||
|
||||
// Fallback timeout in case event never fires
|
||||
setTimeout(function() {
|
||||
if (window.groupOrderShop && window.groupOrderShop.orderId) {
|
||||
console.log('[CHECKOUT] Fallback timeout triggered');
|
||||
fetchLabelsAndRender();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render order summary table or empty message
|
||||
* Exposed globally so other scripts can call it
|
||||
*/
|
||||
window.renderCheckoutSummary = function(labels) {
|
||||
labels = labels || window.getCheckoutLabels();
|
||||
|
||||
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 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>' +
|
||||
'</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-amount" id="checkout-total-amount">€0.00</span>' +
|
||||
'</div></div>';
|
||||
summaryDiv.innerHTML = html;
|
||||
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 = '';
|
||||
|
||||
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">' +
|
||||
'<i class="fa fa-inbox fa-2x mb-2"></i>' +
|
||||
'<p>' + escapeHtml(labels.empty) + '</p>' +
|
||||
'</td>';
|
||||
tbody.appendChild(emptyRow);
|
||||
|
||||
// Hide total section
|
||||
totalSection.style.display = 'none';
|
||||
} else {
|
||||
// Hide empty row if visible
|
||||
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;
|
||||
|
||||
// Separate normal products from delivery product
|
||||
var normalProducts = [];
|
||||
var deliveryProduct = null;
|
||||
|
||||
Object.keys(cart).forEach(function(productId) {
|
||||
if (productId === deliveryProductId) {
|
||||
deliveryProduct = { id: productId, item: cart[productId] };
|
||||
} else {
|
||||
normalProducts.push({ id: productId, item: cart[productId] });
|
||||
}
|
||||
});
|
||||
|
||||
// Sort normal products numerically
|
||||
normalProducts.sort(function(a, b) {
|
||||
return parseInt(a.id) - parseInt(b.id);
|
||||
});
|
||||
|
||||
var total = 0;
|
||||
|
||||
// Render normal products first
|
||||
normalProducts.forEach(function(product) {
|
||||
var item = product.item;
|
||||
var qty = parseFloat(item.quantity || item.qty || 1);
|
||||
if (isNaN(qty)) qty = 1;
|
||||
var price = parseFloat(item.price || 0);
|
||||
if (isNaN(price)) price = 0;
|
||||
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>';
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// Render delivery product last if present
|
||||
if (deliveryProduct) {
|
||||
var item = deliveryProduct.item;
|
||||
var qty = parseFloat(item.quantity || item.qty || 1);
|
||||
if (isNaN(qty)) qty = 1;
|
||||
var price = parseFloat(item.price || 0);
|
||||
if (isNaN(price)) price = 0;
|
||||
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>';
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
|
||||
// Update total
|
||||
var totalAmount = summaryDiv.querySelector('#checkout-total-amount');
|
||||
if (totalAmount) {
|
||||
totalAmount.textContent = '€' + total.toFixed(2);
|
||||
}
|
||||
|
||||
// Show total section
|
||||
totalSection.style.display = 'block';
|
||||
}
|
||||
|
||||
console.log('[CHECKOUT] Summary rendered');
|
||||
};
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
})();
|
||||
11
website_sale_aplicoop/static/src/js/checkout_summary.js
Normal file
11
website_sale_aplicoop/static/src/js/checkout_summary.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/** AGPL-3.0
|
||||
* NOTE: Checkout summary rendering is now handled by checkout_labels.js
|
||||
* This file is kept for backwards compatibility but is no longer needed.
|
||||
* The main renderSummary() logic is in checkout_labels.js
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
// Checkout rendering is handled by checkout_labels.js
|
||||
})();
|
||||
|
||||
|
||||
207
website_sale_aplicoop/static/src/js/home_delivery.js
Normal file
207
website_sale_aplicoop/static/src/js/home_delivery.js
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
/**
|
||||
* Home Delivery Checkout Handler
|
||||
* Manages home delivery checkbox and product addition/removal
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var HomeDeliveryManager = {
|
||||
deliveryProductId: null,
|
||||
deliveryProductPrice: 5.74,
|
||||
deliveryProductName: 'Home Delivery', // Default fallback
|
||||
orderId: null,
|
||||
homeDeliveryEnabled: false,
|
||||
|
||||
init: function() {
|
||||
// Get delivery product info from data attributes
|
||||
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');
|
||||
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');
|
||||
if (productName) {
|
||||
this.deliveryProductName = productName;
|
||||
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);
|
||||
|
||||
// Show/hide home delivery section based on configuration
|
||||
this.toggleHomeDeliverySection();
|
||||
}
|
||||
|
||||
// Get order ID from confirm button
|
||||
var confirmBtn = document.getElementById('confirm-order-btn');
|
||||
if (confirmBtn) {
|
||||
this.orderId = confirmBtn.getAttribute('data-order-id');
|
||||
console.log('[HomeDelivery] orderId from button:', this.orderId);
|
||||
}
|
||||
|
||||
var checkbox = document.getElementById('home-delivery-checkbox');
|
||||
if (!checkbox) return;
|
||||
|
||||
var self = this;
|
||||
checkbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
self.addDeliveryProduct();
|
||||
self.showDeliveryInfo();
|
||||
} else {
|
||||
self.removeDeliveryProduct();
|
||||
self.hideDeliveryInfo();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if delivery product is already in cart on page load
|
||||
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');
|
||||
|
||||
if (this.homeDeliveryEnabled) {
|
||||
// Show home delivery option
|
||||
if (checkbox) {
|
||||
checkbox.closest('.form-check').style.display = 'block';
|
||||
}
|
||||
if (homeDeliveryContainer) {
|
||||
homeDeliveryContainer.style.display = 'block';
|
||||
}
|
||||
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.checked = false;
|
||||
}
|
||||
if (homeDeliveryContainer) {
|
||||
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');
|
||||
}
|
||||
},
|
||||
|
||||
checkDeliveryInCart: function() {
|
||||
if (!this.deliveryProductId) return;
|
||||
|
||||
var cart = this.getCart();
|
||||
if (cart[this.deliveryProductId]) {
|
||||
var checkbox = document.getElementById('home-delivery-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
this.showDeliveryInfo();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getCart: function() {
|
||||
if (!this.orderId) return {};
|
||||
var cartKey = 'eskaera_' + this.orderId + '_cart';
|
||||
var cartStr = localStorage.getItem(cartKey);
|
||||
return cartStr ? JSON.parse(cartStr) : {};
|
||||
},
|
||||
|
||||
saveCart: function(cart) {
|
||||
if (!this.orderId) return;
|
||||
var cartKey = 'eskaera_' + this.orderId + '_cart';
|
||||
localStorage.setItem(cartKey, JSON.stringify(cart));
|
||||
|
||||
// Re-render checkout summary without reloading
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
// Use the global function from checkout_labels.js
|
||||
if (typeof window.renderCheckoutSummary === 'function') {
|
||||
window.renderCheckoutSummary();
|
||||
}
|
||||
}, 50);
|
||||
},
|
||||
|
||||
renderCheckoutSummary: function() {
|
||||
// Stub - now handled by global window.renderCheckoutSummary
|
||||
},
|
||||
|
||||
addDeliveryProduct: function() {
|
||||
if (!this.deliveryProductId) {
|
||||
console.warn('[HomeDelivery] Delivery product ID not found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[HomeDelivery] Adding delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId);
|
||||
var cart = this.getCart();
|
||||
console.log('[HomeDelivery] Current cart before adding:', cart);
|
||||
|
||||
cart[this.deliveryProductId] = {
|
||||
id: this.deliveryProductId,
|
||||
name: this.deliveryProductName,
|
||||
price: this.deliveryProductPrice,
|
||||
qty: 1
|
||||
};
|
||||
console.log('[HomeDelivery] Cart after adding delivery:', cart);
|
||||
this.saveCart(cart);
|
||||
console.log('[HomeDelivery] Delivery product added to localStorage');
|
||||
},
|
||||
|
||||
removeDeliveryProduct: function() {
|
||||
if (!this.deliveryProductId) {
|
||||
console.warn('[HomeDelivery] Delivery product ID not found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[HomeDelivery] Removing delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId);
|
||||
var cart = this.getCart();
|
||||
console.log('[HomeDelivery] Current cart before removing:', cart);
|
||||
|
||||
if (cart[this.deliveryProductId]) {
|
||||
delete cart[this.deliveryProductId];
|
||||
console.log('[HomeDelivery] Cart after removing delivery:', cart);
|
||||
}
|
||||
this.saveCart(cart);
|
||||
console.log('[HomeDelivery] Delivery product removed from localStorage');
|
||||
},
|
||||
|
||||
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';
|
||||
}
|
||||
},
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
HomeDeliveryManager.init();
|
||||
});
|
||||
} else {
|
||||
HomeDeliveryManager.init();
|
||||
}
|
||||
|
||||
// Export to global scope
|
||||
window.HomeDeliveryManager = HomeDeliveryManager;
|
||||
})();
|
||||
67
website_sale_aplicoop/static/src/js/i18n_helpers.js
Normal file
67
website_sale_aplicoop/static/src/js/i18n_helpers.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
// Keep legacy functions as wrappers for backwards compatibility
|
||||
|
||||
/**
|
||||
* DEPRECATED - Use i18nManager.getAll() or i18nManager.get(key) instead
|
||||
*/
|
||||
window.getCheckoutLabels = function(key) {
|
||||
if (window.i18nManager && window.i18nManager.initialized) {
|
||||
if (key) {
|
||||
return window.i18nManager.get(key);
|
||||
}
|
||||
return window.i18nManager.getAll();
|
||||
}
|
||||
// Fallback if i18nManager not yet initialized
|
||||
return key ? key : {};
|
||||
};
|
||||
|
||||
/**
|
||||
* DEPRECATED - Use i18nManager.getAll() instead
|
||||
*/
|
||||
window.getSearchLabels = function() {
|
||||
if (window.i18nManager && window.i18nManager.initialized) {
|
||||
return {
|
||||
'searchPlaceholder': window.i18nManager.get('search_products'),
|
||||
'noResults': window.i18nManager.get('no_results')
|
||||
};
|
||||
}
|
||||
return {
|
||||
'searchPlaceholder': 'Search products...',
|
||||
'noResults': 'No products found'
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* DEPRECATED - Use i18nManager.formatCurrency(amount) instead
|
||||
*/
|
||||
window.formatCurrency = function(amount) {
|
||||
if (window.i18nManager) {
|
||||
return window.i18nManager.formatCurrency(amount);
|
||||
}
|
||||
// Fallback
|
||||
return '€' + parseFloat(amount).toFixed(2);
|
||||
};
|
||||
|
||||
console.log('[i18n_helpers] DEPRECATED - Use i18n_manager.js instead');
|
||||
|
||||
})();
|
||||
153
website_sale_aplicoop/static/src/js/i18n_manager.js
Normal file
153
website_sale_aplicoop/static/src/js/i18n_manager.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
window.i18nManager = {
|
||||
labels: null,
|
||||
initialized: false,
|
||||
initPromise: null,
|
||||
|
||||
/**
|
||||
* Initialize by fetching translations from server
|
||||
* Returns a Promise that resolves when translations are loaded
|
||||
*/
|
||||
init: function() {
|
||||
if (this.initialized) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.initPromise) {
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Fetch translations from server
|
||||
this.initPromise = fetch('/eskaera/i18n', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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 {};
|
||||
});
|
||||
|
||||
return this.initPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a specific translation label
|
||||
* Returns the translated string or the key if not found
|
||||
*/
|
||||
get: function(key) {
|
||||
if (!this.initialized) {
|
||||
console.warn('[i18nManager] Not yet initialized. Call init() first.');
|
||||
return key;
|
||||
}
|
||||
return this.labels[key] || key;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all translation labels as object
|
||||
*/
|
||||
getAll: function() {
|
||||
if (!this.initialized) {
|
||||
console.warn('[i18nManager] Not yet initialized. Call init() first.');
|
||||
return {};
|
||||
}
|
||||
return this.labels;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a specific label exists
|
||||
*/
|
||||
has: function(key) {
|
||||
if (!this.initialized) return false;
|
||||
return key in this.labels;
|
||||
},
|
||||
|
||||
/**
|
||||
* Format currency to Euro format
|
||||
*/
|
||||
formatCurrency: function(amount) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
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);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// DOM already loaded
|
||||
setTimeout(function() {
|
||||
i18nManager.init().catch(function(err) {
|
||||
console.error('[i18nManager] Auto-init failed:', err);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
})();
|
||||
494
website_sale_aplicoop/static/src/js/realtime_search.js
Normal file
494
website_sale_aplicoop/static/src/js/realtime_search.js
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
/*
|
||||
* Copyright 2025 Criptomart
|
||||
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.realtimeSearch = {
|
||||
searchInput: null,
|
||||
categorySelect: null,
|
||||
allProducts: [],
|
||||
debounceTimer: null,
|
||||
debounceDelay: 0,
|
||||
categoryHierarchy: {}, // Maps parent category IDs to their children
|
||||
selectedTags: new Set(), // Set of selected tag IDs (for OR logic filtering)
|
||||
availableTags: {}, // Maps tag ID to {id, name, count}
|
||||
|
||||
init: function() {
|
||||
console.log('[realtimeSearch] Initializing...');
|
||||
|
||||
// searchInput y categorySelect ya fueron asignados por tryInit()
|
||||
console.log('[realtimeSearch] Search input:', this.searchInput);
|
||||
console.log('[realtimeSearch] Category select:', this.categorySelect);
|
||||
|
||||
if (!this.searchInput) {
|
||||
console.error('[realtimeSearch] ERROR: Search input not found!');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.categorySelect) {
|
||||
console.error('[realtimeSearch] ERROR: Category select not found!');
|
||||
return false;
|
||||
}
|
||||
|
||||
this._buildCategoryHierarchyFromDOM();
|
||||
this._storeAllProducts();
|
||||
console.log('[realtimeSearch] After _storeAllProducts(), calling _attachEventListeners()...');
|
||||
this._attachEventListeners();
|
||||
console.log('[realtimeSearch] ✓ Initialized successfully');
|
||||
return true;
|
||||
},
|
||||
|
||||
_buildCategoryHierarchyFromDOM: function() {
|
||||
/**
|
||||
* Construye un mapa de jerarquía de categorías desde las opciones del select.
|
||||
* Ahora todas las opciones son planas pero con indentación visual (↳ arrows).
|
||||
*
|
||||
* La profundidad se determina contando el número de arrows (↳).
|
||||
* Estructura: categoryHierarchy[parentCategoryId] = [childCategoryId1, childCategoryId2, ...]
|
||||
*/
|
||||
var self = this;
|
||||
var allOptions = this.categorySelect.querySelectorAll('option[value]');
|
||||
var optionStack = []; // Stack para mantener los padres en cada nivel
|
||||
|
||||
allOptions.forEach(function(option) {
|
||||
var categoryId = option.getAttribute('value');
|
||||
var text = option.textContent;
|
||||
|
||||
// Contar arrows para determinar profundidad
|
||||
var arrowCount = (text.match(/↳/g) || []).length;
|
||||
var depth = arrowCount; // Depth: 0 for roots, 1 for children, 2 for grandchildren, etc.
|
||||
|
||||
// Ajustar el stack al nivel actual
|
||||
// Si la profundidad es menor o igual, sacamos elementos del stack
|
||||
while (optionStack.length > depth) {
|
||||
optionStack.pop();
|
||||
}
|
||||
|
||||
// Si hay un padre en el stack (profundidad > 0), agregar como hijo
|
||||
if (depth > 0 && optionStack.length > 0) {
|
||||
var parentId = optionStack[optionStack.length - 1];
|
||||
if (!self.categoryHierarchy[parentId]) {
|
||||
self.categoryHierarchy[parentId] = [];
|
||||
}
|
||||
if (!self.categoryHierarchy[parentId].includes(categoryId)) {
|
||||
self.categoryHierarchy[parentId].push(categoryId);
|
||||
}
|
||||
}
|
||||
|
||||
// Agregar este ID al stack como posible padre para los siguientes
|
||||
// Adjust position in stack based on depth
|
||||
if (optionStack.length > depth) {
|
||||
optionStack[depth] = categoryId;
|
||||
} else {
|
||||
optionStack.push(categoryId);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[realtimeSearch] Complete category hierarchy built:', self.categoryHierarchy);
|
||||
},
|
||||
|
||||
_storeAllProducts: function() {
|
||||
var productCards = document.querySelectorAll('.product-card');
|
||||
console.log('[realtimeSearch] Found ' + productCards.length + ' product cards');
|
||||
|
||||
var self = this;
|
||||
this.allProducts = [];
|
||||
|
||||
productCards.forEach(function(card, index) {
|
||||
var name = card.getAttribute('data-product-name') || '';
|
||||
var categoryId = card.getAttribute('data-category-id') || '';
|
||||
var tagIdsStr = card.getAttribute('data-product-tags') || '';
|
||||
|
||||
// Parse tag IDs from comma-separated string
|
||||
var tagIds = [];
|
||||
if (tagIdsStr) {
|
||||
tagIds = tagIdsStr.split(',').map(function(id) {
|
||||
return parseInt(id.trim(), 10);
|
||||
}).filter(function(id) {
|
||||
return !isNaN(id);
|
||||
});
|
||||
}
|
||||
|
||||
self.allProducts.push({
|
||||
element: card,
|
||||
name: name.toLowerCase(),
|
||||
category: categoryId.toString(),
|
||||
originalCategory: categoryId,
|
||||
tags: tagIds // Array of tag IDs for this product
|
||||
});
|
||||
});
|
||||
|
||||
console.log('[realtimeSearch] Total products stored: ' + this.allProducts.length);
|
||||
},
|
||||
|
||||
_attachEventListeners: function() {
|
||||
var self = this;
|
||||
|
||||
// Initialize available tags from DOM
|
||||
self._initializeAvailableTags();
|
||||
|
||||
// Store original colors for each tag badge
|
||||
self.originalTagColors = {}; // Maps tag ID to original color
|
||||
|
||||
// Store last values at instance level so polling can access them
|
||||
self.lastSearchValue = '';
|
||||
self.lastCategoryValue = '';
|
||||
|
||||
// Prevent form submission completely
|
||||
var form = self.searchInput.closest('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('[realtimeSearch] Form submission prevented and stopped');
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent Enter key from submitting
|
||||
self.searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('[realtimeSearch] Enter key prevented on search input');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Search input: listen to 'input' for real-time filtering
|
||||
self.searchInput.addEventListener('input', function(e) {
|
||||
try {
|
||||
console.log('[realtimeSearch] INPUT event - value: "' + e.target.value + '"');
|
||||
self._filterProducts();
|
||||
} catch (error) {
|
||||
console.error('[realtimeSearch] Error in input listener:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Also keep 'keyup' for extra compatibility
|
||||
self.searchInput.addEventListener('keyup', function(e) {
|
||||
try {
|
||||
console.log('[realtimeSearch] KEYUP event - value: "' + e.target.value + '"');
|
||||
self._filterProducts();
|
||||
} catch (error) {
|
||||
console.error('[realtimeSearch] Error in keyup listener:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Category select
|
||||
self.categorySelect.addEventListener('change', function(e) {
|
||||
try {
|
||||
console.log('[realtimeSearch] CHANGE event - selected: "' + e.target.value + '"');
|
||||
self._filterProducts();
|
||||
} catch (error) {
|
||||
console.error('[realtimeSearch] Error in category change listener:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Tag filter badges: click to toggle selection (independent state)
|
||||
var tagBadges = document.querySelectorAll('[data-toggle="tag-filter"]');
|
||||
console.log('[realtimeSearch] Found ' + tagBadges.length + ' tag filter badges');
|
||||
|
||||
// Get theme colors from CSS variables
|
||||
var rootStyles = getComputedStyle(document.documentElement);
|
||||
var primaryColor = rootStyles.getPropertyValue('--bs-primary').trim() ||
|
||||
rootStyles.getPropertyValue('--primary').trim() ||
|
||||
'#0d6efd';
|
||||
var secondaryColor = rootStyles.getPropertyValue('--bs-secondary').trim() ||
|
||||
rootStyles.getPropertyValue('--secondary').trim() ||
|
||||
'#6c757d';
|
||||
|
||||
console.log('[realtimeSearch] Theme colors - Primary:', primaryColor, 'Secondary:', secondaryColor);
|
||||
|
||||
// Store original colors for each badge BEFORE adding event listeners
|
||||
tagBadges.forEach(function(badge) {
|
||||
var tagId = parseInt(badge.getAttribute('data-tag-id'), 10);
|
||||
var tagColor = badge.getAttribute('data-tag-color');
|
||||
|
||||
// Store the original color (either from data-tag-color or use secondary for tags without color)
|
||||
if (tagColor) {
|
||||
self.originalTagColors[tagId] = tagColor;
|
||||
console.log('[realtimeSearch] Stored original color for tag ' + tagId + ': ' + tagColor);
|
||||
} else {
|
||||
self.originalTagColors[tagId] = 'var(--bs-secondary, ' + secondaryColor + ')';
|
||||
console.log('[realtimeSearch] Tag ' + tagId + ' has no color, using secondary');
|
||||
}
|
||||
});
|
||||
|
||||
tagBadges.forEach(function(badge) {
|
||||
badge.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
var tagId = parseInt(badge.getAttribute('data-tag-id'), 10);
|
||||
var originalColor = self.originalTagColors[tagId];
|
||||
console.log('[realtimeSearch] Tag badge clicked: ' + tagId + ' (original color: ' + originalColor + ')');
|
||||
|
||||
// Toggle tag selection
|
||||
if (self.selectedTags.has(tagId)) {
|
||||
// Deselect
|
||||
self.selectedTags.delete(tagId);
|
||||
console.log('[realtimeSearch] Tag ' + tagId + ' deselected');
|
||||
} else {
|
||||
// Select
|
||||
self.selectedTags.add(tagId);
|
||||
console.log('[realtimeSearch] Tag ' + tagId + ' selected');
|
||||
}
|
||||
|
||||
// Update colors for ALL badges based on selection state
|
||||
tagBadges.forEach(function(badge) {
|
||||
var id = parseInt(badge.getAttribute('data-tag-id'), 10);
|
||||
|
||||
if (self.selectedTags.size === 0) {
|
||||
// No tags selected: restore all to original colors
|
||||
var originalColor = self.originalTagColors[id];
|
||||
badge.style.setProperty('background-color', originalColor, 'important');
|
||||
badge.style.setProperty('border-color', originalColor, 'important');
|
||||
badge.style.setProperty('color', '#ffffff', 'important');
|
||||
console.log('[realtimeSearch] Badge ' + id + ' reset to original color (no selection)');
|
||||
} else if (self.selectedTags.has(id)) {
|
||||
// Selected: primary color
|
||||
badge.style.setProperty('background-color', 'var(--bs-primary, ' + primaryColor + ')', 'important');
|
||||
badge.style.setProperty('border-color', 'var(--bs-primary, ' + primaryColor + ')', 'important');
|
||||
badge.style.setProperty('color', '#ffffff', 'important');
|
||||
console.log('[realtimeSearch] Badge ' + id + ' set to primary (selected)');
|
||||
} else {
|
||||
// Not selected but others are: secondary color
|
||||
badge.style.setProperty('background-color', 'var(--bs-secondary, ' + secondaryColor + ')', 'important');
|
||||
badge.style.setProperty('border-color', 'var(--bs-secondary, ' + secondaryColor + ')', 'important');
|
||||
badge.style.setProperty('color', '#ffffff', 'important');
|
||||
console.log('[realtimeSearch] Badge ' + id + ' set to secondary (not selected)');
|
||||
}
|
||||
});
|
||||
|
||||
// Filter products (independent of search/category state)
|
||||
self._filterProducts();
|
||||
});
|
||||
});
|
||||
|
||||
// POLLING FALLBACK: Since Odoo components may intercept events,
|
||||
// use polling to detect value changes
|
||||
console.log('[realtimeSearch] 🚀 POLLING INITIALIZATION STARTING');
|
||||
console.log('[realtimeSearch] Search input element:', self.searchInput);
|
||||
console.log('[realtimeSearch] Category select element:', self.categorySelect);
|
||||
|
||||
var pollingCounter = 0;
|
||||
var pollInterval = setInterval(function() {
|
||||
try {
|
||||
pollingCounter++;
|
||||
|
||||
// Try multiple ways to get the search value
|
||||
var currentSearchValue = self.searchInput.value || '';
|
||||
var currentSearchAttr = self.searchInput.getAttribute('value') || '';
|
||||
var currentSearchDataValue = self.searchInput.getAttribute('data-value') || '';
|
||||
var currentSearchInnerText = self.searchInput.innerText || '';
|
||||
|
||||
var currentCategoryValue = self.categorySelect ? (self.categorySelect.value || '') : '';
|
||||
|
||||
// FIRST POLL: Detailed debug
|
||||
if (pollingCounter === 1) {
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('[realtimeSearch] 🔍 FIRST POLLING DEBUG (POLL #1)');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
console.log('Search input .value:', JSON.stringify(currentSearchValue));
|
||||
console.log('Search input getAttribute("value"):', JSON.stringify(currentSearchAttr));
|
||||
console.log('Search input getAttribute("data-value"):', JSON.stringify(currentSearchDataValue));
|
||||
console.log('Search input innerText:', JSON.stringify(currentSearchInnerText));
|
||||
console.log('Category select .value:', JSON.stringify(currentCategoryValue));
|
||||
console.log('Last stored values - search:"' + self.lastSearchValue + '" category:"' + self.lastCategoryValue + '"');
|
||||
console.log('═══════════════════════════════════════════');
|
||||
}
|
||||
|
||||
// Log every 20 polls (reduce spam)
|
||||
if (pollingCounter % 20 === 0) {
|
||||
console.log('[realtimeSearch] POLLING #' + pollingCounter + ': search="' + currentSearchValue + '" category="' + currentCategoryValue + '"');
|
||||
}
|
||||
|
||||
// Check for ANY change in either field
|
||||
if (currentSearchValue !== self.lastSearchValue || currentCategoryValue !== self.lastCategoryValue) {
|
||||
console.log('[realtimeSearch] ⚡ CHANGE DETECTED: search="' + currentSearchValue + '" (was:"' + self.lastSearchValue + '") | category="' + currentCategoryValue + '" (was:"' + self.lastCategoryValue + '")');
|
||||
self.lastSearchValue = currentSearchValue;
|
||||
self.lastCategoryValue = currentCategoryValue;
|
||||
self._filterProducts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[realtimeSearch] ❌ Error in polling:', error.message);
|
||||
}
|
||||
}, 300); // Check every 300ms
|
||||
|
||||
console.log('[realtimeSearch] ✅ Polling interval started with ID:', pollInterval);
|
||||
|
||||
console.log('[realtimeSearch] Event listeners attached with polling fallback');
|
||||
},
|
||||
|
||||
_initializeAvailableTags: function() {
|
||||
/**
|
||||
* Initialize availableTags map from the DOM tag filter badges.
|
||||
* Format: availableTags[tagId] = {id, name, count}
|
||||
*/
|
||||
var self = this;
|
||||
var tagBadges = document.querySelectorAll('[data-toggle="tag-filter"]');
|
||||
|
||||
tagBadges.forEach(function(badge) {
|
||||
var tagId = parseInt(badge.getAttribute('data-tag-id'), 10);
|
||||
var tagName = badge.getAttribute('data-tag-name') || '';
|
||||
var countSpan = badge.querySelector('.tag-count');
|
||||
var count = countSpan ? parseInt(countSpan.textContent, 10) : 0;
|
||||
|
||||
self.availableTags[tagId] = {
|
||||
id: tagId,
|
||||
name: tagName,
|
||||
count: count
|
||||
};
|
||||
});
|
||||
|
||||
console.log('[realtimeSearch] Initialized ' + Object.keys(self.availableTags).length + ' available tags');
|
||||
},
|
||||
|
||||
_filterProducts: function() {
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
var searchQuery = (self.searchInput.value || '').toLowerCase().trim();
|
||||
var selectedCategoryId = (self.categorySelect.value || '').toString();
|
||||
|
||||
console.log('[realtimeSearch] Filtering: search=' + searchQuery + ' category=' + selectedCategoryId + ' tags=' + Array.from(self.selectedTags).join(','));
|
||||
|
||||
// Build a set of allowed category IDs (selected category + ALL descendants recursively)
|
||||
var allowedCategories = {};
|
||||
|
||||
if (selectedCategoryId) {
|
||||
allowedCategories[selectedCategoryId] = true;
|
||||
|
||||
// Recursive function to get all descendants
|
||||
var getAllDescendants = function(parentId) {
|
||||
var descendants = [];
|
||||
if (self.categoryHierarchy[parentId]) {
|
||||
self.categoryHierarchy[parentId].forEach(function(childId) {
|
||||
descendants.push(childId);
|
||||
allowedCategories[childId] = true;
|
||||
// Recursivamente obtener descendientes del hijo
|
||||
var grandDescendants = getAllDescendants(childId);
|
||||
descendants = descendants.concat(grandDescendants);
|
||||
});
|
||||
}
|
||||
return descendants;
|
||||
};
|
||||
|
||||
var allDescendants = getAllDescendants(selectedCategoryId);
|
||||
console.log('[realtimeSearch] Selected category ' + selectedCategoryId + ' has ' + allDescendants.length + ' total descendants');
|
||||
console.log('[realtimeSearch] Allowed categories:', Object.keys(allowedCategories));
|
||||
}
|
||||
|
||||
var visibleCount = 0;
|
||||
var hiddenCount = 0;
|
||||
|
||||
// Track tag counts for dynamic badge updates
|
||||
var tagCounts = {};
|
||||
for (var tagId in self.availableTags) {
|
||||
tagCounts[tagId] = 0;
|
||||
}
|
||||
|
||||
self.allProducts.forEach(function(product) {
|
||||
var nameMatches = !searchQuery || product.name.indexOf(searchQuery) !== -1;
|
||||
var categoryMatches = !selectedCategoryId || allowedCategories[product.category];
|
||||
|
||||
// Tag filtering: if tags are selected, product must have AT LEAST ONE selected tag (OR logic)
|
||||
var tagMatches = true;
|
||||
if (self.selectedTags.size > 0) {
|
||||
tagMatches = product.tags.some(function(productTagId) {
|
||||
return self.selectedTags.has(productTagId);
|
||||
});
|
||||
}
|
||||
|
||||
var shouldShow = nameMatches && categoryMatches && tagMatches;
|
||||
|
||||
if (shouldShow) {
|
||||
product.element.classList.remove('hidden-product');
|
||||
visibleCount++;
|
||||
|
||||
// Count this product's tags toward the dynamic counters
|
||||
product.tags.forEach(function(tagId) {
|
||||
if (tagCounts.hasOwnProperty(tagId)) {
|
||||
tagCounts[tagId]++;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
product.element.classList.add('hidden-product');
|
||||
hiddenCount++;
|
||||
}
|
||||
});
|
||||
|
||||
// Update badge counts dynamically
|
||||
for (var tagId in tagCounts) {
|
||||
var badge = document.querySelector('[data-tag-id="' + tagId + '"]');
|
||||
if (badge) {
|
||||
var countSpan = badge.querySelector('.tag-count');
|
||||
if (countSpan) {
|
||||
countSpan.textContent = tagCounts[tagId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[realtimeSearch] Filter result: visible=' + visibleCount + ' hidden=' + hiddenCount);
|
||||
} catch (error) {
|
||||
console.error('[realtimeSearch] ERROR in _filterProducts():', error.message);
|
||||
console.error('[realtimeSearch] Stack:', error.stack);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready
|
||||
console.log('[realtimeSearch] Script loaded, DOM state: ' + document.readyState);
|
||||
|
||||
function tryInit() {
|
||||
try {
|
||||
console.log('[realtimeSearch] Attempting initialization...');
|
||||
|
||||
// Query product cards
|
||||
var productCards = document.querySelectorAll('.product-card');
|
||||
console.log('[realtimeSearch] Found ' + productCards.length + ' product cards');
|
||||
|
||||
// Use the NEW pure HTML input with ID (not transformed by Odoo)
|
||||
var searchInput = document.getElementById('realtime-search-input');
|
||||
console.log('[realtimeSearch] Search input found:', !!searchInput);
|
||||
if (searchInput) {
|
||||
console.log('[realtimeSearch] Search input class:', searchInput.className);
|
||||
console.log('[realtimeSearch] Search input type:', searchInput.type);
|
||||
}
|
||||
|
||||
// Category select with ID (not transformed by Odoo)
|
||||
var categorySelect = document.getElementById('realtime-category-select');
|
||||
console.log('[realtimeSearch] Category select found:', !!categorySelect);
|
||||
|
||||
if (productCards.length > 0 && searchInput) {
|
||||
console.log('[realtimeSearch] ✓ All elements found! Initializing...');
|
||||
// Assign elements to window.realtimeSearch BEFORE calling init()
|
||||
window.realtimeSearch.searchInput = searchInput;
|
||||
window.realtimeSearch.categorySelect = categorySelect;
|
||||
window.realtimeSearch.init();
|
||||
console.log('[realtimeSearch] ✓ Initialization complete!');
|
||||
} else {
|
||||
console.log('[realtimeSearch] Waiting for elements... (products:' + productCards.length + ', search:' + !!searchInput + ')');
|
||||
if (productCards.length === 0) {
|
||||
console.log('[realtimeSearch] No product cards found. Current HTML body length:', document.body.innerHTML.length);
|
||||
}
|
||||
setTimeout(tryInit, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[realtimeSearch] ERROR in tryInit():', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
console.log('[realtimeSearch] Adding DOMContentLoaded listener');
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('[realtimeSearch] DOMContentLoaded fired');
|
||||
tryInit();
|
||||
});
|
||||
} else {
|
||||
console.log('[realtimeSearch] DOM already loaded, initializing with delay');
|
||||
setTimeout(tryInit, 500);
|
||||
}
|
||||
})();
|
||||
1788
website_sale_aplicoop/static/src/js/website_sale.js
Normal file
1788
website_sale_aplicoop/static/src/js/website_sale.js
Normal file
File diff suppressed because it is too large
Load diff
262
website_sale_aplicoop/static/tests/README.md
Normal file
262
website_sale_aplicoop/static/tests/README.md
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
# JavaScript Tests for website_sale_aplicoop
|
||||
|
||||
This directory contains QUnit tests for the JavaScript functionality of the website_sale_aplicoop module.
|
||||
|
||||
## Test Files
|
||||
|
||||
### 1. test_cart_functions.js
|
||||
Tests for core cart functionality:
|
||||
- Cart initialization
|
||||
- Adding items to cart
|
||||
- Removing items from cart
|
||||
- Updating quantities
|
||||
- Calculating totals
|
||||
- localStorage persistence
|
||||
- Decimal quantity handling
|
||||
- Zero quantity handling
|
||||
|
||||
### 2. test_tooltips_labels.js
|
||||
Tests for tooltip and label functionality:
|
||||
- Tooltip initialization from labels
|
||||
- Label loading and structure
|
||||
- Missing label handling
|
||||
- Label reinitialization
|
||||
- JSON serialization of labels
|
||||
- Empty labels handling
|
||||
|
||||
### 3. test_realtime_search.js
|
||||
Tests for real-time product search:
|
||||
- Search input functionality
|
||||
- Category filtering
|
||||
- Combined search and category filters
|
||||
- Case-insensitive search
|
||||
- Partial matching
|
||||
- Whitespace trimming
|
||||
- Product visibility toggling
|
||||
- Result counting
|
||||
|
||||
### 4. test_suite.js
|
||||
Main test suite that imports all test modules.
|
||||
|
||||
## Running the Tests
|
||||
|
||||
### Method 1: Via Odoo Test Runner (Recommended)
|
||||
|
||||
1. **Access the test interface:**
|
||||
```
|
||||
http://localhost:8069/web/tests?mod=website_sale_aplicoop
|
||||
```
|
||||
|
||||
2. **Run specific test modules:**
|
||||
```
|
||||
http://localhost:8069/web/tests?mod=website_sale_aplicoop.test_cart_functions
|
||||
http://localhost:8069/web/tests?mod=website_sale_aplicoop.test_tooltips_labels
|
||||
http://localhost:8069/web/tests?mod=website_sale_aplicoop.test_realtime_search
|
||||
```
|
||||
|
||||
3. **View results:**
|
||||
- Tests run in the browser
|
||||
- Results displayed in QUnit interface
|
||||
- Green = Pass, Red = Fail
|
||||
- Click failed tests to see details
|
||||
|
||||
### Method 2: Via Command Line
|
||||
|
||||
Run Odoo with test mode:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
docker-compose exec odoo odoo -d odoo --test-enable --stop-after-init
|
||||
|
||||
# Run with specific test tags
|
||||
docker-compose exec odoo odoo -d odoo --test-enable --test-tags=website_sale_aplicoop --stop-after-init
|
||||
|
||||
# Run in verbose mode for more details
|
||||
docker-compose exec odoo odoo -d odoo --test-enable --log-level=test --stop-after-init
|
||||
```
|
||||
|
||||
### Method 3: Via Browser Console
|
||||
|
||||
1. Open the application page in browser
|
||||
2. Open browser console (F12)
|
||||
3. Run:
|
||||
```javascript
|
||||
QUnit.start();
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Cart Functions (11 tests)
|
||||
- ✅ Object initialization
|
||||
- ✅ Empty cart verification
|
||||
- ✅ Add item to cart
|
||||
- ✅ Remove item from cart
|
||||
- ✅ Update quantity
|
||||
- ✅ Calculate total
|
||||
- ✅ localStorage persistence
|
||||
- ✅ Decimal quantities
|
||||
- ✅ Zero quantity handling
|
||||
- ✅ Same price products
|
||||
- ✅ Label initialization
|
||||
|
||||
### Tooltips & Labels (10 tests)
|
||||
- ✅ Tooltip initialization
|
||||
- ✅ Missing label handling
|
||||
- ✅ Label object structure
|
||||
- ✅ Label data types
|
||||
- ✅ Global label usage
|
||||
- ✅ Reinitialization
|
||||
- ✅ Elements without tooltips
|
||||
- ✅ querySelectorAll functionality
|
||||
- ✅ JSON serialization
|
||||
- ✅ Empty labels handling
|
||||
|
||||
### Realtime Search (13 tests)
|
||||
- ✅ Element existence
|
||||
- ✅ Search by name
|
||||
- ✅ Case insensitive search
|
||||
- ✅ Empty search shows all
|
||||
- ✅ Category filtering
|
||||
- ✅ Combined filters
|
||||
- ✅ Non-existent product
|
||||
- ✅ Partial matching
|
||||
- ✅ Whitespace trimming
|
||||
- ✅ CSS class toggling
|
||||
- ✅ Visibility restoration
|
||||
- ✅ Result counting
|
||||
|
||||
**Total: 34 tests**
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
1. Create a new test file in `/static/tests/`:
|
||||
```javascript
|
||||
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
|
||||
},
|
||||
afterEach: function() {
|
||||
// Cleanup code
|
||||
}
|
||||
}, function() {
|
||||
QUnit.test('test description', function(assert) {
|
||||
assert.expect(1);
|
||||
assert.ok(true, 'test passes');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
2. Add to `test_suite.js`:
|
||||
```javascript
|
||||
require('website_sale_aplicoop.test_my_feature');
|
||||
```
|
||||
|
||||
3. Add to `__manifest__.py` assets:
|
||||
```python
|
||||
'web.assets_tests': [
|
||||
# ... existing files ...
|
||||
'website_sale_aplicoop/static/tests/test_my_feature.js',
|
||||
],
|
||||
```
|
||||
|
||||
4. Reload module and run tests
|
||||
|
||||
## QUnit Assertions Reference
|
||||
|
||||
Common assertions used in tests:
|
||||
|
||||
- `assert.ok(value, message)` - Verify truthy value
|
||||
- `assert.equal(actual, expected, message)` - Loose equality (==)
|
||||
- `assert.strictEqual(actual, expected, message)` - Strict equality (===)
|
||||
- `assert.deepEqual(actual, expected, message)` - Deep object comparison
|
||||
- `assert.notOk(value, message)` - Verify falsy value
|
||||
- `assert.notEqual(actual, expected, message)` - Verify not equal
|
||||
- `assert.expect(count)` - Set expected assertion count
|
||||
|
||||
## Debugging Tests
|
||||
|
||||
### View Test Output
|
||||
- Open browser console (F12)
|
||||
- Check "Console" tab for test logs
|
||||
- Check "Network" tab for failed requests
|
||||
|
||||
### Debug Individual Test
|
||||
```javascript
|
||||
QUnit.test('test name', function(assert) {
|
||||
debugger; // Browser will pause here
|
||||
// ... test code ...
|
||||
});
|
||||
```
|
||||
|
||||
### Run Single Test
|
||||
```javascript
|
||||
QUnit.only('test name', function(assert) {
|
||||
// Only this test will run
|
||||
});
|
||||
```
|
||||
|
||||
### Skip Test
|
||||
```javascript
|
||||
QUnit.skip('test name', function(assert) {
|
||||
// This test will be skipped
|
||||
});
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
Tests can be integrated into CI/CD pipelines:
|
||||
|
||||
```bash
|
||||
# In CI script
|
||||
docker-compose up -d
|
||||
docker-compose exec -T odoo odoo -d odoo --test-enable --test-tags=website_sale_aplicoop --stop-after-init
|
||||
exit_code=$?
|
||||
docker-compose down
|
||||
exit $exit_code
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tests not loading
|
||||
- Verify module is installed and updated
|
||||
- Check browser console for JavaScript errors
|
||||
- Verify assets are properly declared in __manifest__.py
|
||||
- Clear browser cache and restart Odoo
|
||||
|
||||
### Tests failing unexpectedly
|
||||
- Check if labels are loaded (`window.groupOrderShop.labels`)
|
||||
- Verify DOM elements exist before testing
|
||||
- Check for timing issues (use beforeEach/afterEach)
|
||||
- Verify localStorage is not blocked by browser
|
||||
|
||||
### Assets not found
|
||||
- Update module: `docker-compose exec odoo odoo -d odoo -u website_sale_aplicoop`
|
||||
- Clear assets cache: `docker-compose exec odoo rm -rf /var/lib/odoo/filestore/odoo/web/static/lib/minified_assets/`
|
||||
- Restart Odoo
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use beforeEach/afterEach**: Clean up DOM and global state
|
||||
2. **Expect assertions**: Always use `assert.expect(n)` to verify all assertions run
|
||||
3. **Test isolation**: Each test should be independent
|
||||
4. **Descriptive names**: Use clear, descriptive test names
|
||||
5. **One concept per test**: Test one thing at a time
|
||||
6. **Mock external dependencies**: Don't rely on real API calls
|
||||
7. **Test edge cases**: Empty strings, null values, extreme numbers
|
||||
|
||||
## Resources
|
||||
|
||||
- [QUnit Documentation](https://qunitjs.com/)
|
||||
- [Odoo JavaScript Testing](https://www.odoo.com/documentation/18.0/developer/reference/frontend/javascript_testing.html)
|
||||
- [MDN Web Docs - Testing](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing)
|
||||
|
||||
---
|
||||
|
||||
**Maintainer**: Criptomart
|
||||
**License**: AGPL-3.0
|
||||
**Last Updated**: February 3, 2026
|
||||
224
website_sale_aplicoop/static/tests/test_cart_functions.js
Normal file
224
website_sale_aplicoop/static/tests/test_cart_functions.js
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
/**
|
||||
* QUnit Tests for Cart Functions
|
||||
* Tests core cart functionality (add, remove, update, calculate)
|
||||
*/
|
||||
|
||||
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();
|
||||
},
|
||||
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.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 {};
|
||||
});
|
||||
241
website_sale_aplicoop/static/tests/test_realtime_search.js
Normal file
241
website_sale_aplicoop/static/tests/test_realtime_search.js
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
/**
|
||||
* QUnit Tests for Realtime Search Functionality
|
||||
* Tests product filtering and search behavior
|
||||
*/
|
||||
|
||||
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 };
|
||||
}
|
||||
};
|
||||
},
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
return {};
|
||||
});
|
||||
10
website_sale_aplicoop/static/tests/test_suite.js
Normal file
10
website_sale_aplicoop/static/tests/test_suite.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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');
|
||||
|
||||
// Test suite is automatically registered by importing modules
|
||||
});
|
||||
187
website_sale_aplicoop/static/tests/test_tooltips_labels.js
Normal file
187
website_sale_aplicoop/static/tests/test_tooltips_labels.js
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
/**
|
||||
* QUnit Tests for Tooltip and Label Functions
|
||||
* Tests tooltip initialization and label loading
|
||||
*/
|
||||
|
||||
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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
afterEach: function() {
|
||||
// Cleanup
|
||||
this.$fixture.empty();
|
||||
delete window.groupOrderShop;
|
||||
}
|
||||
}, function() {
|
||||
|
||||
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 {
|
||||
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