Initial commit: Hedena Gold monorepo.
First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// The update gate (scripts/app-version.mjs builds the other half).
|
||||
//
|
||||
// The bundle knows its own build number (__APP_BUILD__). Every few minutes,
|
||||
// and whenever the app comes back to the foreground, it reads version.json
|
||||
// from the server, bypassing every cache. A newer build there means a release
|
||||
// went out: the modal lists what changed since the build this tab is running,
|
||||
// and «بهروزرسانی» drops the service worker and every Cache Storage entry,
|
||||
// then reloads — so the next load is the new release, not a precached copy.
|
||||
// localStorage is left alone: the sign-in and settings survive the update.
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
declare const __APP_BUILD__: number
|
||||
declare const __APP_VERSION__: string
|
||||
|
||||
export interface VersionInfo {
|
||||
build: number
|
||||
version: string
|
||||
history: { build: number; date: string; notes: string[] }[]
|
||||
}
|
||||
|
||||
const CHECK_EVERY_MS = 5 * 60 * 1000
|
||||
|
||||
export const runningBuild: number = typeof __APP_BUILD__ === 'number' ? __APP_BUILD__ : 0
|
||||
export const runningVersion: string = typeof __APP_VERSION__ === 'string' ? __APP_VERSION__ : ''
|
||||
|
||||
export function useAppUpdate() {
|
||||
const available = ref<VersionInfo | null>(null)
|
||||
const updating = ref(false)
|
||||
|
||||
/** What changed between the running build and the server's, newest first. */
|
||||
const changes = computed(() => {
|
||||
const info = available.value
|
||||
if (!info) return []
|
||||
return (info.history ?? []).filter((h) => h.build > runningBuild && h.build <= info.build)
|
||||
})
|
||||
|
||||
async function check() {
|
||||
if (import.meta.env.DEV || !runningBuild || available.value) return
|
||||
try {
|
||||
const res = await fetch(`${import.meta.env.BASE_URL}version.json?t=${Date.now()}`, { cache: 'no-store' })
|
||||
if (!res.ok) return
|
||||
const info = (await res.json()) as VersionInfo
|
||||
if (typeof info.build === 'number' && info.build > runningBuild) available.value = info
|
||||
} catch {
|
||||
// Offline or a deploy in progress: try again on the next tick.
|
||||
}
|
||||
}
|
||||
|
||||
async function applyUpdate() {
|
||||
updating.value = true
|
||||
try {
|
||||
const regs = await navigator.serviceWorker?.getRegistrations?.()
|
||||
if (regs) await Promise.all(regs.map((r) => r.unregister()))
|
||||
} catch {
|
||||
/* no service worker */
|
||||
}
|
||||
try {
|
||||
if ('caches' in window) {
|
||||
const keys = await caches.keys()
|
||||
await Promise.all(keys.map((k) => caches.delete(k)))
|
||||
}
|
||||
} catch {
|
||||
/* Cache Storage unavailable */
|
||||
}
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
let timer: ReturnType<typeof setInterval> | undefined
|
||||
const onVisible = () => {
|
||||
if (document.visibilityState === 'visible') check()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
check()
|
||||
timer = setInterval(check, CHECK_EVERY_MS)
|
||||
document.addEventListener('visibilitychange', onVisible)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
document.removeEventListener('visibilitychange', onVisible)
|
||||
})
|
||||
|
||||
return { available, changes, updating, applyUpdate, check }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Shared "delete a row, refresh the list, show the server's refusal reason"
|
||||
// handler. This exact try/catch body — api.delete, then either a success
|
||||
// toast + refetch, or Modal.error with the server's message — was copy-pasted
|
||||
// verbatim in 6 list views (city/cityList.vue, user/ListUser.vue,
|
||||
// apps/category/List.vue, finical/list.vue, apps/product/ProductList.vue,
|
||||
// manage/Const.vue). Centralised here as a composable rather than a component
|
||||
// since it is pure behaviour with no template.
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
import api from '../api/client';
|
||||
|
||||
export function useDeleteHandler(options: {
|
||||
/** Called after a confirmed, successful delete — typically `table.value?.fetch()`. */
|
||||
onSuccess: () => void | Promise<void>;
|
||||
successMessage?: string;
|
||||
/** Some call sites used message.info rather than message.success; preserved
|
||||
* per-site rather than silently unified. */
|
||||
successType?: 'success' | 'info';
|
||||
/** Shown when the server didn't return its own message. */
|
||||
fallbackErrorMessage?: string;
|
||||
}) {
|
||||
const {
|
||||
onSuccess,
|
||||
successMessage = 'با موفقیت حذف شد',
|
||||
successType = 'success',
|
||||
fallbackErrorMessage = 'خطا در حذف',
|
||||
} = options;
|
||||
|
||||
async function remove(url: string, opts?: { method?: 'delete' | 'post' }) {
|
||||
try {
|
||||
// ProductList's delete is a legacy irregular endpoint (POST, not
|
||||
// DELETE) — see docs/ADMIN-PORT-PLAN.md's endpoint-mapping notes.
|
||||
// `method` lets that call site keep using this composable too.
|
||||
if (opts?.method === 'post') {
|
||||
await api.post(url);
|
||||
} else {
|
||||
await api.delete(url);
|
||||
}
|
||||
message[successType](successMessage);
|
||||
await onSuccess();
|
||||
} catch (e: any) {
|
||||
// The backend refuses deletion while other records still reference the
|
||||
// row (e.g. a city with customers/branches, a brand with products).
|
||||
Modal.error({
|
||||
title: e?.response?.data?.message ?? fallbackErrorMessage,
|
||||
okText: 'بستن',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { remove };
|
||||
}
|
||||
Reference in New Issue
Block a user