feat: accounting integration (D27), SMS relay driver (D26), rebuild user apps
- Add Tahesab/Zarrin accounting sync module with admin panel UI - Add SMS relay driver as default provider, refactor SMS into driver pattern - Update customer services, orders, products, and catalog queries - Rebuild user-app and user-app-v2 dist assets - Add Prisma migration for accounting tables - Update docker-compose and env examples - Update parity docs (DECISIONS, ISSUES, PROGRESS) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a72fa607a5
commit
19d490e790
@@ -37,6 +37,10 @@
|
||||
|
||||
<Spinner :spinning="loading && !rows.length" tip="درحال دریافت اطلاعات">
|
||||
<PullRefresh v-model="refreshing" @refresh="onRefresh">
|
||||
<template #pulling><Loading size="18" /></template>
|
||||
<template #loosing><Loading size="18" /></template>
|
||||
<template #loading><Loading size="18" /></template>
|
||||
<template #success><span /></template>
|
||||
<container class="cho__body">
|
||||
<template v-if="rows.length">
|
||||
<div class="cho__list">
|
||||
@@ -142,7 +146,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { Drawer } from 'ant-design-vue'
|
||||
import { PullRefresh } from 'vant'
|
||||
import { Loading, PullRefresh } from 'vant'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import Spinner from '../../components/Spinner.vue'
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
|
||||
<Spinner :spinning="loading && !rows.length" tip="درحال دریافت اطلاعات">
|
||||
<PullRefresh v-model="refreshing" @refresh="onRefresh">
|
||||
<template #pulling><Loading size="18" /></template>
|
||||
<template #loosing><Loading size="18" /></template>
|
||||
<template #loading><Loading size="18" /></template>
|
||||
<template #success><span /></template>
|
||||
<container class="cch__body">
|
||||
<div v-if="rows.length" class="cch__list">
|
||||
<article
|
||||
@@ -132,7 +136,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { Drawer } from 'ant-design-vue'
|
||||
import { PullRefresh } from 'vant'
|
||||
import { Loading, PullRefresh } from 'vant'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import Spinner from '../../components/Spinner.vue'
|
||||
|
||||
@@ -53,6 +53,10 @@
|
||||
|
||||
<Spinner :spinning="loading && !rows.length && !details.length" tip="درحال دریافت اطلاعات">
|
||||
<PullRefresh v-model="refreshing" @refresh="onRefresh">
|
||||
<template #pulling><Loading size="18" /></template>
|
||||
<template #loosing><Loading size="18" /></template>
|
||||
<template #loading><Loading size="18" /></template>
|
||||
<template #success><span /></template>
|
||||
<container class="rep__body">
|
||||
<!-- ── کلیات حساب ─────────────────────────────────────────── -->
|
||||
<template v-if="activeKey === '1'">
|
||||
@@ -147,7 +151,7 @@
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Drawer } from 'ant-design-vue'
|
||||
import { PullRefresh } from 'vant'
|
||||
import { Loading, PullRefresh } from 'vant'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import Spinner from '../../components/Spinner.vue'
|
||||
|
||||
@@ -245,7 +245,15 @@ function logOut() {
|
||||
|
||||
async function loadDashboard() {
|
||||
if (session.isLoggedIn) {
|
||||
customer.value = await api.getMe()
|
||||
// Not wrapped before: a failure here (expired token, network blip) left
|
||||
// the exception uncaught, so onRefresh's refreshing.value = false never
|
||||
// ran and PullRefresh stayed stuck in its loading state — a working pull
|
||||
// gesture never dismissed it again until a full page reload.
|
||||
try {
|
||||
customer.value = await api.getMe()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
try {
|
||||
goldPrice.value = await api.getLatestGoldPrice()
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
|
||||
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات">
|
||||
<PullRefresh v-model="refreshing" @refresh="onRefresh">
|
||||
<template #pulling><Loading size="18" /></template>
|
||||
<template #loosing><Loading size="18" /></template>
|
||||
<template #loading><Loading size="18" /></template>
|
||||
<template #success><span /></template>
|
||||
<container class="ord__body">
|
||||
<template v-if="filteredOrders.length">
|
||||
<div class="ord__list">
|
||||
@@ -145,7 +149,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Drawer } from 'ant-design-vue'
|
||||
import { PullRefresh } from 'vant'
|
||||
import { Loading, PullRefresh } from 'vant'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import Spinner from '../../components/Spinner.vue'
|
||||
|
||||
@@ -56,6 +56,25 @@
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<!-- D27 §12: the balance in the accounting software, re-read on every visit. -->
|
||||
<section v-if="balance && balance.source !== 'none'" class="prof__block">
|
||||
<SectionHead title="ماندهی حساب" :subtitle="balanceSubtitle" />
|
||||
<dl class="prof__specs">
|
||||
<div class="prof__spec">
|
||||
<dt>ماندهی طلا</dt>
|
||||
<dd :class="sideClass(balance.gold)">{{ grams(balance.gold) }} <small>{{ sideLabel(balance.gold) }}</small></dd>
|
||||
</div>
|
||||
<div class="prof__spec">
|
||||
<dt>ماندهی ریالی</dt>
|
||||
<dd :class="sideClass(balance.rial)">{{ rials(balance.rial) }} <small>{{ sideLabel(balance.rial) }}</small></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
<section v-else-if="balanceLoading" class="prof__block">
|
||||
<SectionHead title="ماندهی حساب" subtitle="در حال دریافت از حسابداری…" />
|
||||
<div class="prof__specs prof__skeleton" />
|
||||
</section>
|
||||
|
||||
<section v-else-if="!loading" class="prof__empty">
|
||||
<Icon icon="vuesax-linear:user" size="26" />
|
||||
<p>اطلاعات حساب در دسترس نیست.</p>
|
||||
@@ -85,7 +104,7 @@ import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import SectionHead from '../../components/home/SectionHead.vue'
|
||||
import { useSessionStore } from '../../stores/session'
|
||||
import { api, type CustomerDetail } from '../../api/client'
|
||||
import { api, type AccountingBalance, type CustomerDetail } from '../../api/client'
|
||||
|
||||
const VERIFICATION_LABELS: Record<string, string> = {
|
||||
UNVERIFIED: 'احراز نشده',
|
||||
@@ -112,8 +131,38 @@ const canCompleteProfile = computed(() => {
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// D27 §12 — fetched on its own, every time the page opens, so a slow
|
||||
// accounting server never holds up the profile itself.
|
||||
const balance = ref<AccountingBalance | null>(null)
|
||||
const balanceLoading = ref(false)
|
||||
|
||||
const num = (v: string | null) => Number(v ?? 0)
|
||||
const sideLabel = (v: string | null) => (num(v) > 0 ? 'بدهکار' : num(v) < 0 ? 'بستانکار' : '')
|
||||
const sideClass = (v: string | null) => (num(v) > 0 ? 'is-debt' : num(v) < 0 ? 'is-credit' : '')
|
||||
const grams = (v: string | null) =>
|
||||
`${Math.abs(num(v)).toLocaleString('fa-IR', { maximumFractionDigits: 3 })} گرم`
|
||||
const rials = (v: string | null) => `${Math.abs(num(v)).toLocaleString('fa-IR')} ریال`
|
||||
const balanceSubtitle = computed(() => {
|
||||
const b = balance.value
|
||||
if (!b?.at) return ''
|
||||
const when = new Date(b.at).toLocaleString('fa-IR', { dateStyle: 'short', timeStyle: 'short' })
|
||||
return b.source === 'stored' ? `آخرین ماندهی ثبتشده — ${when}` : `از ${b.provider ?? 'حسابداری'} — ${when}`
|
||||
})
|
||||
|
||||
async function loadBalance() {
|
||||
balanceLoading.value = true
|
||||
try {
|
||||
balance.value = await api.getAccountingBalance()
|
||||
} catch {
|
||||
balance.value = null
|
||||
} finally {
|
||||
balanceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!session.isLoggedIn) return
|
||||
void loadBalance()
|
||||
loading.value = true
|
||||
try {
|
||||
customer.value = await api.getMe()
|
||||
@@ -225,6 +274,24 @@ onMounted(async () => {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.prof__spec dd.is-debt {
|
||||
color: @wine;
|
||||
}
|
||||
|
||||
.prof__spec dd.is-credit {
|
||||
color: @green;
|
||||
}
|
||||
|
||||
.prof__spec dd small {
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.prof__skeleton {
|
||||
height: 92px;
|
||||
background: @hairlineSoft;
|
||||
}
|
||||
|
||||
.prof__empty {
|
||||
margin-top: 26px;
|
||||
padding: 34px 20px;
|
||||
|
||||
Reference in New Issue
Block a user