master
parent
1d3a19cebf
commit
a856ed7de1
@ -1,14 +1,56 @@
|
||||
<script lang="ts">
|
||||
import {dataConfig} from "../data/DataConfig.js";
|
||||
import type {DownloadState} from "../data/DownloadTypes";
|
||||
import PoDownload from "../cmps/PoDownload.svelte";
|
||||
|
||||
async function generatePdf () {}
|
||||
|
||||
const stepTimeout = 1000;
|
||||
const logoProject = dataConfig.sources.logos + dataConfig.logos.project;
|
||||
const logoCompany = dataConfig.sources.logos + dataConfig.logos.company;
|
||||
console.log(logoProject, logoCompany);
|
||||
|
||||
|
||||
let downloadState : DownloadState = 'idle';
|
||||
let isVisible = false;
|
||||
let message = '';
|
||||
|
||||
// Función auxiliar para esperar
|
||||
const wait = (ms : number ) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
async function generatePdf () {
|
||||
isVisible = true;
|
||||
downloadState = 'creating';
|
||||
|
||||
try {
|
||||
await wait(3000);
|
||||
|
||||
} catch (err : any) {
|
||||
downloadState = 'error';
|
||||
message = err?.message;
|
||||
console.log(err);
|
||||
} finally {
|
||||
await wait(1000);
|
||||
isVisible = false;
|
||||
downloadState = 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<button type="button"
|
||||
class="px-3 lg:px-4 py-2 rounded-md text-base font-medium transition-colors duration-200 text-white bg-green-900 hover:bg-green-700 cursor-pointer transition"
|
||||
class="px-3 lg:px-4 py-2 rounded-md text-base font-medium transition-colors duration-200 text-white bg-green-900 hover:bg-green-700 cursor-pointer"
|
||||
onclick={() => generatePdf()}>
|
||||
Descargar Selección
|
||||
</button>
|
||||
|
||||
<PoDownload
|
||||
{isVisible}
|
||||
{message}
|
||||
{downloadState}
|
||||
logoCompany={logoCompany}
|
||||
logoProject={logoProject}
|
||||
|
||||
text={'testo'}
|
||||
/>
|
||||
@ -0,0 +1,89 @@
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { fade, scale } from 'svelte/transition';
|
||||
import type {DownloadState} from "../data/DownloadTypes";
|
||||
import {cubicOut} from "svelte/easing";
|
||||
|
||||
let {
|
||||
downloadState,
|
||||
isVisible ,
|
||||
message,
|
||||
logoProject,
|
||||
logoCompany,
|
||||
text,
|
||||
} = $props< {
|
||||
downloadState: DownloadState;
|
||||
isVisible : boolean;
|
||||
message : string ;
|
||||
logoProject : string ;
|
||||
logoCompany? : string ;
|
||||
text : string
|
||||
}> ();
|
||||
|
||||
|
||||
let stateText = $derived (
|
||||
downloadState === 'creating' ? 'Creando documento PDF' :
|
||||
downloadState === 'downloading' ? 'Descargando documento' :
|
||||
downloadState === 'success' ? '✅ PDF Generado correctamente' :
|
||||
''
|
||||
);
|
||||
|
||||
let showSpin = $derived (
|
||||
downloadState === 'creating' || downloadState === 'downloading'
|
||||
);
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{#if isVisible}
|
||||
<!-- Fondo oscuro con fundido -->
|
||||
<div
|
||||
transition:fade={{ duration: 200, easing: cubicOut }}
|
||||
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||
>
|
||||
<!-- Contenedor del popup con entrada tipo zoom -->
|
||||
<div
|
||||
transition:scale={{ duration: 250, easing: cubicOut }}
|
||||
class="bg-white rounded-2xl px-10 py-8 w-[420px] text-center shadow-2xl flex flex-col items-center space-y-6 "
|
||||
>
|
||||
<!-- Bloque superior: logos y título -->
|
||||
<div class="flex flex-col leading-none">
|
||||
{#if logoCompany}
|
||||
<img src={logoCompany} alt="logo empresa" class="w-40 block leading-none" />
|
||||
{/if}
|
||||
|
||||
<img src={logoProject} alt="logo proyecto" class="w-92 block" />
|
||||
|
||||
<h2 class="text-2xl font-semibold text-gray-600 tracking-wide leading-tight mt-1">
|
||||
{text}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Contenido del estado -->
|
||||
{#if downloadState !== 'error' && downloadState !== 'idle' }
|
||||
|
||||
<div class="flex items-center space-x-3 mt-4 w-full">
|
||||
<!-- Contenedor para reservar espacio del spinner -->
|
||||
<div class="w-10 h-10 flex-shrink-0">
|
||||
{#if showSpin}
|
||||
<div class="w-10 h-10 border-4 border-gray-200 border-t-primary-700 border-t-opacity-75 rounded-full animate-spin"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Texto -->
|
||||
<div class="text-primary-700 font-medium text-lg">
|
||||
{stateText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class="text-red-600 text-lg font-semibold mt-4">
|
||||
❌ Error al generar PDF
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm mt-2 max-w-xs">{message}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@ -0,0 +1,170 @@
|
||||
<script lang="ts">
|
||||
import { appState } from '../data/AppState.svelte';
|
||||
import { dataConfig } from '../data/DataConfig';
|
||||
import type { View } from '../data/DataConfigTypes';
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
view: View;
|
||||
onClick?: (viewId: string) => void;
|
||||
}
|
||||
|
||||
let { view, onClick }: Props = $props();
|
||||
|
||||
// Función para obtener la URL de la imagen de la vista
|
||||
function getViewImageUrl(viewId: string): string {
|
||||
const parts: string[] = [viewId];
|
||||
|
||||
// Agregar opciones generales
|
||||
Object.keys(dataConfig.options.general).forEach(genKey => {
|
||||
const value = appState.selection.general[genKey];
|
||||
if (value) parts.push(value);
|
||||
});
|
||||
|
||||
// Agregar opciones específicas de la vista
|
||||
const viewConfig = dataConfig.options.views[viewId];
|
||||
if (viewConfig?.options) {
|
||||
viewConfig.options.forEach((group) => {
|
||||
const value = appState.selection.views[viewId]?.[group.id];
|
||||
if (value) parts.push(value);
|
||||
});
|
||||
}
|
||||
|
||||
return appState.imgSrc + `${parts.join('_')}.jpg`;
|
||||
}
|
||||
|
||||
// Función para calcular el total de la vista
|
||||
function getViewTotal(): number {
|
||||
let total = 0;
|
||||
|
||||
for (const group of view.options) {
|
||||
const currentOptionId = appState.getCurrentOption(view.id, group.id);
|
||||
if (currentOptionId) {
|
||||
const option = group.values.find(v => v.id === currentOptionId);
|
||||
if (option?.amount && typeof option.amount === 'number') {
|
||||
total += option.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
// Función para formatear precio
|
||||
function formatearPrecio(cantidad: number): string {
|
||||
return new Intl.NumberFormat('es-ES', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(cantidad);
|
||||
}
|
||||
|
||||
// Verificar si hay incidencias en esta vista
|
||||
function hasIncidencias(): boolean {
|
||||
for (const group of view.options) {
|
||||
const currentOptionId = appState.getCurrentOption(view.id, group.id);
|
||||
if (currentOptionId) {
|
||||
const consultaInfo = appState.requiresConsultation(view.id, group.id, currentOptionId);
|
||||
if (consultaInfo.required) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handler de click
|
||||
function handleClick() {
|
||||
if (onClick) {
|
||||
onClick(view.id);
|
||||
} else {
|
||||
appState.setView(view.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Valores derivados reactivos
|
||||
let imageUrl = $derived(getViewImageUrl(view.id));
|
||||
let total = $derived(getViewTotal());
|
||||
let hasWarnings = $derived(hasIncidencias());
|
||||
</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"
|
||||
class:border-red-300={hasWarnings}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={handleClick}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleClick()}
|
||||
>
|
||||
|
||||
<!-- Imagen de la vista -->
|
||||
<div class="relative h-48 bg-gray-100 overflow-hidden">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={view.name || view.id}
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
|
||||
|
||||
|
||||
/>
|
||||
|
||||
<!-- Indicador de incidencias -->
|
||||
{#if hasWarnings}
|
||||
<div class="absolute top-2 right-2 bg-red-500 text-white px-2 py-1 rounded-full text-xs font-semibold shadow-lg">
|
||||
⚠️ Revisar
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Título de la vista -->
|
||||
<div class="bg-green-800 text-white p-3">
|
||||
<h3 class="text-lg font-semibold">
|
||||
{view.name || view.id}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- Lista de selecciones -->
|
||||
<div class="p-4 space-y-2 text-left min-h-[180px]">
|
||||
{#if view.options.length > 0}
|
||||
{#each view.options as group}
|
||||
{@const currentOptionId = appState.getCurrentOption(view.id, group.id)}
|
||||
{@const optionValue = currentOptionId ? appState.getOptionValue(currentOptionId) : null}
|
||||
{@const requiresConsult = currentOptionId ? appState.requiresConsultation(view.id, group.id, currentOptionId) : { required: false }}
|
||||
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs text-gray-500 uppercase tracking-wide">
|
||||
{group.name || group.id}
|
||||
</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-sm font-medium text-gray-800">
|
||||
{optionValue?.name || currentOptionId || 'No seleccionado'}
|
||||
</span>
|
||||
{#if requiresConsult.required}
|
||||
<span class="text-xs text-red-600" title={requiresConsult.message}>⚠️</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<p class="text-sm text-gray-500 italic">Sin opciones específicas</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Total de la vista (si hay precios) -->
|
||||
{#if total > 0}
|
||||
<div class="px-4 pb-4">
|
||||
<div class="pt-3 border-t border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-semibold text-gray-700">Extras:</span>
|
||||
<span class="text-base font-bold text-green-700">
|
||||
{formatearPrecio(total)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Estilos adicionales si son necesarios */
|
||||
</style>
|
||||
@ -0,0 +1,3 @@
|
||||
// DownloadTypes.ts
|
||||
|
||||
export type DownloadState = 'idle' | 'creating' | 'downloading' | 'success' | 'error'
|
||||
Loading…
Reference in new issue