Files
web-app/src/lib/pages/print/PrintMenu.svelte

155 lines
5.8 KiB
Svelte
Raw Normal View History

2026-09-08 17:38:13 +03:00
<!-- lib/pages/print/PrintMenu.svelte -->
<script>
2026-09-17 03:50:12 +03:00
import { createPrintStore } from './store.svelte.js';
import PrintUI from './PrintUI.svelte';
import PreviewOverlay from './PreviewOverlay.svelte';
import PrintProgress from './PrintProgress.svelte';
import PaymentPage from '$lib/common/ui/PaymentPage.svelte';
import { plFiles, plCopies } from '$lib/common/utils/pricing.util.js';
import NetworkGate from '$lib/common/ui/NetworkGate.svelte';
import { detectNetwork } from '$lib/common/services/network.service.js';
2026-09-08 17:38:13 +03:00
2026-09-17 03:50:12 +03:00
let { onBack = () => {} } = $props();
const store = createPrintStore();
let fileInput = $state(null);
2026-09-08 17:38:13 +03:00
2026-09-17 03:50:12 +03:00
function handleFileSelect(event) {
store.addFiles(event.target.files);
event.target.value = '';
}
2026-09-08 17:38:13 +03:00
2026-09-19 03:05:36 +03:00
const hasDuplex = $derived(
store.files.some((f) => f.sides === 'Двусторонняя')
);
const hasTierSmall = $derived(
store.files.some((f) => {
const s = f.selectedPages?.size || 0;
return f.sides !== 'Двусторонняя' && s > 0 && s < 25;
})
);
const hasTierMedium = $derived(
store.files.some((f) => {
2026-09-17 03:50:12 +03:00
const s = f.selectedPages?.size || 0;
2026-09-19 03:05:36 +03:00
return f.sides !== 'Двусторонняя' && s >= 25;
})
);
/* ── Проверка сети перед отправкой печати ──
* Печать работает только в локальной сети принтера (домен → 192.168.20.1).
* Если «Я оплатил(а)» нажата не в той сети — показываем экран подключения
* и автоматически отправляем печать, как только сеть появится.
*/
/** @type {null | 'checking' | 'blocked'} */
let printNetGate = $state(null);
async function handleConfirmPayment() {
if (printNetGate === 'checking') return;
printNetGate = 'checking';
const net = await detectNetwork();
if (net === 'local') {
printNetGate = null;
store.submitPrint();
} else {
printNetGate = 'blocked';
}
}
function handleGateReady() {
// сеть принтера появилась — пропускаем пользователя и сразу печатаем
printNetGate = null;
store.submitPrint();
}
function handleGateBack() {
printNetGate = null; // возврат к странице оплаты
}
2026-09-08 17:38:13 +03:00
</script>
2026-09-17 03:50:12 +03:00
{#if store.job}
<!-- Страница прогресса печати (после оплаты) -->
<PrintProgress {store} onBack={onBack} />
{:else if store.showPayment}
{#if printNetGate === 'blocked'}
<NetworkGate mode="need-local" onReady={handleGateReady} onBack={handleGateBack} />
{:else if printNetGate === 'checking'}
<div class="page-container">
<div class="net-checking">
<div class="net-spinner"></div>
<p>Проверка подключения к принтеру…</p>
</div>
</div>
{:else}
<PaymentPage
totalPrice={store.totalPrice}
filesCount={store.files.length}
onBack={store.hidePayment}
onConfirmPayment={handleConfirmPayment}
/>
{/if}
2026-09-17 03:50:12 +03:00
{:else if !store.activeFile}
<div class="page-container">
<header class="header">
<button class="back-btn" onclick={onBack}>←</button>
<h1>Печать</h1>
</header>
<main class="options">
{#if store.files.length === 0}
<PrintUI as="upload-zone" onSelect={() => fileInput.click()} />
{:else}
{#each store.files as file (file.id)}
<PrintUI
as="file-card"
{file}
onToggle={store.toggleExpand}
onRemove={store.removeFile}
onUpdate={store.updateFile}
onPreview={(f) => store.openPreview(f)}
onPageSelect={(f) => (f.fileType === 'pdf' ? store.openGallery(f) : store.openPreview(f))}
pagesLabel={store.pagesLabel}
/>
{/each}
<button class="add-file-btn" onclick={() => fileInput.click()}> Добавить файлы</button>
{/if}
</main>
<PrintUI
2026-09-19 03:05:36 +03:00
as="footer"
totalPrice={store.totalPrice}
filesCount={store.files.length}
extraCopies={store.extraCopies}
isPrinting={store.isPrinting}
onSubmit={store.openPayment}
hasDuplex={hasDuplex}
hasTierSmall={hasTierSmall}
hasTierMedium={hasTierMedium}
plFiles={plFiles}
plCopies={plCopies}
2026-09-17 03:50:12 +03:00
/>
<input
type="file"
bind:this={fileInput}
accept="image/*,.pdf,.doc,.docx,.odt,.xls,.xlsx,.ods,.ppt,.pptx,.odp"
multiple
onchange={handleFileSelect}
hidden
/>
</div>
2026-09-08 17:38:13 +03:00
{/if}
2026-09-17 03:50:12 +03:00
{#if !store.job}
<PreviewOverlay {store} />
2026-09-08 17:38:13 +03:00
{/if}
<style>
.net-checking { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px; padding: 40px 20px; color: #8b93a1; font-size: 15px; font-weight: 600; text-align: center; }
.net-spinner { width: 42px; height: 42px; border-radius: 50%; border: 3px solid rgba(255,255,255,0.12); border-top-color: #6366f1; animation: net-spin 0.8s linear infinite; }
@keyframes net-spin { to { transform: rotate(360deg); } }
2026-09-17 03:50:12 +03:00
.page-container { width: 100%; min-height: 100dvh; display: flex; flex-direction: column; background: radial-gradient(120% 60% at 50% -10%, rgba(99,102,241,0.18) 0%, transparent 60%); font-family: system-ui, -apple-system, sans-serif; color: #f5f7fa; }
.header { padding: 24px 20px 8px; display: flex; align-items: center; position: relative; }
.back-btn { background: none; border: none; color: #6366f1; font-size: 15px; font-weight: 600; cursor: pointer; padding: 8px 4px; margin-right: auto; min-height: 44px; }
.header h1 { margin: 0; font-size: 28px; font-weight: 700; position: absolute; left: 50%; transform: translateX(-50%); white-space: nowrap; }
.options { flex: 1; padding: 24px 20px 16px; display: flex; flex-direction: column; gap: 14px; }
.add-file-btn { align-self: center; background: none; border: 1px dashed rgba(255,255,255,0.18); color: #a9b0c0; font-size: 14px; font-weight: 600; padding: 12px 18px; border-radius: 14px; cursor: pointer; margin-top: 8px; }
2026-09-08 17:38:13 +03:00
</style>