fix reports

This commit is contained in:
2026-06-24 01:46:30 +03:30
parent 69da635247
commit 78ce8ad4b4
5 changed files with 295 additions and 101 deletions
+129 -76
View File
@@ -15,104 +15,157 @@
configReject = reject;
});
// API endpoint for config - you can change this URL
// You can also set it via a data attribute: <script src="/config-loader.js" data-config-url="https://your-api.com/config"></script>
//const scriptTag = document.currentScript || document.querySelector('script[src*="config-loader.js"]');
//const customUrl = scriptTag ? scriptTag.getAttribute('data-config-url') : null;
const CONFIG_URL = 'https://nerkh.gold/get_user' //customUrl || window.location.origin + '/api/config';
const VERSION_URL = 'https://abshode-service.montgit.ir/server/get_version';
const CONFIG_URLS = [
'https://nerkh.gold/get_user',
'https://abshode-service.montgit.ir/server/get_user'
];
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL';
// Timeout to ensure main.ts loads even if config fails (5 seconds)
const CONFIG_TIMEOUT = 5000;
// Timeout to ensure main.ts loads even if config fails
const CONFIG_TIMEOUT = 12000;
let configTimeoutId;
let mainScriptLoaded = false;
// Fetch config from server
async function loadConfig() {
function getProtocol() {
return window.location.protocol === 'https:' ? 'https:' : 'http:';
}
function sortConfigUrls(urls) {
const lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY);
return [...urls].sort((a, b) => {
if (a === lastSuccessUrl) return -1;
if (b === lastSuccessUrl) return 1;
return 0;
});
}
async function fetchJson(url, timeout = 3500) {
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(CONFIG_URL, {
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
Accept: 'application/json'
},
cache: 'no-cache'
cache: 'no-store',
signal: controller.signal
});
if (!response.ok) {
throw new Error('Failed to fetch config: ' + response.status);
throw new Error('Request failed: ' + response.status);
}
// Check if response is actually JSON
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
const text = await response.text();
throw new Error('Response is not JSON. Received: ' + text.substring(0, 100));
}
const config = await response.json();
return await response.json();
} finally {
window.clearTimeout(timeoutId);
}
}
// Store config in window object
window.__APP_CONFIG__ = config;
window.__CONFIG_READY__ = true;
if (config?.user?.name_fa) {
document.title = 'پنل مدیریت '+ '-'+config.user.name_fa;
}
// ---------- Dynamic Favicon ----------
if (config?.user?.avatar) {
const faviconUrl = config.user.avatar;
// remove existing favicon(s)
const existingIcons = document.querySelectorAll('link[rel*="icon"]');
existingIcons.forEach(icon => icon.remove());
// create new favicon
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/png'; // یا image/x-icon
link.href = faviconUrl + '?v=' + Date.now();
document.head.appendChild(link);
}
// Resolve the promise
if (configResolve) {
configResolve(config);
}
// Dispatch custom event to notify that config is ready
window.dispatchEvent(new CustomEvent('configReady', { detail: config }));
console.log('Config loaded successfully:', config);
// Clear timeout if config loaded successfully
if (configTimeoutId) {
clearTimeout(configTimeoutId);
configTimeoutId = null;
}
// Load main.ts after config is ready
loadMainScript();
async function loadVersion() {
try {
await fetchJson(VERSION_URL, 3500);
console.log('Version loaded from:', VERSION_URL);
} catch (error) {
console.warn('Config loader: Could not fetch config from', CONFIG_URL);
console.warn('Version check failed from', VERSION_URL, error);
}
}
async function fetchConfig() {
const protocol = getProtocol();
const urls = sortConfigUrls(
CONFIG_URLS.map((url) => url.replace(/^https?:/, protocol))
);
for (let index = 0; index < urls.length; index++) {
const url = urls[index];
const timeout = index === urls.length - 1 ? 5000 : 3500;
try {
const config = await fetchJson(url, timeout);
if (!config || typeof config !== 'object') {
throw new Error('Config response is invalid');
}
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url);
return { config, url };
} catch (error) {
console.warn('Config failed from', url, error);
}
}
throw new Error('Cannot load config from any server');
}
function applyConfig(config) {
window.__APP_CONFIG__ = config;
window.__CONFIG_READY__ = true;
if (config?.user?.name_fa) {
document.title = 'پنل مدیریت ' + '-' + config.user.name_fa;
}
if (config?.user?.avatar) {
const faviconUrl = config.user.avatar;
const existingIcons = document.querySelectorAll('link[rel*="icon"]');
existingIcons.forEach((icon) => icon.remove());
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/png';
link.href = faviconUrl + '?v=' + Date.now();
document.head.appendChild(link);
}
}
function finishConfigLoad(config, error) {
if (configResolve) {
configResolve(config);
}
if (error) {
window.dispatchEvent(new CustomEvent('configError', { detail: error }));
} else {
window.dispatchEvent(new CustomEvent('configReady', { detail: config }));
}
if (configTimeoutId) {
clearTimeout(configTimeoutId);
configTimeoutId = null;
}
loadMainScript();
}
// Fetch config from server
async function loadConfig() {
try {
await loadVersion();
const { config, url } = await fetchConfig();
applyConfig(config);
console.log('Config loaded successfully from', url, config);
finishConfigLoad(config);
} catch (error) {
console.warn('Config loader: Could not fetch config from any endpoint');
console.warn('Error details:', error.message);
console.warn('This is normal if the endpoint does not exist yet. App will continue with empty config.');
// Set default empty config on error
console.warn('App will continue with empty config.');
window.__APP_CONFIG__ = {};
window.__CONFIG_READY__ = true;
// Resolve the promise even on error (with empty config)
if (configResolve) {
configResolve({});
}
window.dispatchEvent(new CustomEvent('configError', { detail: error }));
// Clear timeout if error occurred
if (configTimeoutId) {
clearTimeout(configTimeoutId);
configTimeoutId = null;
}
// Load main.ts even on error (with empty config)
loadMainScript();
finishConfigLoad({}, error);
}
}
@@ -62,15 +62,28 @@
{{ row.sts_str || '-' }}
</span>
<div class="flex flex-wrap items-center gap-2">
<span
class="inline-flex rounded-lg border px-2 py-0.5 text-xs font-600"
:class="typeTagClass(row)"
>
{{ typeLabel(row) }}
</span>
</div>
<div class="space-y-2">
<MobileStatCard bordered variant="danger" label="مقدار (ریال)" :value="usePrice(row.cash ?? 0, true, '')">
<MobileStatCard
bordered
:variant="amountVariant(row)"
label="مقدار"
:value="amountLabel(row)"
>
<template #prefix>
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#dc2626" />
</template>
</MobileStatCard>
<MobileStatCard bordered variant="warning" :label="metalLabel.goldUnit()" :value="goldLabel(row)">
<template #prefix>
<BaseIcon icon="vuesax-linear:coin" size="18" color="#b45309" />
<BaseIcon
:icon="isMoneyWithdraw(row) ? 'vuesax-linear:empty-wallet' : 'vuesax-linear:coin'"
size="18"
:color="amountIconColor(row)"
/>
</template>
</MobileStatCard>
</div>
@@ -104,10 +117,14 @@ import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue'
import MobileSheetHeader from '@/components/mobile/MobileSheetHeader.vue'
import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
import { status } from '@/pages/menuPages/withdraw/information/hooks/status'
import {
formatWithdrawAmountDisplay,
isMoneyWithdraw,
withdrawAmountValue,
withdrawTypeLabel
} from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
import { isMobile } from '@core/utils/is'
import { toJalali } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import { metalLabel } from '@/utils/adminMetalLabels'
import { toNumber } from 'lodash'
import { useRouter } from 'vue-router'
@@ -155,8 +172,36 @@ function noteLabel(row: Withdraw) {
return String(row.note)
}
function goldLabel(row: Withdraw) {
if (row.gold == null) return '-'
return String(row.gold)
function typeLabel(row: Withdraw) {
return withdrawTypeLabel(row)
}
function typeTagClass(row: Withdraw) {
if (!isMoneyWithdraw(row)) {
return 'border-amber-300 bg-amber-50 text-amber-700'
}
return withdrawAmountValue(row) > 0
? 'border-green-300 bg-green-50 text-green-700'
: withdrawAmountValue(row) < 0
? 'border-red-300 bg-red-50 text-red-700'
: 'border-gray-300 bg-gray-50 text-gray-700'
}
function amountLabel(row: Withdraw) {
return formatWithdrawAmountDisplay(row)
}
function amountVariant(row: Withdraw) {
const value = withdrawAmountValue(row)
if (value > 0) return 'success'
if (value < 0) return 'danger'
return 'warning'
}
function amountIconColor(row: Withdraw) {
const value = withdrawAmountValue(row)
if (value > 0) return '#15803d'
if (value < 0) return '#dc2626'
return '#b45309'
}
</script>
@@ -8,7 +8,7 @@
<div class="flex items-center justify-between gap-2 text-xs">
<span class="truncate text-gray-600">{{ noteLabel }}</span>
<span class="shrink-0 font-700 text-red-600">{{ cashLabel }}</span>
<span class="shrink-0 font-700" :class="amountClass">{{ amountLabel }}</span>
</div>
</div>
</MobileCard>
@@ -18,7 +18,10 @@
import type { Withdraw } from '@/model/wallet'
import MobileCard from '@/components/mobile/MobileCard.vue'
import { toJalaliNoTime } from '@core/utils/useDataTime'
import { usePrice } from '@core/utils/usePrice'
import {
formatWithdrawAmountDisplay,
withdrawAmountValue
} from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
const props = defineProps<{
item: Withdraw
@@ -35,5 +38,11 @@ const noteLabel = computed(() => {
const customerName = computed(() => props.item.customer?.name || '-')
const dateLabel = computed(() => (props.item.created_at ? toJalaliNoTime(props.item.created_at) : '-'))
const cashLabel = computed(() => usePrice(props.item.cash ?? 0, true, ''))
const amountLabel = computed(() => formatWithdrawAmountDisplay(props.item))
const amountClass = computed(() => {
const value = withdrawAmountValue(props.item)
if (value > 0) return 'text-green-600'
if (value < 0) return 'text-red-600'
return 'text-gray-600'
})
</script>
@@ -1,9 +1,13 @@
import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalali, toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import { usePrice } from '@core/utils/usePrice'
import WithdrawStatus from "@/pages/menuPages/withdraw/components/WithdrawStatus.vue";
import { metalLabel } from '@/utils/adminMetalLabels'
import {
formatWithdrawAmountDisplay,
isMoneyWithdraw,
withdrawAmountColorClass,
withdrawTypeLabel
} from '@/pages/menuPages/withdraw/utils/withdrawDisplay'
/*import BudgetSelect from '@/components/BudgetSelect.vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { FilterSelect } from '@/model/filterSelect'
@@ -71,23 +75,43 @@ export function getWithdrawColumns(): BaseTableColumn[] {
type: 'Text'
},
{
title: 'مقدار (ریال)',
dataIndex: 'cash',
key: 'cash',
type: 'Number',
title: 'نوع',
dataIndex: 'asset_type',
key: 'asset_type',
filterable: false,
sortable: false,
customRender: ({ record }) => {
const label = withdrawTypeLabel(record)
if (label === '-') {
return <div class="flex justify-center px-6">-</div>
}
return (
<div class="flex justify-center">
<div class={'font-bold px-6 text-red'}>{usePrice(record.cash ?? 0, true, '')}</div>
<CustomTag
textColor={isMoneyWithdraw(record) ? '#25855A' : '#ca8a04'}
bgColor={isMoneyWithdraw(record) ? '#E3F4E9' : '#fef3c7'}
class="font-bold px-6"
>
{label}
</CustomTag>
</div>
)
}
},
{
title: metalLabel.goldUnit(),
dataIndex: 'gold',
key: 'gold',
title: 'مقدار',
dataIndex: 'amount',
key: 'amount',
type: 'Number',
customRender: ({ record }) => {
const colorClass = withdrawAmountColorClass(record)
return (
<div class="flex justify-center">
<div class={`font-bold px-6 ${colorClass}`}>{formatWithdrawAmountDisplay(record)}</div>
</div>
)
}
},
{
title: 'شماره کارت',
@@ -0,0 +1,63 @@
import type { Withdraw } from '@/model/wallet'
import { metalLabel } from '@/utils/adminMetalLabels'
import { getMainMetal } from '@/utils/metalLabels'
import {
formatMetalDisplayValue,
METAL_ID_COPPER,
METAL_ID_GOLD,
METAL_ID_SILVER,
metalDisplayUnit
} from '@/utils/metalWeight'
import { usePrice } from '@core/utils/usePrice'
export function resolveWithdrawMetalId(): number {
const main = getMainMetal()
if (main.includes('مس')) return METAL_ID_COPPER
if (main.includes('نقره')) return METAL_ID_SILVER
return METAL_ID_GOLD
}
export function isMoneyWithdraw(record: Pick<Withdraw, 'cash' | 'gold'>): boolean {
const gold = Math.abs(Number(record.gold ?? 0))
if (gold > 0) return false
return true
}
export function withdrawTypeLabel(record: Pick<Withdraw, 'cash' | 'gold'>): string {
if (!isMoneyWithdraw(record)) {
return metalLabel.goldUnit()
}
const cash = Math.abs(Number(record.cash ?? 0))
return cash > 0 ? 'پول' : '-'
}
export function withdrawAmountValue(record: Pick<Withdraw, 'cash' | 'gold'>): number {
if (isMoneyWithdraw(record)) {
return Number(record.cash ?? 0)
}
return Number(record.gold ?? 0)
}
export function formatWithdrawAmountDisplay(record: Pick<Withdraw, 'cash' | 'gold'>): string {
const value = withdrawAmountValue(record)
if (isMoneyWithdraw(record)) {
if (!value) return `0 ریال`
const formatted = usePrice(Math.abs(value), true, '')
return value > 0 ? `${formatted} ریال` : `-${formatted} ریال`
}
const metalId = resolveWithdrawMetalId()
const unit = metalDisplayUnit(metalId)
if (!value) return `0 ${unit}`
const formatted = formatMetalDisplayValue(Math.abs(value), metalId)
return value > 0 ? formatted : `-${formatted}`
}
export function withdrawAmountColorClass(record: Pick<Withdraw, 'cash' | 'gold'>): string {
const value = withdrawAmountValue(record)
if (!value) return ''
return value > 0 ? 'text-green' : 'text-red'
}