master
parent
1d3a19cebf
commit
a856ed7de1
@ -1,14 +1,56 @@
|
|||||||
<script lang="ts">
|
<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>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<button type="button"
|
<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()}>
|
onclick={() => generatePdf()}>
|
||||||
Descargar Selección
|
Descargar Selección
|
||||||
</button>
|
</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