feat(admin): colleague accounting codes and reliable banner saves

Add per-provider Montzar account codes on the colleague transactions page, rename the menu label, and auto-persist banner uploads with https file URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 16:59:24 +03:30
co-authored by Cursor
parent 1c26ec3eaf
commit 3d63e2cf7f
12 changed files with 388 additions and 99 deletions
@@ -0,0 +1,223 @@
<template>
<section class="colleague-accounts">
<header class="colleague-accounts__head">
<div>
<h3 class="colleague-accounts__title">کد حسابداری معامله با همکار</h3>
<p class="colleague-accounts__desc">
برای هر خرید/فروشی که نزد همکار ثبت می‌شود، سند روی این کد حساب در سیستم حسابداری فعال زده می‌شود.
</p>
</div>
<BaseButton type="primary" :loading="saving" @click="save">
ذخیره کدها
</BaseButton>
</header>
<div class="colleague-accounts__grid">
<label
v-for="field in fields"
:key="field.key"
class="colleague-accounts__field"
:class="{ 'colleague-accounts__field--active': field.active }"
>
<span class="colleague-accounts__label">
{{ field.label }}
<span v-if="field.active" class="colleague-accounts__badge">فعال</span>
</span>
<a-input
v-model:value="values[field.key]"
:placeholder="field.placeholder"
allow-clear
class="colleague-accounts__input"
/>
</label>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { message } from 'ant-design-vue'
import { getConfiguration, setConfiguration } from '@/api/configuration'
import type { Const } from '@/model/configuration'
import {
accountingProviderSetting,
initWalletSettingsFromApi,
type AccountingProvider,
} from '@/utils/config'
const KEYS = {
zarin: 'montzar_account_zarin',
tahesab: 'montzar_account_tahesab',
pargar: 'montzar_account_pargar',
} as const
const values = reactive<Record<string, string>>({
[KEYS.zarin]: '',
[KEYS.tahesab]: '',
[KEYS.pargar]: '',
})
const rowIds = reactive<Record<string, number | null>>({
[KEYS.zarin]: null,
[KEYS.tahesab]: null,
[KEYS.pargar]: null,
})
const saving = ref(false)
const loaded = ref(false)
const fields = computed(() => {
const provider = accountingProviderSetting.value as AccountingProvider
return [
{
key: KEYS.zarin,
label: 'کد حساب زرین',
placeholder: 'کد حساب همکار در زرین',
active: provider === 'zarin',
},
{
key: KEYS.tahesab,
label: 'کد حساب ته‌حساب',
placeholder: 'کد حساب همکار در ته‌حساب',
active: provider === 'tahesab',
},
{
key: KEYS.pargar,
label: 'کد حساب پرگار',
placeholder: 'کد حساب همکار در پرگار',
active: provider === 'pargar',
},
]
})
async function load() {
await initWalletSettingsFromApi()
const rows = await getConfiguration()
const list = Array.isArray(rows) ? rows : []
for (const key of Object.values(KEYS)) {
const row = list.find((item) => item?.key === key)
values[key] = row?.value != null ? String(row.value) : ''
rowIds[key] = row?.id != null ? Number(row.id) : null
}
loaded.value = true
}
async function save() {
saving.value = true
try {
for (const key of Object.values(KEYS)) {
await setConfiguration({
id: rowIds[key] ?? undefined,
key,
value: (values[key] ?? '').trim(),
title_fa:
key === KEYS.zarin
? 'کد حساب همکار در زرین'
: key === KEYS.tahesab
? 'کد حساب همکار در ته‌حساب'
: 'کد حساب همکار در پرگار',
is_active: true,
can_edit: true,
type: 'text',
description:
'کد حساب طرف حساب همکار؛ سند خرید/فروش پوشش‌شده روی Montzar روی این حساب ثبت می‌شود.',
} satisfies Const)
}
message.success('کدهای حساب همکار ذخیره شد')
await load()
} catch (error) {
console.error(error)
message.error('ذخیره کد حساب همکار ناموفق بود')
} finally {
saving.value = false
}
}
onMounted(() => {
void load()
})
</script>
<style scoped lang="less">
.colleague-accounts {
margin-bottom: 1rem;
padding: 1rem 1.125rem;
border: 1px solid var(--admin-border, rgba(255, 255, 255, 0.12));
border-radius: 14px;
background: var(--admin-bg-elevated, rgba(28, 28, 34, 0.92));
}
.colleague-accounts__head {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 1rem;
}
.colleague-accounts__title {
margin: 0;
font-size: 0.9375rem;
font-weight: 700;
color: var(--admin-text, #fff);
}
.colleague-accounts__desc {
margin: 0.35rem 0 0;
max-width: 42rem;
font-size: 0.75rem;
line-height: 1.7;
color: var(--admin-text-secondary, rgba(255, 255, 255, 0.65));
}
.colleague-accounts__grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.875rem;
@media (min-width: 900px) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.colleague-accounts__field {
display: flex;
min-width: 0;
flex-direction: column;
gap: 0.4rem;
padding: 0.75rem;
border: 1px solid var(--admin-border, rgba(255, 255, 255, 0.1));
border-radius: 12px;
background: color-mix(in srgb, var(--admin-bg-muted, #1a1a1f) 80%, transparent);
&--active {
border-color: color-mix(in srgb, #d4af37 55%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, #d4af37 18%, transparent);
}
}
.colleague-accounts__label {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--admin-text, #fff);
}
.colleague-accounts__badge {
display: inline-flex;
align-items: center;
padding: 0.1rem 0.4rem;
border-radius: 999px;
background: rgba(212, 175, 55, 0.18);
color: #d4af37;
font-size: 0.6875rem;
font-weight: 700;
}
.colleague-accounts__input {
width: 100%;
}
</style>