Add Tahesab jewelry accounting integration with multi-DB connections.

Encrypt tokens server-side, sync customers both ways, show balances/documents on the person Assets tab, and push sanads with idempotent Factor_Code logging.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-04 22:00:23 +03:30
co-authored by Cursor
parent d2f66d11fc
commit 4b47b753c3
26 changed files with 13784 additions and 8 deletions
@@ -0,0 +1,868 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import { formatMoney } from '@/lib/money';
import type {
PersonAccountingLinkDto,
PersonAssetsDto,
TahesabConnectionDto,
TahesabSanadType,
} from '@mento/shared';
import { TAHSAB_SANAD_LABELS } from '@/lib/tahesab';
const props = defineProps<{
personId: string;
}>();
const loading = ref(false);
const error = ref('');
const ok = ref('');
const connections = ref<TahesabConnectionDto[]>([]);
const links = ref<PersonAccountingLinkDto[]>([]);
const connectionId = ref('');
const assets = ref<PersonAssetsDto | null>(null);
const activeTab = ref<'مانده حساب' | 'جزئیات حساب' | 'ثبت سند'>('مانده حساب');
const asnadLoading = ref(false);
const asnadItems = ref<Record<string, unknown>[]>([]);
const fromDate = ref('');
const toDate = ref('');
const manualCode = ref('');
const matchCandidates = ref<
Array<{ moshtariCode: number; name: string; tel: string; matchBy: string }>
>([]);
const sanadBusy = ref(false);
const sanad = ref({
docType: 'buy_sale_gold' as TahesabSanadType,
year: 1405,
month: 1,
day: 1,
sharh: '',
vazn: 0,
ayar: 750,
buyOrSale: 0 as 0 | 1,
mazaneh: 0,
mazanehIsGeram: 0 as 0 | 1,
isAbshode: 1 as 0 | 1,
mablaghKol: 0,
count: 1,
nameSekeh: '',
isBedehiOrTalab: 0 as 0 | 1,
mablagh: 0,
baabat: 'CRM',
isVoroodOrKhorooj: 0 as 0 | 1,
angNumber: '',
nameAz: '',
isDaryaftOrPardakht: 1 as 0 | 1,
nameBank: '',
codeRahgiri: '',
});
const currentLink = computed(() =>
links.value.find((l) => l.connectionId === connectionId.value) ?? null,
);
const hasMultipleConnections = computed(() => connections.value.length > 1);
const goldBalance = computed(() => assets.value?.goldBalance ?? null);
const rialBalance = computed(() => assets.value?.rialBalance ?? null);
const mandehGroups = computed(() => {
const items = assets.value?.mandehDetail ?? [];
const groups: Record<string, typeof items> = {
مالی: [],
'فلزات/وزنی': [],
سکه: [],
کالا: [],
ارز: [],
سایر: [],
};
for (const item of items) {
const name = item.Name || '';
if (/ریال|تومان|مالی|پول|وجه/i.test(name)) groups['مالی']!.push(item);
else if (/سکه/i.test(name)) groups['سکه']!.push(item);
else if (/ارز|دلار|یورو/i.test(name)) groups['ارز']!.push(item);
else if (/طلا|نقره|پلاتین|وزن|آبشده|شمش/i.test(name))
groups['فلزات/وزنی']!.push(item);
else if (/کار|اتیکت|کالا/i.test(name)) groups['کالا']!.push(item);
else groups['سایر']!.push(item);
}
return Object.entries(groups)
.filter(([, list]) => list.length > 0)
.map(([label, list]) => ({ label, items: list }));
});
function balanceClass(v: number | null) {
if (v == null || v === 0) return '';
return v > 0 ? 'credit' : 'debt';
}
function balanceLabel(v: number | null) {
if (v == null || v === 0) return '';
return v > 0 ? 'بستانکار' : 'بدهکار';
}
function formatAmt(v: number | null, digits = 3) {
if (v == null) return '—';
return Math.abs(v).toLocaleString('fa-IR', {
maximumFractionDigits: digits,
minimumFractionDigits: 0,
});
}
function formatMandehAmount(amount: string | number, unit: string) {
const n = Number(amount);
if (!Number.isFinite(n)) return String(amount);
const abs = Math.abs(n);
if (unit === 'ریال') return formatMoney(abs, 'IRR');
return abs.toLocaleString('fa-IR', { maximumFractionDigits: 3 });
}
function defaultJalaliRange() {
// تقریبی: کاربر می‌تواند عوض کند؛ پیش‌فرض بازه یک‌ماهه متنی
const now = new Date();
// بدون وابستگی سنگین: مقادیر نمونه قابل ویرایش
toDate.value = `${now.getFullYear() - 621}-01-01`;
// بهتر: از API تاریخ نخواهیم؛ فیلدها را خالی‌تر با placeholder بگذاریم
const y = 1405;
fromDate.value = `${y}-01-01`;
toDate.value = `${y}-12-29`;
sanad.value.year = y;
sanad.value.month = 1;
sanad.value.day = 1;
}
async function loadConnectionsAndLinks() {
loading.value = true;
error.value = '';
try {
const [connRes, linkRes] = await Promise.all([
api.get<TahesabConnectionDto[]>('/integrations/tahesab/connections/public'),
api.get<PersonAccountingLinkDto[]>(
`/integrations/tahesab/people/${props.personId}/links`,
),
]);
connections.value = connRes.data.filter((c) => c.enabled);
links.value = linkRes.data;
const def =
connections.value.find((c) => c.isDefault) ?? connections.value[0];
if (!connectionId.value && def) connectionId.value = def.id;
else if (
connectionId.value &&
!connections.value.some((c) => c.id === connectionId.value)
) {
connectionId.value = def?.id ?? '';
}
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
(e.response?.data as { message?: string })?.message ??
'بارگذاری اتصال ته‌حساب ناموفق بود.',
)
: 'بارگذاری اتصال ته‌حساب ناموفق بود.';
} finally {
loading.value = false;
}
}
async function refreshAssets() {
if (!connectionId.value || !currentLink.value) {
assets.value = null;
return;
}
loading.value = true;
error.value = '';
try {
const { data } = await api.get<PersonAssetsDto>(
`/integrations/tahesab/people/${props.personId}/assets`,
{ params: { connectionId: connectionId.value, refresh: '1' } },
);
assets.value = data;
} catch (e) {
assets.value = null;
error.value = axios.isAxiosError(e)
? String(
(e.response?.data as { message?: string })?.message ??
'دریافت مانده ناموفق بود.',
)
: 'دریافت مانده ناموفق بود.';
} finally {
loading.value = false;
}
}
async function loadAsnad() {
if (!connectionId.value || !currentLink.value) return;
if (!fromDate.value || !toDate.value) {
error.value = 'تاریخ از و تا را وارد کنید (شمسی YYYY-MM-DD).';
return;
}
asnadLoading.value = true;
error.value = '';
try {
const { data } = await api.get<{ items: Record<string, unknown>[] }>(
`/integrations/tahesab/people/${props.personId}/asnad`,
{
params: {
connectionId: connectionId.value,
from: fromDate.value,
to: toDate.value,
},
},
);
asnadItems.value = data.items ?? [];
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
(e.response?.data as { message?: string })?.message ??
'دریافت اسناد ناموفق بود.',
)
: 'دریافت اسناد ناموفق بود.';
} finally {
asnadLoading.value = false;
}
}
async function runMatch() {
if (!connectionId.value) return;
error.value = '';
ok.value = '';
try {
const { data } = await api.post<{
status: string;
candidates: typeof matchCandidates.value;
link: PersonAccountingLinkDto | null;
}>(`/integrations/tahesab/people/${props.personId}/match`, {
connectionId: connectionId.value,
autoLinkIfUnique: true,
});
matchCandidates.value = data.candidates ?? [];
if (data.status === 'linked') {
ok.value = 'مشتری با موفقیت لینک شد.';
await loadConnectionsAndLinks();
await refreshAssets();
} else if (data.status === 'none') {
error.value =
'مشتری در ته‌حساب پیدا نشد. کد را دستی وارد کنید یا «ثبت در ته‌حساب» را بزنید.';
} else if (data.status === 'ambiguous') {
error.value = 'چند مشتری پیدا شد — یکی را انتخاب کنید.';
} else if (data.status === 'unique') {
ok.value = 'یک مشتری پیدا شد — برای لینک تأیید کنید.';
}
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'تطبیق ناموفق')
: 'تطبیق ناموفق';
}
}
async function linkCode(code: number) {
error.value = '';
ok.value = '';
try {
await api.post(`/integrations/tahesab/people/${props.personId}/links`, {
connectionId: connectionId.value,
moshtariCode: code,
});
ok.value = `لینک به کد ${code} انجام شد.`;
matchCandidates.value = [];
await loadConnectionsAndLinks();
await refreshAssets();
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'لینک ناموفق')
: 'لینک ناموفق';
}
}
async function manualLink() {
const code = Number(manualCode.value);
if (!Number.isFinite(code) || code <= 0) {
error.value = 'کد مشتری معتبر وارد کنید.';
return;
}
await linkCode(code);
}
async function createInTahesab() {
if (!connectionId.value) return;
if (!confirm('این شخص به‌عنوان مشتری جدید در ته‌حساب ثبت شود؟')) return;
error.value = '';
ok.value = '';
try {
await api.post(
`/integrations/tahesab/people/${props.personId}/create-in-tahesab`,
{ connectionId: connectionId.value },
);
ok.value = 'مشتری در ته‌حساب ثبت و لینک شد.';
await loadConnectionsAndLinks();
await refreshAssets();
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
(e.response?.data as { message?: string })?.message ?? 'ثبت ناموفق',
)
: 'ثبت ناموفق';
}
}
async function unlink() {
if (!connectionId.value || !currentLink.value) return;
if (!confirm('لینک این شخص با ته‌حساب قطع شود؟')) return;
await api.delete(
`/integrations/tahesab/people/${props.personId}/links/${connectionId.value}`,
);
assets.value = null;
await loadConnectionsAndLinks();
}
async function pushSanad() {
if (!connectionId.value || !currentLink.value) return;
if (!confirm('سند در ته‌حساب ثبت شود؟ این عمل مالی است و قابل بازگشت از CRM نیست.')) {
return;
}
sanadBusy.value = true;
error.value = '';
ok.value = '';
try {
const body = {
connectionId: connectionId.value,
...sanad.value,
};
const { data } = await api.post<{
idempotent?: boolean;
log: { factorCode: string; status: string; tahesabOk: string | null };
}>(`/integrations/tahesab/people/${props.personId}/sanad`, body);
ok.value = data.idempotent
? `سند تکراری نبود — همان Factor_Code قبلی: ${data.log.factorCode}`
: `سند ثبت شد. کد: ${data.log.tahesabOk ?? data.log.factorCode}`;
await refreshAssets();
if (activeTab.value === 'جزئیات حساب') await loadAsnad();
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
(e.response?.data as { message?: string | string[] })?.message ??
'ثبت سند ناموفق',
)
: 'ثبت سند ناموفق';
} finally {
sanadBusy.value = false;
}
}
watch(connectionId, async () => {
matchCandidates.value = [];
asnadItems.value = [];
if (currentLink.value) await refreshAssets();
else assets.value = null;
});
onMounted(async () => {
defaultJalaliRange();
await loadConnectionsAndLinks();
if (currentLink.value) await refreshAssets();
});
</script>
<template>
<div class="assets">
<p v-if="!connections.length && !loading" class="empty">
اتصال ته‌حساب تعریف نشده است.
<RouterLink to="/settings/tahesab">تنظیمات ته‌حساب</RouterLink>
</p>
<template v-else>
<div class="toolbar">
<label v-if="hasMultipleConnections" class="conn-pick">
<span>اتصال</span>
<select v-model="connectionId">
<option v-for="c in connections" :key="c.id" :value="c.id">
{{ c.label }} ({{ c.dbName }})
</option>
</select>
</label>
<div v-else-if="connections[0]" class="muted">
اتصال: {{ connections[0].label }} · DB {{ connections[0].dbName }}
</div>
</div>
<div class="link-bar card">
<template v-if="currentLink">
<div>
<strong>لینک‌شده</strong>
<span class="muted">
کد ته‌حساب: {{ currentLink.moshtariCode }} · روش:
{{ currentLink.matchMethod }}
</span>
</div>
<div class="actions">
<button type="button" class="btn ghost" @click="refreshAssets">بروزرسانی مانده</button>
<button type="button" class="btn ghost" @click="unlink">قطع لینک</button>
</div>
</template>
<template v-else>
<p class="muted">این مشتری هنوز به ته‌حساب لینک نشده است.</p>
<div class="actions wrap">
<button type="button" class="btn" @click="runMatch">تطبیق خودکار</button>
<button type="button" class="btn ghost" @click="createInTahesab">ثبت در ته‌حساب</button>
<div class="manual">
<input
v-model="manualCode"
type="number"
min="1"
placeholder="کد مشتری"
dir="ltr"
/>
<button type="button" class="btn ghost" @click="manualLink">لینک دستی</button>
</div>
</div>
<ul v-if="matchCandidates.length" class="candidates">
<li v-for="c in matchCandidates" :key="c.moshtariCode">
<button type="button" class="btn ghost" @click="linkCode(c.moshtariCode)">
{{ c.moshtariCode }} — {{ c.name || 'بدون نام' }}
<span class="muted">({{ c.tel || c.matchBy }})</span>
</button>
</li>
</ul>
</template>
</div>
<p v-if="error" class="error">{{ error }}</p>
<p v-if="ok" class="ok">{{ ok }}</p>
<p v-if="loading" class="muted">در حال دریافت اطلاعات…</p>
<template v-if="currentLink && assets">
<section class="summary card">
<h3>دارایی</h3>
<div class="balance-row">
<span>دارایی طلایی</span>
<div class="val" :class="balanceClass(goldBalance)" dir="ltr">
<small v-if="balanceLabel(goldBalance)">{{ balanceLabel(goldBalance) }}</small>
{{ formatAmt(goldBalance) }}
<em>(گرم)</em>
</div>
</div>
<div class="balance-row">
<span>دارایی ریالی</span>
<div class="val" :class="balanceClass(rialBalance)" dir="ltr">
<small v-if="balanceLabel(rialBalance)">{{ balanceLabel(rialBalance) }}</small>
{{ rialBalance != null ? formatMoney(Math.abs(rialBalance), 'IRR') : '—' }}
</div>
</div>
<div v-if="assets.totalGoldEquiv != null" class="balance-row">
<span>دارایی کل به طلا</span>
<div class="val" :class="balanceClass(assets.totalGoldEquiv)" dir="ltr">
{{ formatAmt(assets.totalGoldEquiv) }}
<em>(گرم)</em>
</div>
</div>
<p class="muted tiny">آخرین دریافت: {{ assets.fetchedAt }}</p>
</section>
<div class="tabs">
<button
v-for="t in ['مانده حساب', 'جزئیات حساب', 'ثبت سند'] as const"
:key="t"
type="button"
class="tab"
:class="{ on: activeTab === t }"
@click="activeTab = t"
>
{{ t }}
</button>
</div>
<div v-show="activeTab === 'مانده حساب'" class="detail">
<template v-for="group in mandehGroups" :key="group.label">
<div class="group-title">
<span class="line" />
<strong>{{ group.label }}</strong>
<span class="line" />
</div>
<div
v-for="(item, idx) in group.items"
:key="idx"
class="mandeh-card card"
>
<div class="name">{{ item.Name }}:</div>
<div class="vals">
<template v-for="(v, vi) in item.values" :key="vi">
<span :class="Number(v.amount) >= 0 ? 'credit' : 'debt'">
{{ formatMandehAmount(v.amount, v.unit) }}
</span>
<span class="unit">{{ v.unit }}</span>
<span class="st">{{ v.status === 'بس' ? 'بد' : 'بس' }}</span>
<span v-if="vi < item.values.length - 1" class="sep">|</span>
</template>
</div>
</div>
</template>
<p v-if="!mandehGroups.length" class="muted">جزئیات مانده‌ای نیست.</p>
</div>
<div v-show="activeTab === 'جزئیات حساب'" class="asnad">
<div class="date-row">
<label>
از
<input v-model="fromDate" dir="ltr" placeholder="1405-01-01" />
</label>
<label>
تا
<input v-model="toDate" dir="ltr" placeholder="1405-12-29" />
</label>
<button type="button" class="btn" :disabled="asnadLoading" @click="loadAsnad">
{{ asnadLoading ? '…' : 'نمایش اسناد' }}
</button>
</div>
<div class="asnad-grid">
<article
v-for="(item, idx) in asnadItems"
:key="String(item.Factor_Code ?? item.ID ?? idx)"
class="sanad-card card"
>
<div class="sc-head">
<span>شماره سند: {{ item.Sh_Factor ?? item.Sh_factor ?? '—' }}</span>
<span>تاریخ: {{ item.Tarikh ?? '—' }}</span>
</div>
<div class="sc-row gold">
<span>نوع</span>
<strong>{{ item.NO ?? '—' }}</strong>
</div>
<div class="sc-row gold">
<span>وزن / عیار</span>
<strong dir="ltr">{{ item.Vazn ?? '—' }} / {{ item.Ayar ?? '—' }}</strong>
</div>
<div class="sc-row cash">
<span>مالی</span>
<strong dir="ltr">{{ item.Mali ?? '—' }}</strong>
</div>
<div class="sc-row">
<span>شرح</span>
<span>{{ item.Sharh1 ?? item.Sharh ?? '—' }}</span>
</div>
</article>
</div>
<p v-if="!asnadLoading && !asnadItems.length" class="muted">سندی نیست.</p>
</div>
<form v-show="activeTab === 'ثبت سند'" class="sanad-form card" @submit.prevent="pushSanad">
<p class="warn">
ثبت سند مستقیماً در ته‌حساب انجام می‌شود. قبل از تأیید مقادیر را دوباره بررسی کنید.
</p>
<label>
نوع سند
<select v-model="sanad.docType">
<option v-for="(label, key) in TAHSAB_SANAD_LABELS" :key="key" :value="key">
{{ label }}
</option>
</select>
</label>
<div class="triple">
<label>سال<input v-model.number="sanad.year" type="number" required /></label>
<label>ماه<input v-model.number="sanad.month" type="number" min="1" max="12" required /></label>
<label>روز<input v-model.number="sanad.day" type="number" min="1" max="31" required /></label>
</div>
<label>شرح<input v-model="sanad.sharh" /></label>
<template v-if="sanad.docType === 'buy_sale_gold' || sanad.docType === 'buy_sale_sekeh'">
<div class="triple">
<label>وزن<input v-model.number="sanad.vazn" type="number" step="any" required /></label>
<label>عیار<input v-model.number="sanad.ayar" type="number" step="any" required /></label>
<label>
خرید/فروش
<select v-model.number="sanad.buyOrSale">
<option :value="1">خرید</option>
<option :value="0">فروش</option>
</select>
</label>
</div>
<div class="triple">
<label>مظنه<input v-model.number="sanad.mazaneh" type="number" step="any" required /></label>
<label>مبلغ کل<input v-model.number="sanad.mablaghKol" type="number" step="any" required /></label>
<label v-if="sanad.docType === 'buy_sale_gold'">
آبشده؟
<select v-model.number="sanad.isAbshode">
<option :value="1">آبشده</option>
<option :value="0">متفرقه</option>
</select>
</label>
</div>
<template v-if="sanad.docType === 'buy_sale_sekeh'">
<label>تعداد<input v-model.number="sanad.count" type="number" step="any" required /></label>
<label>نام سکه<input v-model="sanad.nameSekeh" required /></label>
</template>
</template>
<template v-else-if="sanad.docType === 'talab_bedehi'">
<label>
نوع
<select v-model.number="sanad.isBedehiOrTalab">
<option :value="0">طلب</option>
<option :value="1">بدهی</option>
</select>
</label>
<label>مبلغ<input v-model.number="sanad.mablagh" type="number" step="any" required /></label>
<label>وزن (اختیاری)<input v-model.number="sanad.vazn" type="number" step="any" /></label>
<label>بابت<input v-model="sanad.baabat" /></label>
</template>
<template v-else-if="sanad.docType === 'vkh_gold'">
<div class="triple">
<label>وزن<input v-model.number="sanad.vazn" type="number" step="any" required /></label>
<label>عیار<input v-model.number="sanad.ayar" type="number" step="any" required /></label>
<label>
ورود/خروج
<select v-model.number="sanad.isVoroodOrKhorooj">
<option :value="0">ورود</option>
<option :value="1">خروج</option>
</select>
</label>
</div>
<label>شماره انگ<input v-model="sanad.angNumber" /></label>
<label>آزمایشگاه<input v-model="sanad.nameAz" /></label>
</template>
<template v-else-if="sanad.docType === 'vkh_bank'">
<label>
دریافت/پرداخت
<select v-model.number="sanad.isDaryaftOrPardakht">
<option :value="1">دریافت از مشتری</option>
<option :value="0">پرداخت به مشتری</option>
</select>
</label>
<label>مبلغ<input v-model.number="sanad.mablagh" type="number" step="any" required /></label>
<label>نام بانک<input v-model="sanad.nameBank" required /></label>
<label>کد رهگیری<input v-model="sanad.codeRahgiri" /></label>
</template>
<button class="btn" type="submit" :disabled="sanadBusy">
{{ sanadBusy ? 'در حال ثبت…' : 'ثبت سند در ته‌حساب' }}
</button>
</form>
</template>
</template>
</div>
</template>
<style scoped>
.assets {
display: grid;
gap: 0.9rem;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0.9rem;
}
.toolbar,
.link-bar,
.actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
justify-content: space-between;
}
.actions.wrap {
justify-content: flex-start;
}
.conn-pick {
display: flex;
gap: 0.5rem;
align-items: center;
}
.manual {
display: flex;
gap: 0.35rem;
}
.candidates {
list-style: none;
margin: 0.5rem 0 0;
padding: 0;
display: grid;
gap: 0.35rem;
width: 100%;
}
.summary h3 {
margin: 0 0 0.75rem;
}
.balance-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
padding: 0.45rem 0;
border-bottom: 1px dashed var(--line);
}
.balance-row:last-of-type {
border-bottom: 0;
}
.val {
display: flex;
align-items: baseline;
gap: 0.35rem;
font-weight: 700;
}
.val em {
font-style: normal;
font-weight: 500;
font-size: 0.75rem;
color: var(--muted);
}
.val small {
font-size: 0.7rem;
font-weight: 600;
}
.credit {
color: #15803d;
}
.debt {
color: #b91c1c;
}
.tabs {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.tab {
border: 1px solid var(--line);
background: var(--bg);
color: var(--text);
border-radius: 10px;
padding: 0.45rem 0.75rem;
cursor: pointer;
}
.tab.on {
background: var(--accent);
color: #fff;
border-color: transparent;
}
.group-title {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0.75rem 0 0.4rem;
}
.group-title .line {
flex: 1;
border-top: 1px dashed var(--line);
}
.mandeh-card {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
margin-bottom: 0.45rem;
}
.vals {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
align-items: center;
}
.unit,
.st,
.sep,
.muted,
.tiny {
color: var(--muted);
font-size: 0.85rem;
}
.date-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: end;
margin-bottom: 0.75rem;
}
.date-row label,
.sanad-form label {
display: grid;
gap: 0.25rem;
font-size: 0.85rem;
color: var(--muted);
}
.asnad-grid {
display: grid;
gap: 0.65rem;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.sc-head {
display: flex;
justify-content: space-between;
gap: 0.5rem;
font-size: 0.85rem;
margin-bottom: 0.4rem;
}
.sc-row {
display: flex;
justify-content: space-between;
gap: 0.5rem;
padding: 0.3rem 0.4rem;
border-radius: 8px;
margin-top: 0.25rem;
font-size: 0.9rem;
}
.sc-row.gold {
background: color-mix(in srgb, #ca8a04 12%, transparent);
}
.sc-row.cash {
background: color-mix(in srgb, #16a34a 12%, transparent);
}
.sanad-form {
display: grid;
gap: 0.65rem;
}
.triple {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.warn {
margin: 0;
color: #92400e;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
padding: 0.55rem 0.7rem;
font-size: 0.88rem;
}
input,
select {
border: 1px solid var(--line);
border-radius: 10px;
padding: 0.45rem 0.6rem;
background: var(--bg);
color: var(--text);
}
.btn {
border: 0;
border-radius: 10px;
padding: 0.45rem 0.8rem;
background: var(--accent);
color: #fff;
font-weight: 600;
cursor: pointer;
}
.btn.ghost {
background: transparent;
color: var(--text);
border: 1px solid var(--line);
}
.error {
color: #b91c1c;
}
.ok {
color: #15803d;
}
.empty a {
color: var(--accent);
}
@media (max-width: 640px) {
.triple {
grid-template-columns: 1fr;
}
}
</style>