diff --git a/src/lib/common/utils/pricing.util.js b/src/lib/common/utils/pricing.util.js index f7e5085..16503f4 100644 --- a/src/lib/common/utils/pricing.util.js +++ b/src/lib/common/utils/pricing.util.js @@ -14,12 +14,34 @@ export const COLOR_MODES = [ export const FORMATS = ['A4', 'A5']; /** - * Расчет цены за страницу в зависимости от объема + * Возвращает количество физических листов. + * Для двусторонней печати: страницы / 2, округлённые вверх. */ -export function getPricePerPage(pagesCount) { - if (pagesCount >= 1000) return 4; - if (pagesCount >= 25) return 9; - return 10; +export function getSheetsCount(pagesCount, isDuplex = false) { + const pages = Math.max(0, Number(pagesCount) || 0); + + if (!isDuplex) { + return pages; + } + + return Math.ceil(pages / 2); +} + +/** + * Расчет цены за лист. + * Для двусторонней печати фиксированно 15 ₽/лист. + * Для односторонней остаются старые тарифы. + */ +export function getPricePerPage(pagesOrSheetsCount, sides = 'Односторонняя') { + if (sides === 'Двусторонняя') { + return 15; + } + + const n = Math.max(0, Number(pagesOrSheetsCount) || 0); + + if (n >= 25) return 9; + + return 10; } /** diff --git a/src/lib/pages/main/MainMenu.svelte b/src/lib/pages/main/MainMenu.svelte index f290b67..ec6b923 100644 --- a/src/lib/pages/main/MainMenu.svelte +++ b/src/lib/pages/main/MainMenu.svelte @@ -205,5 +205,5 @@
v{backendVersion} - v1.0.5 + v1.0.7
diff --git a/src/lib/pages/print/PrintMenu.svelte b/src/lib/pages/print/PrintMenu.svelte index 5daff6e..df0055a 100644 --- a/src/lib/pages/print/PrintMenu.svelte +++ b/src/lib/pages/print/PrintMenu.svelte @@ -16,12 +16,23 @@ event.target.value = ''; } - const hasTierSmall = $derived(store.files.some((f) => (f.selectedPages?.size || 0) < 25)); - const hasTierMedium = $derived(store.files.some((f) => { + const hasDuplex = $derived( + store.files.some((f) => f.sides === 'Двусторонняя') + ); + + const hasTierSmall = $derived( + store.files.some((f) => { const s = f.selectedPages?.size || 0; - return s >= 25 && s < 1000; - })); - const hasTierLarge = $derived(store.files.some((f) => (f.selectedPages?.size || 0) >= 1000)); + return f.sides !== 'Двусторонняя' && s > 0 && s < 25; + }) + ); + + const hasTierMedium = $derived( + store.files.some((f) => { + const s = f.selectedPages?.size || 0; + return f.sides !== 'Двусторонняя' && s >= 25; + }) + ); {#if store.job} @@ -60,17 +71,17 @@ {/if} { if (!store.job) return 'idle'; - const f = store.job.files.find(f => f.status === 'printing'); + + const f = store.job.files?.find((x) => x.status === 'printing'); if (f) return 'printing'; - const q = store.job.files.find(f => f.status === 'queued'); + + const q = store.job.files?.find((x) => x.status === 'queued'); return q ? 'queued' : store.job.phase; }); @@ -36,7 +38,121 @@ }); const canCancel = $derived( - store.job?.files.some(f => f.status === 'queued' || f.status === 'printing') + store.job?.files?.some((f) => f.status === 'queued' || f.status === 'printing') ?? false + ); + + const jobPhase = $derived(store.job?.phase); + const jobFiles = $derived(store.job?.files ?? []); + + /** + * Двусторонняя печать определяется по настройкам файлов, + * которые всё ещё лежат в store.files после отправки задания. + */ + const isDuplex = $derived( + store.files.some((f) => f.sides === 'Двусторонняя') + ); + + /** + * Текущие запросы сервера на пользовательские действия. + */ + const hasClearPhase = $derived(jobPhase === 'awaiting_clear_output'); + const hasFlip = $derived( + jobPhase === 'awaiting_flip' || + jobFiles.some((f) => f.status === 'awaiting_flip') + ); + const hasPickup = $derived( + jobPhase === 'awaiting_pickup' || + jobFiles.some((f) => f.status === 'awaiting_pickup') + ); + + /** + * Проверяем, есть ли уже напечатанные или отправленные файлы. + * Если нет — значит это первая печать и лоток выдачи принтера пуст. + */ + const hasPrintedFiles = $derived( + store.job?.files?.some(f => ['done', 'sent', 'awaiting_flip', 'awaiting_pickup'].includes(f.status)) ?? false + ); + + /** + * Нам важно запомнить, что первый проход уже был, + * иначе при печати второй стороны инструкция очистки может появиться снова. + */ + let hadFlip = $state(false); + let hadPickup = $state(false); + + $effect(() => { + if (!store.job) return; + + if ( + jobPhase === 'awaiting_flip' || + jobFiles.some((f) => f.status === 'awaiting_flip') + ) { + hadFlip = true; + } + + if ( + jobPhase === 'awaiting_pickup' || + jobFiles.some((f) => f.status === 'awaiting_pickup') + ) { + hadPickup = true; + } + }); + + const firstSideFinished = $derived( + hadFlip || + hadPickup || + jobPhase === 'finished' || + jobPhase === 'cancelled' || + jobFiles.some((f) => f.status === 'done') + ); + + /** + * Инструкция очистки лотка: + * - если сервер явно просит awaiting_clear_output И есть уже напечатанные файлы; + * - для двусторонней печати — до первого прохода, + * пока ещё не было flip/pickup/done И есть напечатанные файлы. + */ + const showClearOutput = $derived( + store.jobActive && + !firstSideFinished && + ( + (hasClearPhase && hasPrintedFiles) || + (isDuplex && hasPrintedFiles && ['waiting_start', 'printing', 'awaiting_clear_output'].includes(jobPhase ?? '')) + ) + ); + + const showFlip = $derived( + store.jobActive && + hasFlip && + jobPhase !== 'finished' && + jobPhase !== 'cancelled' + ); + + const showPickup = $derived( + store.jobActive && + hasPickup && + !showFlip && + jobPhase !== 'finished' && + jobPhase !== 'cancelled' + ); + + const clearActionAvailable = $derived( + showClearOutput && + (jobPhase === 'awaiting_clear_output' || jobPhase === 'waiting_start') + ); + + const flipActionAvailable = $derived( + showFlip && jobPhase === 'awaiting_flip' + ); + + const pickupActionAvailable = $derived( + showPickup && jobPhase === 'awaiting_pickup' + ); + + const clearButtonLabel = $derived( + jobPhase === 'waiting_start' + ? 'Убрал листы — начать печать' + : 'Убрал листы — продолжать' ); const HOLD_MS = 1500; @@ -46,14 +162,23 @@ function startHold(e) { if (!store.jobActive || !canCancel) return; + e.preventDefault(); e.currentTarget.setPointerCapture?.(e.pointerId); + t0 = performance.now(); + const step = (now) => { hold = Math.min(1, (now - t0) / HOLD_MS); - if (hold >= 1) { raf = 0; hold = 0; store.cancelJob(); return; } + if (hold >= 1) { + raf = 0; + hold = 0; + store.cancelJob(); + return; + } raf = requestAnimationFrame(step); }; + raf = requestAnimationFrame(step); } @@ -76,6 +201,50 @@
+ {#if showClearOutput} +
+

+ Уберите распечатанные листы из выходного лотка +

+
    +
  1. Уберите распечатанные листы из выходного лотка
  2. + {#if clearActionAvailable} +
  3. Нажмите кнопку ниже для продолжения
  4. + {:else} +
  5. Дождитесь завершения первого прохода
  6. + {/if} +
+
+ {:else if showFlip} +
+

+ Переложите бумагу +

+
    +
  1. Не переворачивая, положите листы в слот ручной подачи
  2. + {#if flipActionAvailable} +
  3. Нажмите кнопку ниже для печати обратной стороны
  4. + {:else} +
  5. Дождитесь завершения текущего этапа
  6. + {/if} +
+
+ {:else if showPickup} +
+

+ Печать завершается... +

+
    +
  1. Дождитесь завершения печати
  2. + {#if pickupActionAvailable} +
  3. Нажмите кнопку ниже для подтверждения
  4. + {:else} +
  5. Дождитесь завершения текущего этапа
  6. + {/if} +
+
+ {/if} + {#if store.job} {#each store.job.files as f, i (i + '-' + f.name)} {@const b = badge(f.name)} @@ -93,23 +262,6 @@ - {#if f.status === 'awaiting_pickup'} -
    -
  1. Дождитесь завершения печати
  2. -
  3. Нажмите кнопку ниже для подтверждения
  4. -
- {:else if f.status === 'awaiting_clear_output'} -
    -
  1. Уберите распечатанные листы из выходного лотка
  2. -
  3. Нажмите кнопку ниже для продолжения
  4. -
- {:else if f.status === 'awaiting_flip'} -
    -
  1. Не переворачивая, положите листы в слот ручной подачи
  2. -
  3. Нажмите кнопку ниже для печати обратной стороны
  4. -
- {/if} - {#if f.error}

{f.error}

{/if} @@ -119,32 +271,34 @@