feat: catalog read cache (api + user apps) and clearer Zarrin accounting errors

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
s.esmaeili
2026-10-11 14:31:32 +03:30
co-authored by Cursor
parent 561addc676
commit 574fa1b4aa
53 changed files with 1241 additions and 401 deletions
@@ -87,6 +87,7 @@ import Spinner from '../../components/Spinner.vue'
import DialogVantOneButton from '../../components/DialogVantOneButton.vue'
import SingleImageUploader from '../../components/upload/SingleImageUploader.vue'
import { api } from '../../api/client'
import { KEYS, useQueryCache } from '../../stores/queryCache'
const router = useRouter()
@@ -117,6 +118,7 @@ async function submit() {
shop_image_id: form.shop_image_id,
ncode_image_id: form.ncode_image_id,
})
useQueryCache().invalidate(KEYS.profile)
done.value = true
} catch {
error.value = 'ثبت اطلاعات انجام نشد. دوباره تلاش کنید.'
+28 -21
View File
@@ -154,7 +154,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, onActivated, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Loading, PullRefresh } from 'vant'
import Icon from '../../components/icon/Icon.vue'
@@ -167,12 +167,15 @@ import { price } from '../../utils/filters'
import { useSessionStore } from '../../stores/session'
import { useReveal } from '../../composables/useReveal'
import { api, type CustomerDetail, type GoldPriceQuote } from '../../api/client'
import { useCachedReads } from '../../api/cachedReads'
import { KEYS } from '../../stores/queryCache'
const router = useRouter()
const session = useSessionStore()
const reads = useCachedReads()
const customer = ref<CustomerDetail | null>(null)
const goldPrice = ref<GoldPriceQuote | null>(null)
const customer = ref<CustomerDetail | null>(reads.peek<CustomerDetail>(KEYS.profile) ?? null)
const goldPrice = ref<GoldPriceQuote | null>(reads.peek<GoldPriceQuote>(KEYS.goldPrice) ?? null)
const refreshing = ref(false)
// The ledger stores the gram position as a signed figure — positive means the
@@ -254,30 +257,34 @@ async function logOut() {
router.push({ name: 'home' })
}
async function loadDashboard() {
if (session.isLoggedIn) {
// 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()
} catch {
goldPrice.value = null
}
async function loadDashboard(force = false) {
// 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.
const [me, price] = await Promise.allSettled([
// The balance on this tab is the one figure read fresh on every visit;
// the cached copy only fills the screen until it arrives.
session.isLoggedIn ? reads.me({ force: true }) : Promise.resolve(null),
reads.goldPrice({ force }),
])
if (me.status === 'fulfilled') customer.value = me.value
else console.log(me.reason)
goldPrice.value = price.status === 'fulfilled' ? price.value : null
}
async function onRefresh() {
await loadDashboard()
await loadDashboard(true)
refreshing.value = false
}
let activatedOnce = false
onActivated(() => {
// Kept alive by MasterLayout, so every later visit lands here, not in onMounted.
if (activatedOnce) void loadDashboard()
activatedOnce = true
})
// First-paint entrance, the same choreography HomeView uses: played once,
// after the first load; pull-to-refresh and tab returns don't replay it.
const { revealed, revealDelay, reveal } = useReveal('dashboard')
@@ -43,19 +43,22 @@ import Spinner from '../../components/Spinner.vue'
import SectionHead from '../../components/home/SectionHead.vue'
import ProductCard from '../../components/product/ProductCard.vue'
import { useFavoritesStore } from '../../stores/favoritesStore'
import { api, type ProductSummary } from '../../api/client'
import { useCatalogStore } from '../../stores/catalogStore'
const router = useRouter()
const favorites = useFavoritesStore()
const catalog = useCatalogStore()
const loading = ref(false)
const allProducts = ref<ProductSummary[]>([])
const allProducts = computed(() => catalog.products)
const favoriteProducts = computed(() => allProducts.value.filter((p) => favorites.isFavorite(p.id)))
onMounted(async () => {
loading.value = true
loading.value = !catalog.loaded
try {
allProducts.value = await api.listProducts()
await catalog.load()
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
+16 -8
View File
@@ -164,9 +164,12 @@ import {
} from '../../utils/orderStatus'
import { jdate } from '../../utils/filters'
import { useSessionStore } from '../../stores/session'
import { api, type OrderDetail } from '../../api/client'
import type { OrderDetail } from '../../api/client'
import { useCachedReads } from '../../api/cachedReads'
import { KEYS } from '../../stores/queryCache'
const session = useSessionStore()
const reads = useCachedReads()
const tabs = [
{ title: 'همه', key: 'ALL' },
@@ -183,7 +186,7 @@ const activeTab = ref('ALL')
const loading = ref(false)
const refreshing = ref(false)
const allOrders = ref<OrderDetail[]>([])
const allOrders = ref<OrderDetail[]>(reads.peek<OrderDetail[]>(KEYS.orders) ?? [])
const searchTerm = ref<string | undefined>(undefined)
const filteredOrders = computed(() => {
@@ -197,11 +200,16 @@ function readSearch(value: string | undefined) {
searchTerm.value = value
}
async function loadOrders() {
if (!session.isLoggedIn) return
loading.value = true
async function loadOrders(force = false) {
if (!session.isLoggedIn) {
allOrders.value = []
return
}
loading.value = reads.peek(KEYS.orders) === undefined
try {
allOrders.value = await api.getMyOrders()
allOrders.value = await reads.myOrders({ force })
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
@@ -220,11 +228,11 @@ function openDetail(item: OrderDetail) {
}
async function onRefresh() {
await loadOrders()
await loadOrders(true)
refreshing.value = false
}
onMounted(loadOrders)
onMounted(() => loadOrders())
</script>
<style scoped lang="less">
+12 -8
View File
@@ -104,7 +104,9 @@ 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 AccountingBalance, type CustomerDetail } from '../../api/client'
import type { AccountingBalance, CustomerDetail } from '../../api/client'
import { useCachedReads } from '../../api/cachedReads'
import { KEYS } from '../../stores/queryCache'
const VERIFICATION_LABELS: Record<string, string> = {
UNVERIFIED: 'احراز نشده',
@@ -115,7 +117,8 @@ const VERIFICATION_LABELS: Record<string, string> = {
}
const session = useSessionStore()
const customer = ref<CustomerDetail | null>(null)
const reads = useCachedReads()
const customer = ref<CustomerDetail | null>(reads.peek<CustomerDetail>(KEYS.profile) ?? null)
const loading = ref(false)
const isVerified = computed(() => customer.value?.verificationStep === 'VERIFIED')
@@ -133,8 +136,9 @@ 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)
// accounting server never holds up the profile itself. The last copy fills the
// screen until the fresh one arrives.
const balance = ref<AccountingBalance | null>(reads.peek<AccountingBalance>(KEYS.balance) ?? null)
const balanceLoading = ref(false)
const num = (v: string | null) => Number(v ?? 0)
@@ -151,9 +155,9 @@ const balanceSubtitle = computed(() => {
})
async function loadBalance() {
balanceLoading.value = true
balanceLoading.value = !balance.value
try {
balance.value = await api.getAccountingBalance()
balance.value = await reads.accountingBalance({ force: true })
} catch {
balance.value = null
} finally {
@@ -164,9 +168,9 @@ async function loadBalance() {
onMounted(async () => {
if (!session.isLoggedIn) return
void loadBalance()
loading.value = true
loading.value = !customer.value
try {
customer.value = await api.getMe()
customer.value = await reads.me({ force: true })
} finally {
loading.value = false
}
@@ -107,7 +107,9 @@ import Spinner from '../../components/Spinner.vue'
import SectionHead from '../../components/home/SectionHead.vue'
import { jdate, price } from '../../utils/filters'
import { useSessionStore } from '../../stores/session'
import { api, type LedgerEntrySummary } from '../../api/client'
import type { LedgerEntrySummary } from '../../api/client'
import { useCachedReads } from '../../api/cachedReads'
import { KEYS } from '../../stores/queryCache'
const SOURCE_LABELS: Record<string, string> = {
ORDER_STOCK: 'سفارش',
@@ -121,9 +123,10 @@ const SOURCE_LABELS: Record<string, string> = {
}
const session = useSessionStore()
const reads = useCachedReads()
const loading = ref(false)
const entries = ref<LedgerEntrySummary[]>([])
const entries = ref<LedgerEntrySummary[]>(reads.peek<LedgerEntrySummary[]>(KEYS.ledger) ?? [])
const showPopUp = ref(false)
const activeEntry = ref<LedgerEntrySummary | null>(null)
@@ -141,9 +144,9 @@ const openEntry = (item: LedgerEntrySummary) => {
onMounted(async () => {
if (!session.isLoggedIn) return
loading.value = true
loading.value = reads.peek(KEYS.ledger) === undefined
try {
entries.value = await api.getMyLedgerEntries()
entries.value = await reads.myLedgerEntries()
} finally {
loading.value = false
}