Compare commits
2 Commits
b61ae3cb32
...
8f7f9445b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f7f9445b7 | |||
| ad39ccb561 |
@@ -16,7 +16,8 @@ const tenant: Tenant = {
|
|||||||
secondary_color: '#A0A0A0',
|
secondary_color: '#A0A0A0',
|
||||||
danger_color: '#FF8888',
|
danger_color: '#FF8888',
|
||||||
success_color: '#198754',
|
success_color: '#198754',
|
||||||
header_footer_bg_color: '#313131',
|
header_bg_color: '#313131',
|
||||||
|
footer_bg_color: '#313131',
|
||||||
header_logo: 'https://example.com/header.png',
|
header_logo: 'https://example.com/header.png',
|
||||||
footer_logo: 'https://example.com/footer.png'
|
footer_logo: 'https://example.com/footer.png'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ const tenant: Tenant = {
|
|||||||
secondary_color: '#A0A0A0',
|
secondary_color: '#A0A0A0',
|
||||||
danger_color: '#FF8888',
|
danger_color: '#FF8888',
|
||||||
success_color: '#198754',
|
success_color: '#198754',
|
||||||
header_footer_bg_color: '#313131',
|
header_bg_color: '#313131',
|
||||||
|
footer_bg_color: '#313131',
|
||||||
header_logo: 'https://example.com/header.png',
|
header_logo: 'https://example.com/header.png',
|
||||||
footer_logo: 'https://example.com/footer.png'
|
footer_logo: 'https://example.com/footer.png'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ function hexToRgb(hex: string): string {
|
|||||||
'[style.--tenant-secondary-rgb]': 'tenantSecondaryColorRgb()',
|
'[style.--tenant-secondary-rgb]': 'tenantSecondaryColorRgb()',
|
||||||
'[style.--tenant-danger-rgb]': 'tenantDangerColorRgb()',
|
'[style.--tenant-danger-rgb]': 'tenantDangerColorRgb()',
|
||||||
'[style.--tenant-success-rgb]': 'tenantSuccessColorRgb()',
|
'[style.--tenant-success-rgb]': 'tenantSuccessColorRgb()',
|
||||||
'[style.--tenant-header-footer-bg]': 'tenantHeaderFooterBgColor()'
|
'[style.--tenant-header-bg]': 'tenantHeaderBgColor()',
|
||||||
|
'[style.--tenant-footer-bg]': 'tenantFooterBgColor()'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export class App {
|
export class App {
|
||||||
@@ -66,10 +67,15 @@ export class App {
|
|||||||
protected readonly tenantSuccessColorRgb = computed(() =>
|
protected readonly tenantSuccessColorRgb = computed(() =>
|
||||||
hexToRgb(this.tenantSuccessColor())
|
hexToRgb(this.tenantSuccessColor())
|
||||||
);
|
);
|
||||||
protected readonly tenantHeaderFooterBgColor = computed(
|
protected readonly tenantHeaderBgColor = computed(
|
||||||
() =>
|
() =>
|
||||||
this.tenantService.tenant()?.header_footer_bg_color ??
|
this.tenantService.tenant()?.header_bg_color ??
|
||||||
DEFAULT_TENANT_BRANDING.headerFooterBgColor
|
DEFAULT_TENANT_BRANDING.headerBgColor
|
||||||
|
);
|
||||||
|
protected readonly tenantFooterBgColor = computed(
|
||||||
|
() =>
|
||||||
|
this.tenantService.tenant()?.footer_bg_color ??
|
||||||
|
DEFAULT_TENANT_BRANDING.footerBgColor
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.store-layout__footer {
|
.store-layout__footer {
|
||||||
background-color: var(--tenant-header-footer-bg);
|
background-color: var(--tenant-footer-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-layout__brand-slot {
|
.store-layout__brand-slot {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.store-layout__header {
|
.store-layout__header {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: var(--tenant-header-footer-bg);
|
background-color: var(--tenant-header-bg);
|
||||||
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.08);
|
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,14 +58,14 @@
|
|||||||
.store-layout__search-button {
|
.store-layout__search-button {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border-color: rgba(255, 255, 255, 0.2);
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
background-color: color-mix(in srgb, white 10%, var(--tenant-header-footer-bg));
|
background-color: color-mix(in srgb, white 10%, var(--tenant-header-bg));
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-layout__search-button:hover,
|
.store-layout__search-button:hover,
|
||||||
.store-layout__search-button:focus-visible {
|
.store-layout__search-button:focus-visible {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border-color: rgba(255, 255, 255, 0.28);
|
border-color: rgba(255, 255, 255, 0.28);
|
||||||
background-color: color-mix(in srgb, white 16%, var(--tenant-header-footer-bg));
|
background-color: color-mix(in srgb, white 16%, var(--tenant-header-bg));
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-layout__action-button {
|
.store-layout__action-button {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ const tenant: Tenant = {
|
|||||||
secondary_color: '#A0A0A0',
|
secondary_color: '#A0A0A0',
|
||||||
danger_color: '#FF8888',
|
danger_color: '#FF8888',
|
||||||
success_color: '#198754',
|
success_color: '#198754',
|
||||||
header_footer_bg_color: '#313131',
|
header_bg_color: '#313131',
|
||||||
|
footer_bg_color: '#313131',
|
||||||
header_logo: 'https://example.com/header.png',
|
header_logo: 'https://example.com/header.png',
|
||||||
footer_logo: 'https://example.com/footer.png'
|
footer_logo: 'https://example.com/footer.png'
|
||||||
};
|
};
|
||||||
|
|||||||
21
src/app/core/services/cart/cart.interface.ts
Normal file
21
src/app/core/services/cart/cart.interface.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export interface CartItemProduct {
|
||||||
|
nombre: string;
|
||||||
|
imagen: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CartItem {
|
||||||
|
id: number;
|
||||||
|
cantidad: number;
|
||||||
|
precio_unitario: string;
|
||||||
|
product_id: number;
|
||||||
|
product_variant_id: number;
|
||||||
|
product: CartItemProduct | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Cart {
|
||||||
|
id: number | null;
|
||||||
|
tenant_codigo: string;
|
||||||
|
status: string;
|
||||||
|
items: CartItem[];
|
||||||
|
subtotal: string;
|
||||||
|
}
|
||||||
125
src/app/core/services/cart/cart.service.spec.ts
Normal file
125
src/app/core/services/cart/cart.service.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { CartService } from './cart.service';
|
||||||
|
import { TenantService } from '../tenant.service';
|
||||||
|
import { Cart } from './cart.interface';
|
||||||
|
|
||||||
|
describe('CartService', () => {
|
||||||
|
let service: CartService;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
let tenantServiceMock: any;
|
||||||
|
|
||||||
|
const mockCart: Cart = {
|
||||||
|
id: 123,
|
||||||
|
tenant_codigo: 'acme',
|
||||||
|
status: 'active',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
cantidad: 2,
|
||||||
|
precio_unitario: '10.00',
|
||||||
|
product_id: 5,
|
||||||
|
product_variant_id: 10,
|
||||||
|
product: {
|
||||||
|
nombre: 'Test Product (Size: M)',
|
||||||
|
imagen: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
subtotal: '20.00'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tenantServiceMock = {
|
||||||
|
getTenantApiUrl: vi.fn().mockReturnValue('http://api.test/tenants/acme')
|
||||||
|
};
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
CartService,
|
||||||
|
provideHttpClient(),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
{ provide: TenantService, useValue: tenantServiceMock }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
service = TestBed.inject(CartService);
|
||||||
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize with null cart signal', () => {
|
||||||
|
expect(service.cart()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load cart and update signal', () => {
|
||||||
|
service.loadCart().subscribe((cart) => {
|
||||||
|
expect(cart).toEqual(mockCart);
|
||||||
|
expect(service.cart()).toEqual(mockCart);
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne('http://api.test/tenants/acme/cart');
|
||||||
|
expect(req.request.method).toBe('GET');
|
||||||
|
expect(req.request.withCredentials).toBe(true);
|
||||||
|
req.flush({ data: mockCart });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add item and update signal', () => {
|
||||||
|
service.addItem(10, 2).subscribe((cart) => {
|
||||||
|
expect(cart).toEqual(mockCart);
|
||||||
|
expect(service.cart()).toEqual(mockCart);
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne('http://api.test/tenants/acme/cart/items');
|
||||||
|
expect(req.request.method).toBe('POST');
|
||||||
|
expect(req.request.body).toEqual({ product_variant_id: 10, cantidad: 2 });
|
||||||
|
expect(req.request.withCredentials).toBe(true);
|
||||||
|
req.flush({ data: mockCart });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update item quantity and update signal', () => {
|
||||||
|
const updatedCart = { ...mockCart, subtotal: '30.00' };
|
||||||
|
updatedCart.items[0].cantidad = 3;
|
||||||
|
|
||||||
|
service.updateItemQuantity(10, 3).subscribe((cart) => {
|
||||||
|
expect(cart).toEqual(updatedCart);
|
||||||
|
expect(service.cart()).toEqual(updatedCart);
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne('http://api.test/tenants/acme/cart/items/10');
|
||||||
|
expect(req.request.method).toBe('PATCH');
|
||||||
|
expect(req.request.body).toEqual({ cantidad: 3 });
|
||||||
|
expect(req.request.withCredentials).toBe(true);
|
||||||
|
req.flush({ data: updatedCart });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove item and update signal', () => {
|
||||||
|
const emptyCart: Cart = {
|
||||||
|
id: 123,
|
||||||
|
tenant_codigo: 'acme',
|
||||||
|
status: 'active',
|
||||||
|
items: [],
|
||||||
|
subtotal: '0.00'
|
||||||
|
};
|
||||||
|
|
||||||
|
service.removeItem(10).subscribe((cart) => {
|
||||||
|
expect(cart).toEqual(emptyCart);
|
||||||
|
expect(service.cart()).toEqual(emptyCart);
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne('http://api.test/tenants/acme/cart/items/10');
|
||||||
|
expect(req.request.method).toBe('DELETE');
|
||||||
|
expect(req.request.withCredentials).toBe(true);
|
||||||
|
req.flush({ data: emptyCart });
|
||||||
|
});
|
||||||
|
});
|
||||||
71
src/app/core/services/cart/cart.service.ts
Normal file
71
src/app/core/services/cart/cart.service.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { inject, Injectable, signal } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { map, Observable, tap } from 'rxjs';
|
||||||
|
|
||||||
|
import { ApiResponse } from '../api-response.interface';
|
||||||
|
import { TenantService } from '../tenant.service';
|
||||||
|
import { Cart } from './cart.interface';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class CartService {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly tenantService = inject(TenantService);
|
||||||
|
|
||||||
|
private readonly cartState = signal<Cart | null>(null);
|
||||||
|
readonly cart = this.cartState.asReadonly();
|
||||||
|
|
||||||
|
private get tenantApiUrl(): string {
|
||||||
|
return this.tenantService.getTenantApiUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCart(): Observable<Cart> {
|
||||||
|
return this.http
|
||||||
|
.get<ApiResponse<Cart>>(`${this.tenantApiUrl}/cart`, {
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
map((response) => response.data),
|
||||||
|
tap((cart) => this.cartState.set(cart))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(productVariantId: number, cantidad: number): Observable<Cart> {
|
||||||
|
return this.http
|
||||||
|
.post<ApiResponse<Cart>>(
|
||||||
|
`${this.tenantApiUrl}/cart/items`,
|
||||||
|
{ product_variant_id: productVariantId, cantidad },
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
map((response) => response.data),
|
||||||
|
tap((cart) => this.cartState.set(cart))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateItemQuantity(productVariantId: number, cantidad: number): Observable<Cart> {
|
||||||
|
return this.http
|
||||||
|
.patch<ApiResponse<Cart>>(
|
||||||
|
`${this.tenantApiUrl}/cart/items/${productVariantId}`,
|
||||||
|
{ cantidad },
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
map((response) => response.data),
|
||||||
|
tap((cart) => this.cartState.set(cart))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(productVariantId: number): Observable<Cart> {
|
||||||
|
return this.http
|
||||||
|
.delete<ApiResponse<Cart>>(
|
||||||
|
`${this.tenantApiUrl}/cart/items/${productVariantId}`,
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
map((response) => response.data),
|
||||||
|
tap((cart) => this.cartState.set(cart))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@ export const DEFAULT_TENANT_BRANDING = {
|
|||||||
secondaryColor: '#9e9e9e',
|
secondaryColor: '#9e9e9e',
|
||||||
dangerColor: '#fd4c4c',
|
dangerColor: '#fd4c4c',
|
||||||
successColor: '#198754',
|
successColor: '#198754',
|
||||||
headerFooterBgColor: '#313131'
|
headerBgColor: '#313131',
|
||||||
|
footerBgColor: '#313131'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const TENANT_SSR_STATE_KEY = makeStateKey<TenantSsrState | null>('tenant-ssr-state');
|
export const TENANT_SSR_STATE_KEY = makeStateKey<TenantSsrState | null>('tenant-ssr-state');
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export interface Tenant {
|
|||||||
secondary_color: string;
|
secondary_color: string;
|
||||||
danger_color: string;
|
danger_color: string;
|
||||||
success_color: string;
|
success_color: string;
|
||||||
header_footer_bg_color: string;
|
header_bg_color: string;
|
||||||
|
footer_bg_color: string;
|
||||||
header_logo: string;
|
header_logo: string;
|
||||||
footer_logo: string;
|
footer_logo: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ const tenant: Tenant = {
|
|||||||
secondary_color: '#A0A0A0',
|
secondary_color: '#A0A0A0',
|
||||||
danger_color: '#FF8888',
|
danger_color: '#FF8888',
|
||||||
success_color: '#198754',
|
success_color: '#198754',
|
||||||
header_footer_bg_color: '#313131',
|
header_bg_color: '#313131',
|
||||||
|
footer_bg_color: '#313131',
|
||||||
header_logo: 'https://example.com/header.png',
|
header_logo: 'https://example.com/header.png',
|
||||||
footer_logo: 'https://example.com/footer.png'
|
footer_logo: 'https://example.com/footer.png'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -220,7 +220,7 @@
|
|||||||
<div class="col-md-6 mb-3 mb-md-0">
|
<div class="col-md-6 mb-3 mb-md-0">
|
||||||
<div class="tenant-logo-card">
|
<div class="tenant-logo-card">
|
||||||
<div class="tenant-logo-card__label">Logo del Header</div>
|
<div class="tenant-logo-card__label">Logo del Header</div>
|
||||||
<div class="tenant-logo-card__container header-footer-bg">
|
<div class="tenant-logo-card__container header-bg">
|
||||||
@if (tenant()?.header_logo) {
|
@if (tenant()?.header_logo) {
|
||||||
<img [src]="tenant()?.header_logo" alt="Header Logo" class="tenant-logo-card__img" />
|
<img [src]="tenant()?.header_logo" alt="Header Logo" class="tenant-logo-card__img" />
|
||||||
} @else {
|
} @else {
|
||||||
@@ -236,7 +236,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="tenant-logo-card">
|
<div class="tenant-logo-card">
|
||||||
<div class="tenant-logo-card__label">Logo del Footer</div>
|
<div class="tenant-logo-card__label">Logo del Footer</div>
|
||||||
<div class="tenant-logo-card__container header-footer-bg">
|
<div class="tenant-logo-card__container footer-bg">
|
||||||
@if (tenant()?.footer_logo) {
|
@if (tenant()?.footer_logo) {
|
||||||
<img [src]="tenant()?.footer_logo" alt="Footer Logo" class="tenant-logo-card__img" />
|
<img [src]="tenant()?.footer_logo" alt="Footer Logo" class="tenant-logo-card__img" />
|
||||||
} @else {
|
} @else {
|
||||||
@@ -295,6 +295,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="color-swatch-card shadow-sm">
|
||||||
|
<div class="color-swatch-card__preview" [style.background-color]="'var(--tenant-header-bg)'"></div>
|
||||||
|
<div class="color-swatch-card__body">
|
||||||
|
<span class="color-swatch-card__title">Header BG</span>
|
||||||
|
<code class="color-swatch-card__value">{{ tenant()?.header_bg_color || 'Default' }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="color-swatch-card shadow-sm">
|
||||||
|
<div class="color-swatch-card__preview" [style.background-color]="'var(--tenant-footer-bg)'"></div>
|
||||||
|
<div class="color-swatch-card__body">
|
||||||
|
<span class="color-swatch-card__title">Footer BG</span>
|
||||||
|
<code class="color-swatch-card__value">{{ tenant()?.footer_bg_color || 'Default' }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -164,8 +164,12 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-footer-bg {
|
.header-bg {
|
||||||
background-color: var(--tenant-header-footer-bg, #f8f9fa);
|
background-color: var(--tenant-header-bg, #f8f9fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bg {
|
||||||
|
background-color: var(--tenant-footer-bg, #f8f9fa);
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-swatch-card {
|
.color-swatch-card {
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ const tenant: Tenant = {
|
|||||||
secondary_color: '#A0A0A0',
|
secondary_color: '#A0A0A0',
|
||||||
danger_color: '#FF8888',
|
danger_color: '#FF8888',
|
||||||
success_color: '#198754',
|
success_color: '#198754',
|
||||||
header_footer_bg_color: '#313131',
|
header_bg_color: '#313131',
|
||||||
|
footer_bg_color: '#313131',
|
||||||
header_logo: 'https://example.com/header.png',
|
header_logo: 'https://example.com/header.png',
|
||||||
footer_logo: 'https://example.com/footer.png'
|
footer_logo: 'https://example.com/footer.png'
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user