feat(apps): version on every build, a «what changed» modal, and a clean update

- The build number is the commit count, so every build raises it with no
  file to bump. scripts/app-version.mjs bakes it into each front-end
  (__APP_BUILD__) and writes dist/version.json with that app's notes.
- Notes come from commit messages: only Persian lines that start with
  one of the four app tags are shown, each to its own app
  (docs/RELEASE-NOTES.md).
- Both customer apps and the admin panel poll version.json (every 5
  minutes and on return to the tab, bypassing caches). A newer build opens
  a modal that cannot be dismissed. «به‌روزرسانی» unregisters the service
  worker, empties Cache Storage and reloads; localStorage (sign-in, cart,
  settings) is kept.
- Replaces legacy's version dialog, which compared against a
  package.json version that was never bumped and so never opened, and
  useRefresh, which swapped in a new service worker silently.
- nginx (all three apps and nginx-spa.conf): index.html, sw.js and
  version.json are never cached. sw.js used to fall under the 6-month rule,
  which could pin a browser to an old release.

Checked: served with the real nginx config, a newer version.json opens
the modal with its notes; the same build does not.

اپ: اجرت هر کالا حالا همان است که در فاکتور حساب می‌شود؛ درصد خود شما و تخفیف‌ها هم در آن آمده است.
اپ: علاقه‌مندی‌های شما ذخیره می‌شوند و در هر دستگاهی که وارد شوید دیده می‌شوند.
اپ: از این به بعد هر نسخه‌ی تازه با فهرست تغییراتش اعلام می‌شود.
پنل: بخش تازه‌ی «اجرت»: فرمول اجرت را با یک نمونه امتحان و بعد تأیید کنید.
پنل: شهرها مثل قبل به‌صورت درخت‌اند (کشور، استان، شهر، محله)؛ شهری که درصد ندارد درصد شهر بالاتر را می‌گیرد.
پنل: «تاریخ تولد» به فرم مشتری عمده و تکی برگشت.
پنل: در گزارش تجمیعی سفارش‌ها، دکمه‌ی «دریافت فایل اکسل» اضافه شد.
پنل: رد احراز هویت مشتری حالا دلیل هم می‌گیرد.
پنل: با تأیید یا رد سند مالی، به مشتری پیامک می‌رود؛ در پیامک گروهی، %name با نام مشتری عوض می‌شود.
پنل: از این به بعد هر نسخه‌ی تازه‌ی پنل با فهرست تغییراتش اعلام می‌شود.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-10 12:45:52 +03:30
co-authored by Claude Opus 5.5
parent 2bbd9408dd
commit dbead8a6ba
21 changed files with 816 additions and 126 deletions
@@ -0,0 +1,117 @@
<template>
<!--
The update modal. It cannot be dismissed: the only way out is the update,
so every customer runs the release that is live (owner ruling 2026-10-10).
-->
<Teleport to="body">
<div v-if="available" class="update-gate" role="dialog" aria-modal="true" aria-labelledby="update-gate-title">
<div class="update-gate__card">
<div class="update-gate__badge">نسخه‌ی {{ available.version }}</div>
<h2 id="update-gate-title" class="update-gate__title">نسخه‌ی تازه آماده است</h2>
<div class="update-gate__body">
<template v-if="changes.length">
<p class="update-gate__lead">در این نسخه:</p>
<ul class="update-gate__list">
<template v-for="c in changes" :key="c.build">
<li v-for="(n, i) in c.notes" :key="c.build + '-' + i">{{ n }}</li>
</template>
</ul>
</template>
<p v-else class="update-gate__lead">بهبودها و رفع اشکال‌ها.</p>
</div>
<button type="button" class="update-gate__button" :disabled="updating" @click="applyUpdate">
{{ updating ? 'در حال به‌روزرسانی…' : 'به‌روزرسانی' }}
</button>
<p class="update-gate__hint">ورود و سبد خرید شما سر جایش می‌ماند.</p>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { useAppUpdate } from '../composables/useAppUpdate'
const { available, changes, updating, applyUpdate } = useAppUpdate()
</script>
<style scoped>
.update-gate {
position: fixed;
inset: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: rgba(0, 0, 0, 0.55);
direction: rtl;
}
.update-gate__card {
width: 100%;
max-width: 380px;
max-height: calc(100vh - 32px);
display: flex;
flex-direction: column;
gap: 12px;
padding: 22px 20px 18px;
border-radius: 18px;
background: var(--c-canvas, #fff);
color: var(--c-ink, #1f1f1f);
border: 1px solid var(--c-hairline, transparent);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
text-align: right;
}
.update-gate__badge {
align-self: flex-start;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
background: color-mix(in srgb, var(--c-primary, #b8860b) 14%, transparent);
color: var(--c-primary, #b8860b);
}
.update-gate__title {
margin: 0;
font-size: 18px;
font-weight: 700;
}
.update-gate__body {
overflow-y: auto;
min-height: 0;
}
.update-gate__lead {
margin: 0 0 6px;
font-size: 14px;
color: var(--c-inkMuted, inherit);
}
.update-gate__list {
margin: 0;
padding-inline-start: 18px;
list-style: disc;
font-size: 14px;
line-height: 1.9;
}
.update-gate__button {
width: 100%;
padding: 12px;
border: 0;
border-radius: 12px;
font: inherit;
font-size: 15px;
font-weight: 700;
color: #fff;
background: var(--c-primary, #b8860b);
cursor: pointer;
}
.update-gate__button:disabled {
opacity: 0.7;
cursor: progress;
}
.update-gate__hint {
margin: 0;
font-size: 12px;
text-align: center;
color: var(--c-inkMuted, inherit);
}
</style>