master
parent
e4cb3145fa
commit
490b766a74
|
After Width: | Height: | Size: 100 KiB |
@ -0,0 +1,79 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Badge.svelte
|
||||||
|
* Componente reutilizable para mostrar badges con iconos
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type?: 'warning' | 'success' | 'error' | 'info' | 'disabled';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
position?: 'top-right' | 'top-right-i' | 'top-left' | 'bottom-right' | 'bottom-left';
|
||||||
|
customIcon?: 'snippet'; // Para pasar HTML del icono
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
type = 'warning',
|
||||||
|
size = 'md',
|
||||||
|
position = 'top-right',
|
||||||
|
customIcon
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
// Configuración de tamaños
|
||||||
|
const sizes = {
|
||||||
|
sm: { container: 'w-6 h-6', icon: 'w-4 h-4' },
|
||||||
|
md: { container: 'w-8 h-8', icon: 'w-6 h-6' },
|
||||||
|
lg: { container: 'w-10 h-10', icon: 'w-8 h-8' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configuración de colores
|
||||||
|
const colors = {
|
||||||
|
warning: 'bg-yellow-400',
|
||||||
|
success: 'bg-green-500',
|
||||||
|
error: 'bg-red-500',
|
||||||
|
info: 'bg-blue-500',
|
||||||
|
disabled: 'bg-gray-400'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configuración de posiciones
|
||||||
|
const positions = {
|
||||||
|
'top-right': '-top-2 -right-2',
|
||||||
|
'top-right-i': 'top-1 right-1',
|
||||||
|
'top-left': '-top-2 -left-2',
|
||||||
|
'bottom-right': '-bottom-2 -right-2',
|
||||||
|
'bottom-left': '-bottom-2 -left-2'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Iconos por tipo
|
||||||
|
const icons = {
|
||||||
|
warning: `<svg class="${sizes[size].icon} text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||||
|
</svg>`,
|
||||||
|
success: `<svg class="${sizes[size].icon} text-white" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
||||||
|
</svg>`,
|
||||||
|
error: `<svg class="${sizes[size].icon} text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||||
|
</svg>`,
|
||||||
|
info: `<svg class="${sizes[size].icon} text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||||
|
</svg>`,
|
||||||
|
disabled: `<svg class="${sizes[size].icon} text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z" clip-rule="evenodd"/>
|
||||||
|
</svg>`
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconHtml = customIcon || icons[type];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute {positions[position]} {sizes[size].container} {colors[type]} rounded-full flex items-center justify-center shadow-md z-10"
|
||||||
|
>
|
||||||
|
{@html iconHtml}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Asegurar que el badge esté siempre visible */
|
||||||
|
div {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,165 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { appState } from "../data/AppState.svelte";
|
|
||||||
import type { OptionReference } from "../data/DataConfigTypes";
|
|
||||||
import {dataConfig} from "../data/DataConfig";
|
|
||||||
import {formatPrice} from "../utils/formats";
|
|
||||||
import {popupMessage} from "../data/PopsState.svelte";
|
|
||||||
|
|
||||||
const imgPath : string = dataConfig.sources.imgValues
|
|
||||||
|
|
||||||
let {
|
|
||||||
optionRef,
|
|
||||||
groupId,
|
|
||||||
viewId,
|
|
||||||
isDisabled = false,
|
|
||||||
isSelected = false
|
|
||||||
} = $props<{
|
|
||||||
optionRef: OptionReference;
|
|
||||||
groupId: string;
|
|
||||||
viewId: string;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
isSelected?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const textWithCost = dataConfig.texts.with_cost;
|
|
||||||
|
|
||||||
// Forzar que estos sean reactivos
|
|
||||||
const optionValue = $derived(appState.getOptionValue(optionRef.id));
|
|
||||||
const consultation = $derived(appState.requiresConsultation(viewId, groupId, optionRef.id));
|
|
||||||
|
|
||||||
const imgOptSrc = $derived(imgPath + optionValue.thumb)
|
|
||||||
|
|
||||||
// Añadir log solo cuando isSelected cambia
|
|
||||||
$effect(() => {
|
|
||||||
if (isSelected) {
|
|
||||||
console.log(`✓ ${optionRef.id} SELECCIONADO en ${viewId}/${groupId}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
if (isSelected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('🔵 CLICK:', { viewId, groupId, optionId: optionRef.id });
|
|
||||||
if (consultation.message) {
|
|
||||||
popupMessage.open(consultation.message);
|
|
||||||
}
|
|
||||||
appState.setOption(viewId, groupId, optionRef.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Obtener el precio desde optionRef.amount
|
|
||||||
const hasPrice = $derived(
|
|
||||||
optionRef.amount !== undefined &&
|
|
||||||
optionRef.amount === textWithCost || (
|
|
||||||
typeof optionRef.amount === 'number' &&
|
|
||||||
optionRef.amount > 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="relative border-2 rounded-lg p-3 text-left transition-all hover:shadow-md flex flex-col items-center"
|
|
||||||
class:border-green-600={isSelected}
|
|
||||||
class:ring-offset-1={isSelected}
|
|
||||||
class:ring-green-600={isSelected}
|
|
||||||
|
|
||||||
|
|
||||||
class:border-gray-200={!isSelected}
|
|
||||||
|
|
||||||
class:cursor-not-allowed={isDisabled}
|
|
||||||
class:cursor-pointer={!isDisabled && !isSelected}
|
|
||||||
onclick={handleClick}
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- Warning indicator (simple) -->
|
|
||||||
|
|
||||||
{#if consultation.message}
|
|
||||||
<div class="absolute -top-3 -left-1 w-5 h-5 rounded-full flex items-center justify-center text-white text-s font-bold shadow-md p-3">
|
|
||||||
⚠
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Selected indicator (simple) -->
|
|
||||||
{#if isSelected}
|
|
||||||
<div class="absolute -top-1 {consultation.message ? '-right-1' : '-right-1'} w-5 h-5 bg-green-600 rounded-full flex items-center justify-center text-white text-xs font-bold shadow-md">
|
|
||||||
✓
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Imagen -->
|
|
||||||
{#if optionValue?.thumb}
|
|
||||||
<img
|
|
||||||
class="h-20 object-cover mb-2"
|
|
||||||
src={imgOptSrc}
|
|
||||||
alt={optionValue.name}
|
|
||||||
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<!--
|
|
||||||
<div class="w-full h-20 bg-gray-100 rounded mb-2 flex items-center justify-center">
|
|
||||||
<span class="text-gray-400 text-xs">Sin imagen</span>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Nombre -->
|
|
||||||
<div class="font-medium text-xs mb-1 text-center">
|
|
||||||
{optionValue?.name || optionRef.id}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ✅ Badge "No disponible" para opciones deshabilitadas -->
|
|
||||||
{#if isDisabled}
|
|
||||||
<div class="absolute -top-2 -right-1 w-5 h-5 rounded-full flex items-center justify-center text-white text-s font-bold shadow-md">
|
|
||||||
🚫
|
|
||||||
</div>
|
|
||||||
<!--
|
|
||||||
<div class="absolute top-1 right-1 bg-red-500 text-white px-2 py-0.5 rounded-full text-xs font-semibold">
|
|
||||||
🚫
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Texto adicional -->
|
|
||||||
<!--
|
|
||||||
{#if optionValue?.text}
|
|
||||||
<div class="text-xs text-gray-600 mb-1">
|
|
||||||
{optionValue.text}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Precio -->
|
|
||||||
{#if hasPrice}
|
|
||||||
<div class="text-center mt-1">
|
|
||||||
<span class="w-full inline-block bg-gray-200 px-2 py-1 text-gray-800 text-xm font-semibold rounded">
|
|
||||||
+ {formatPrice(optionRef.amount)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Badge consultar -->
|
|
||||||
<!--
|
|
||||||
{#if consultation.required}
|
|
||||||
<span class="inline-block px-2 py-0.5 bg-yellow-100 text-yellow-800 text-xs rounded mt-1">
|
|
||||||
Consultar
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
-->
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Advertencia -->
|
|
||||||
<!--
|
|
||||||
{#if consultation.message && isSelected}
|
|
||||||
<div class="col-span-2 p-3 bg-yellow-50 border-l-4 border-yellow-400 text-xs text-yellow-800 rounded">
|
|
||||||
⚠️ {consultation.message}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
-->
|
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {OptionGroup} from "../data/DataConfigTypes";
|
||||||
|
import {appState} from "../data/AppState.svelte";
|
||||||
|
import BtOption from "./BtOption.svelte";
|
||||||
|
import {derived} from "svelte/store";
|
||||||
|
|
||||||
|
let {
|
||||||
|
group,
|
||||||
|
viewId = appState.selection.currentViewId
|
||||||
|
} = $props<{
|
||||||
|
group: OptionGroup;
|
||||||
|
viewId?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedValue = $derived(appState.getCurrentOption(viewId, group.id));
|
||||||
|
const options = $derived(appState.getGroupOptions(viewId, group.id));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Obtener la información de consulta de la opción seleccionada
|
||||||
|
const selectedConsultation = $derived.by(() => {
|
||||||
|
if (!selectedValue) return null;
|
||||||
|
return appState.requiresConsultation(viewId, group.id, selectedValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-0" >
|
||||||
|
<!--
|
||||||
|
<h4 class="text-sm font-semibold text-gray-800 mb-3">{group.name || 'Opciones' }</h4>
|
||||||
|
-->
|
||||||
|
<p class="text-sl mb-4">
|
||||||
|
El pavimento de la vivienda es el mismo en todas las estancias, por lo que la opción elegida será la que se instale también en el resto de la vivienda.
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
{#each options as option (option.id) }
|
||||||
|
|
||||||
|
{@const isSelected = selectedValue === option.id}
|
||||||
|
|
||||||
|
<BtOption
|
||||||
|
optionRef={option}
|
||||||
|
groupId={group.id}
|
||||||
|
{viewId}
|
||||||
|
{isSelected}
|
||||||
|
isDisabled={!option.isValid}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { appState } from '../data/AppState.svelte';
|
||||||
|
import { dataConfig } from '../data/DataConfig';
|
||||||
|
import type { View } from '../data/DataConfigTypes';
|
||||||
|
import {formatPrice} from "../utils/formats";
|
||||||
|
import Badget from "./Badget.svelte";
|
||||||
|
import {handleImageStyleError} from "../utils/uix";
|
||||||
|
import {navigationState} from "../data/NavState.svelte";
|
||||||
|
|
||||||
|
// Props
|
||||||
|
/*
|
||||||
|
interface Props {
|
||||||
|
view: View;
|
||||||
|
onClick?: (viewId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { view, onClick }: Props = $props();
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
appState.setView('suelo');
|
||||||
|
navigationState.activePage = 'cfg'; // Navegar al configurador
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-xl border border-gray-300 shadow-sm overflow-hidden hover:shadow-lg transition-all duration-200 cursor-pointer group"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onclick={handleClick}
|
||||||
|
onkeydown={(e) => e.key === 'Enter' && handleClick()}
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Título de la vista -->
|
||||||
|
|
||||||
|
<div class="bg-green-900 text-white p-2">
|
||||||
|
<h4 class="text-lg font-semibold">
|
||||||
|
Opciones Generales
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Imagen de la vista -->
|
||||||
|
<div class="relative h-48 bg-gray-100 overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={appState.imgSrc + 'edificio.jpg'}
|
||||||
|
alt="Pavimento General"
|
||||||
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Lista de selecciones -->
|
||||||
|
|
||||||
|
<div class="p-4 space-y-2 text-left min-h-[180px]">
|
||||||
|
{#each Object.entries(dataConfig.options.general) as [key, group]}
|
||||||
|
{@const currentOptionId = appState.getCurrentOption('general', key)}
|
||||||
|
{@const optionValue = currentOptionId ? appState.getOptionValue(currentOptionId) : null}
|
||||||
|
|
||||||
|
<div class="flex flex-col p-2">
|
||||||
|
<span class="text-xs text-gray-500 uppercase tracking-wider mb-2 font-semibold">
|
||||||
|
{group.name || group.id}
|
||||||
|
</span>
|
||||||
|
<span class="text-base font-semibold text-gray-800 mb-3">
|
||||||
|
{optionValue?.name || currentOptionId || 'No seleccionado'}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{#if optionValue?.thumb}
|
||||||
|
<img
|
||||||
|
src={dataConfig.sources.imgValues + optionValue.image}
|
||||||
|
alt={optionValue.name}
|
||||||
|
class="w-full h-12 object-cover rounded-md shadow-sm"
|
||||||
|
onerror={handleImageStyleError}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
// uix.ts
|
||||||
|
|
||||||
|
|
||||||
|
export function handleImageStyleError(e: Event) {
|
||||||
|
const target = e.currentTarget as HTMLImageElement;
|
||||||
|
target.onerror = null; // evita bucle
|
||||||
|
target.style.display = 'none';
|
||||||
|
}
|
||||||
Loading…
Reference in new issue