feat(ui): standardize design system, modals, sidebar, and form inputs
- Implement unified design system tokens, 10px radius, and IRANSans typography - Modernize sidebar with Gemini-style collapsed expander, square 1:1 items, and elevated profile flyout - Standardize all modal dialogs with 16px padding, fixed header/footer, 16px medium title, and sticky bottom actions - Refactor DealFormView and CustomFieldsSection with standard UiField, UiInput, and UiSelect components - Add draggable bottom-left QuickCreate floating action button
This commit is contained in:
@@ -9,6 +9,16 @@ import type {
|
||||
UpdateTahesabConnectionRequest,
|
||||
} from '@mento/shared';
|
||||
import { brand } from '@mento/shared';
|
||||
import {
|
||||
UiBadge,
|
||||
UiButton,
|
||||
UiCard,
|
||||
UiField,
|
||||
UiInput,
|
||||
UiPageHeader,
|
||||
UiSelect,
|
||||
} from '@/components/ui';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
@@ -221,29 +231,27 @@ async function removeConn(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function searchTahesab(mode: 'filter' | 'range' = 'filter') {
|
||||
async function searchTahesab(mode: 'range' | 'filter') {
|
||||
error.value = '';
|
||||
ok.value = '';
|
||||
syncResults.value = [];
|
||||
if (!sync.connectionId) {
|
||||
error.value = 'ابتدا اتصال را انتخاب کنید.';
|
||||
error.value = 'ابتدا یک اتصال انتخاب کنید.';
|
||||
return;
|
||||
}
|
||||
syncBusy.value = true;
|
||||
try {
|
||||
syncBusy.value = true;
|
||||
const body: Record<string, unknown> = {
|
||||
connectionId: sync.connectionId,
|
||||
};
|
||||
const body: Record<string, unknown> = { connectionId: sync.connectionId };
|
||||
if (mode === 'range') {
|
||||
const fromRaw = String(sync.fromCode ?? '').trim();
|
||||
const toRaw = String(sync.toCode ?? '').trim();
|
||||
if (!fromRaw && !toRaw) {
|
||||
body.listAll = true;
|
||||
body.fromCode = 1;
|
||||
body.toCode = 5000;
|
||||
} else {
|
||||
const fromCode = Number(fromRaw);
|
||||
const toCode = Number(toRaw);
|
||||
if (!Number.isFinite(fromCode) || !Number.isFinite(toCode)) {
|
||||
error.value = 'بازهٔ کد را درست وارد کنید، یا هر دو را خالی بگذارید تا همه نمایش داده شوند.';
|
||||
const fromCode = fromRaw ? Number(fromRaw) : 1;
|
||||
const toCode = toRaw ? Number(toRaw) : fromCode + 1999;
|
||||
if (Number.isNaN(fromCode) || Number.isNaN(toCode) || fromCode < 1 || toCode < fromCode) {
|
||||
error.value = 'بازه کد مشتری نامعتبر است.';
|
||||
return;
|
||||
}
|
||||
body.fromCode = fromCode;
|
||||
@@ -328,22 +336,27 @@ async function importBulk(mode: 'results' | 'range') {
|
||||
if (!fromRaw && !toRaw) {
|
||||
if (
|
||||
!confirm(
|
||||
`همهٔ مشتریان تهحساب وارد ${brand.nameFa} میشوند (قبلاً لینکشدهها رد میشوند). ممکن است کمی طول بکشد. ادامه؟`,
|
||||
`کل مشتریان از کد ۱ تا ۵۰۰۰ از تهحساب بهعنوان شخص وارد ${brand.nameFa} میشوند. ادامه؟`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
body.listAll = true;
|
||||
body.fromCode = 1;
|
||||
body.toCode = 5000;
|
||||
} else {
|
||||
const fromCode = Number(fromRaw);
|
||||
const toCode = Number(toRaw);
|
||||
if (!Number.isFinite(fromCode) || !Number.isFinite(toCode)) {
|
||||
error.value = 'بازهٔ کد را درست وارد کنید، یا هر دو را خالی بگذارید تا همه وارد شوند.';
|
||||
const fromCode = fromRaw ? Number(fromRaw) : 1;
|
||||
const toCode = toRaw ? Number(toRaw) : fromCode + 1999;
|
||||
if (Number.isNaN(fromCode) || Number.isNaN(toCode) || fromCode < 1 || toCode < fromCode) {
|
||||
error.value = 'بازه نامعتبر است.';
|
||||
return;
|
||||
}
|
||||
if (toCode - fromCode > 2000) {
|
||||
error.value = 'حداکثر طول بازه در هر درخواست ۲۰۰۰ کد است.';
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
`همهٔ مشتریان تهحساب با کد ${fromCode} تا ${toCode} وارد ${brand.nameFa} میشوند (قبلاً لینکشدهها رد میشوند). ادامه؟`,
|
||||
`مشتریان با کد ${fromCode} تا ${toCode} از تهحساب وارد ${brand.nameFa} میشوند. ادامه؟`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -372,482 +385,469 @@ onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<header class="page-head">
|
||||
<div class="head-copy">
|
||||
<h1>اتصال تهحساب</h1>
|
||||
<p class="lead">
|
||||
هر نسخه/دیتابیس تهحساب یک اتصال جدا است (آدرس وبسرویس + توکن + نام DB).
|
||||
اگر فقط یک اتصال دارید، همان بهعنوان پیشفرض استفاده میشود.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<section class="crm-page tahesab-page" dir="rtl">
|
||||
<UiPageHeader
|
||||
title="اتصال نرمافزار مالی تهحساب"
|
||||
description="همگامسازی اشخاص، ماندهحسابها و اسناد مالی بین تهحساب و نرمافزار مدیریت ارتباط با مشتریان"
|
||||
/>
|
||||
|
||||
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-if="ok" class="ok">{{ ok }}</p>
|
||||
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
|
||||
<div v-if="ok" class="crm-alert crm-alert-success">{{ ok }}</div>
|
||||
|
||||
<div
|
||||
v-if="items.some((r) => !r.allowInsecureTls && isTlsError(r.lastError))"
|
||||
class="banner warn"
|
||||
class="crm-alert crm-alert-warning"
|
||||
>
|
||||
اتصال شما بهخاطر گواهی SSL خودامضا قطع است. روی همان کارت اتصال،
|
||||
<strong>فعالسازی SSL خودامضا</strong> را بزنید؛ بعد دوباره «نمایش بازه» را امتحان کنید.
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && items.length" class="list">
|
||||
<article v-for="row in items" :key="row.id" class="card row">
|
||||
<div class="row-main">
|
||||
<strong>{{ row.label }}</strong>
|
||||
<span class="muted">{{ row.baseUrl }}</span>
|
||||
<span class="meta">
|
||||
DB: {{ row.dbName }} · توکن: {{ row.tokenMasked }}
|
||||
<template v-if="row.isDefault"> · پیشفرض</template>
|
||||
<template v-if="!row.enabled"> · غیرفعال</template>
|
||||
<template v-if="row.allowInsecureTls"> · SSL خودامضا مجاز</template>
|
||||
</span>
|
||||
<span
|
||||
v-if="row.lastHealthAt"
|
||||
class="health"
|
||||
:class="row.lastHealthOk ? 'ok' : 'bad'"
|
||||
>
|
||||
آخرین تست:
|
||||
{{ row.lastHealthOk ? 'موفق' : 'ناموفق' }}
|
||||
<template v-if="row.lastError"> — {{ row.lastError }}</template>
|
||||
</span>
|
||||
<UiCard v-if="!loading && items.length" title="اتصالات تعریفشده" class="section-card">
|
||||
<div class="connections-list">
|
||||
<div v-for="row in items" :key="row.id" class="connection-row">
|
||||
<div class="connection-info">
|
||||
<div class="connection-title-row">
|
||||
<strong class="connection-label">{{ row.label }}</strong>
|
||||
<UiBadge v-if="row.isDefault" variant="primary">پیشفرض</UiBadge>
|
||||
<UiBadge :variant="row.enabled ? 'success' : 'neutral'">
|
||||
{{ row.enabled ? 'فعال' : 'غیرفعال' }}
|
||||
</UiBadge>
|
||||
<UiBadge v-if="row.allowInsecureTls" variant="warning">SSL خودامضا</UiBadge>
|
||||
</div>
|
||||
<p class="connection-url font-mono">{{ row.baseUrl }}</p>
|
||||
<p class="connection-meta">
|
||||
<span>بانک: {{ row.dbName }}</span>
|
||||
<span class="meta-dot">·</span>
|
||||
<span>توکن: {{ row.tokenMasked }}</span>
|
||||
</p>
|
||||
<div
|
||||
v-if="row.lastHealthAt"
|
||||
class="health-status"
|
||||
:class="row.lastHealthOk ? 'ok' : 'bad'"
|
||||
>
|
||||
<span class="health-indicator"></span>
|
||||
<span>آخرین وضعیت: {{ row.lastHealthOk ? 'موفق' : 'ناموفق' }}</span>
|
||||
<span v-if="row.lastError"> — {{ row.lastError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connection-actions">
|
||||
<UiButton
|
||||
v-if="!row.allowInsecureTls && isTlsError(row.lastError)"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@click="enableInsecureTls(row.id)"
|
||||
>
|
||||
فعالسازی SSL خودامضا
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" size="sm" @click="startEdit(row)">
|
||||
<template #icon>
|
||||
<UiIcon name="pencil" />
|
||||
</template>
|
||||
ویرایش
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" size="sm" @click="testConn(row.id)">
|
||||
<template #icon>
|
||||
<UiIcon name="refresh" />
|
||||
</template>
|
||||
تست اتصال
|
||||
</UiButton>
|
||||
<UiButton variant="danger" size="sm" @click="removeConn(row.id)">
|
||||
<template #icon>
|
||||
<UiIcon name="trash" />
|
||||
</template>
|
||||
حذف
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-actions">
|
||||
<button
|
||||
v-if="!row.allowInsecureTls && isTlsError(row.lastError)"
|
||||
type="button"
|
||||
class="btn primary"
|
||||
@click="enableInsecureTls(row.id)"
|
||||
>
|
||||
فعالسازی SSL خودامضا
|
||||
</button>
|
||||
<button type="button" class="btn ghost" @click="startEdit(row)">ویرایش</button>
|
||||
<button type="button" class="btn ghost" @click="testConn(row.id)">تست</button>
|
||||
<button type="button" class="btn danger" @click="removeConn(row.id)">حذف</button>
|
||||
</div>
|
||||
</UiCard>
|
||||
|
||||
<UiCard :title="editingId ? 'ویرایش اتصال تهحساب' : 'افزودن اتصال جدید تهحساب'" class="section-card">
|
||||
<form class="crm-form" @submit.prevent="save">
|
||||
<div class="crm-form-grid-2">
|
||||
<UiField label="عنوان اتصال" required>
|
||||
<UiInput v-model="form.label" required maxlength="120" placeholder="مثلاً شعبه مرکزی، دیتابیس مالی" />
|
||||
</UiField>
|
||||
|
||||
<UiField label="آدرس وبسرویس (Base URL)" required hint="آدرس سرور وبسرویس تهحساب">
|
||||
<UiInput
|
||||
v-model="form.baseUrl"
|
||||
required
|
||||
dir="ltr"
|
||||
placeholder="https://IP:8081 یا https://api.tahesab.ir"
|
||||
@blur="onBaseUrlBlur"
|
||||
/>
|
||||
</UiField>
|
||||
|
||||
<UiField label="نام دیتابیس (DBName)" required>
|
||||
<UiInput v-model="form.dbName" required dir="ltr" placeholder="DB یا TahesabDB" />
|
||||
</UiField>
|
||||
|
||||
<UiField label="توکن دسترسی (Bearer Token)" :required="!editingId" :hint="editingId ? 'خالی بگذارید تا توکن فعلی تغییر نکند.' : 'توکن احراز هویت وبسرویس'">
|
||||
<UiInput
|
||||
v-model="form.token"
|
||||
:required="!editingId"
|
||||
type="password"
|
||||
dir="ltr"
|
||||
autocomplete="new-password"
|
||||
placeholder="توکن امنیتی..."
|
||||
/>
|
||||
</UiField>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<form class="card" @submit.prevent="save">
|
||||
<h2>{{ editingId ? 'ویرایش اتصال' : 'افزودن اتصال' }}</h2>
|
||||
<div class="field">
|
||||
<label>برچسب</label>
|
||||
<input v-model="form.label" required maxlength="120" placeholder="مثلاً شعبه بازار" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>آدرس وبسرویس (base URL)</label>
|
||||
<input
|
||||
v-model="form.baseUrl"
|
||||
required
|
||||
dir="ltr"
|
||||
placeholder="https://api-test.tahesab.ir یا https://IP:8081"
|
||||
@blur="onBaseUrlBlur"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>نام دیتابیس (DBname)</label>
|
||||
<input v-model="form.dbName" required dir="ltr" placeholder="DB یا TahesabDB" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
توکن
|
||||
<span v-if="editingId" class="muted">(خالی بگذارید تا عوض نشود)</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="form.token"
|
||||
:required="!editingId"
|
||||
type="password"
|
||||
dir="ltr"
|
||||
autocomplete="new-password"
|
||||
placeholder="Bearer token"
|
||||
/>
|
||||
</div>
|
||||
<label class="check">
|
||||
<input v-model="form.isDefault" type="checkbox" />
|
||||
اتصال پیشفرض
|
||||
</label>
|
||||
<label class="check">
|
||||
<input v-model="form.enabled" type="checkbox" />
|
||||
فعال
|
||||
</label>
|
||||
<label class="check highlight">
|
||||
<input v-model="form.allowInsecureTls" type="checkbox" />
|
||||
پذیرش گواهی SSL خودامضا (برای IP داخلی / https بدون گواهی معتبر — معمولاً لازم است)
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button class="btn primary" type="submit" :disabled="saving">
|
||||
{{ saving ? 'در حال ذخیره…' : editingId ? 'بهروزرسانی' : 'افزودن' }}
|
||||
</button>
|
||||
<button v-if="editingId" type="button" class="btn ghost" @click="resetForm">انصراف</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="checkbox-group">
|
||||
<label class="custom-check">
|
||||
<input v-model="form.isDefault" type="checkbox" />
|
||||
<span>اتصال پیشفرض سازمان</span>
|
||||
</label>
|
||||
<label class="custom-check">
|
||||
<input v-model="form.enabled" type="checkbox" />
|
||||
<span>اتصال فعال باشد</span>
|
||||
</label>
|
||||
<label class="custom-check highlight">
|
||||
<input v-model="form.allowInsecureTls" type="checkbox" />
|
||||
<span>پذیرش گواهی SSL خودامضا (برای IP محلی یا پورت داخلی بدون گواهی رسمی)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<section class="card sync">
|
||||
<h2>وارد کردن مشتریان از تهحساب</h2>
|
||||
<p class="muted">
|
||||
فقط «افزودن اتصال» مشتریها را نمیآورد. باید با بازهٔ کد یا جستجو آنها را از تهحساب
|
||||
<strong>وارد</strong> کنید تا در «اشخاص و شرکتها» دیده شوند. جزئیات مانده/اسناد بعد از لینک، در پروفایل شخص → تب دارایی است.
|
||||
<div class="form-actions-row">
|
||||
<UiButton type="submit" :loading="saving">
|
||||
{{ editingId ? 'بهروزرسانی اتصال' : 'افزودن اتصال' }}
|
||||
</UiButton>
|
||||
<UiButton v-if="editingId" variant="ghost" type="button" @click="resetForm">
|
||||
انصراف
|
||||
</UiButton>
|
||||
</div>
|
||||
</form>
|
||||
</UiCard>
|
||||
|
||||
<UiCard title="وارد کردن و همگامسازی مشتریان از تهحساب" class="section-card">
|
||||
<p class="sync-desc">
|
||||
با استفاده از این بخش میتوانید اشخاص و طرفحسابهای تهحساب را به عنوان مخاطب وارد نرمافزار کرده و سوابق مالی آنها را پیوند دهید.
|
||||
</p>
|
||||
<div class="field">
|
||||
<label>اتصال</label>
|
||||
<select v-model="sync.connectionId">
|
||||
<option disabled value="">انتخاب اتصال</option>
|
||||
<option v-for="row in items" :key="row.id" :value="row.id">
|
||||
{{ row.label }} ({{ row.dbName }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h3 class="subhead">۱) وارد کردن دستهای با بازهٔ کد مشتری</h3>
|
||||
<p class="muted small">
|
||||
اگر «از» و «تا» خالی باشند، مشتریان با کد ۱ تا ۵۰۰۰ از تهحساب لیست میشوند.
|
||||
برای کدهای بالاتر بازه را دستی بگذارید (حداکثر ۲۰۰۰ کد در هر درخواست محدود).
|
||||
</p>
|
||||
<div class="sync-grid range">
|
||||
<input v-model="sync.fromCode" type="number" min="1" dir="ltr" placeholder="از کد (خالی = همه)" />
|
||||
<input v-model="sync.toCode" type="number" min="1" dir="ltr" placeholder="تا کد (خالی = همه)" />
|
||||
</div>
|
||||
<div class="sync-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn ghost"
|
||||
:disabled="!sync.connectionId || syncBusy"
|
||||
@click="searchTahesab('range')"
|
||||
>
|
||||
نمایش بازه
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn primary"
|
||||
:disabled="!sync.connectionId || syncBusy"
|
||||
@click="importBulk('range')"
|
||||
>
|
||||
وارد کردن کل بازه
|
||||
</button>
|
||||
</div>
|
||||
<div class="crm-form">
|
||||
<UiField label="اتصال مورد نظر جهت همگامسازی" required>
|
||||
<UiSelect v-model="sync.connectionId">
|
||||
<option disabled value="">انتخاب اتصال</option>
|
||||
<option v-for="row in items" :key="row.id" :value="row.id">
|
||||
{{ row.label }} ({{ row.dbName }})
|
||||
</option>
|
||||
</UiSelect>
|
||||
</UiField>
|
||||
|
||||
<h3 class="subhead">۲) جستجوی تکی</h3>
|
||||
<div class="sync-grid">
|
||||
<input v-model="sync.phone" dir="ltr" placeholder="موبایل" />
|
||||
<input v-model="sync.name" placeholder="نام" />
|
||||
<input v-model="sync.moshtariCode" type="number" dir="ltr" placeholder="کد مشتری" />
|
||||
</div>
|
||||
<div class="sync-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn ghost"
|
||||
:disabled="!sync.connectionId || syncBusy"
|
||||
@click="searchTahesab('filter')"
|
||||
>
|
||||
جستجو
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn primary"
|
||||
:disabled="!sync.connectionId || syncBusy || !pendingImportCodes().length"
|
||||
@click="importBulk('results')"
|
||||
>
|
||||
وارد کردن نتایج جدید ({{ pendingImportCodes().length }})
|
||||
</button>
|
||||
</div>
|
||||
<div class="sync-sub-section">
|
||||
<h4 class="sync-subhead">۱) وارد کردن دستهای با بازه کد مشتری</h4>
|
||||
<p class="sync-sub-desc">
|
||||
در صورت خالی بودن «از» و «تا»، مشتریان کد ۱ تا ۵۰۰۰ بررسی میشوند (حداکثر ۲۰۰۰ کد در هر درخواست).
|
||||
</p>
|
||||
<div class="crm-form-grid-2">
|
||||
<UiInput v-model="sync.fromCode" type="number" min="1" dir="ltr" placeholder="از کد (مثلاً ۱)" />
|
||||
<UiInput v-model="sync.toCode" type="number" min="1" dir="ltr" placeholder="تا کد (مثلاً ۵۰۰)" />
|
||||
</div>
|
||||
<div class="sync-buttons">
|
||||
<UiButton
|
||||
variant="secondary"
|
||||
:disabled="!sync.connectionId || syncBusy"
|
||||
@click="searchTahesab('range')"
|
||||
>
|
||||
نمایش بازه
|
||||
</UiButton>
|
||||
<UiButton
|
||||
:disabled="!sync.connectionId || syncBusy"
|
||||
@click="importBulk('range')"
|
||||
>
|
||||
وارد کردن کل بازه
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul v-if="syncResults.length" class="sync-results">
|
||||
<li v-for="r in syncResults" :key="String(r.moshtariCode)">
|
||||
<span>
|
||||
{{ r.moshtariCode }} — {{ r.name || 'بدون نام' }}
|
||||
<span class="muted">{{ r.tel }}</span>
|
||||
<span v-if="r.alreadyLinked" class="linked">لینکشده{{ r.personName ? ` · ${r.personName}` : '' }}</span>
|
||||
</span>
|
||||
<button
|
||||
v-if="!r.alreadyLinked"
|
||||
type="button"
|
||||
class="btn"
|
||||
:disabled="!r.moshtariCode || syncBusy"
|
||||
@click="importOne(r.moshtariCode!)"
|
||||
>
|
||||
وارد کردن
|
||||
</button>
|
||||
<RouterLink
|
||||
v-else-if="r.personId"
|
||||
class="btn ghost"
|
||||
:to="{ name: 'person-detail', params: { id: r.personId } }"
|
||||
>
|
||||
مشاهده شخص
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div class="sync-sub-section">
|
||||
<h4 class="sync-subhead">۲) جستجوی تکی یا فیلتر مشخصات</h4>
|
||||
<div class="crm-form-grid-3">
|
||||
<UiInput v-model="sync.phone" dir="ltr" placeholder="موبایل یا تلفن..." />
|
||||
<UiInput v-model="sync.name" placeholder="نام یا عنوان مشتری..." />
|
||||
<UiInput v-model="sync.moshtariCode" type="number" dir="ltr" placeholder="کد مشتری تهحساب..." />
|
||||
</div>
|
||||
<div class="sync-buttons">
|
||||
<UiButton
|
||||
variant="secondary"
|
||||
:disabled="!sync.connectionId || syncBusy"
|
||||
@click="searchTahesab('filter')"
|
||||
>
|
||||
جستجو در تهحساب
|
||||
</UiButton>
|
||||
<UiButton
|
||||
:disabled="!sync.connectionId || syncBusy || !pendingImportCodes().length"
|
||||
@click="importBulk('results')"
|
||||
>
|
||||
وارد کردن نتایج جدید ({{ pendingImportCodes().length }})
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="syncResults.length" class="sync-results-box">
|
||||
<h4 class="sync-results-title">نتایج جستجو ({{ syncResults.length }})</h4>
|
||||
<div class="crm-table-container">
|
||||
<table class="crm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>کد مشتری</th>
|
||||
<th>نام و نام خانوادگی</th>
|
||||
<th>تلفن</th>
|
||||
<th>وضعیت لینک</th>
|
||||
<th>عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in syncResults" :key="String(r.moshtariCode)">
|
||||
<td class="font-mono">{{ r.moshtariCode }}</td>
|
||||
<td class="font-bold">{{ r.name || 'بدون نام' }}</td>
|
||||
<td class="font-mono" dir="ltr">{{ r.tel }}</td>
|
||||
<td>
|
||||
<UiBadge v-if="r.alreadyLinked" variant="success">
|
||||
لینک شده{{ r.personName ? ` (${r.personName})` : '' }}
|
||||
</UiBadge>
|
||||
<UiBadge v-else variant="neutral">آماده ورود</UiBadge>
|
||||
</td>
|
||||
<td>
|
||||
<UiButton
|
||||
v-if="!r.alreadyLinked"
|
||||
size="sm"
|
||||
:disabled="!r.moshtariCode || syncBusy"
|
||||
@click="importOne(r.moshtariCode!)"
|
||||
>
|
||||
وارد کردن
|
||||
</UiButton>
|
||||
<RouterLink
|
||||
v-else-if="r.personId"
|
||||
:to="{ name: 'person-detail', params: { id: r.personId } }"
|
||||
>
|
||||
<UiButton variant="ghost" size="sm">
|
||||
مشاهده شخص
|
||||
</UiButton>
|
||||
</RouterLink>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UiCard>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
max-width: 720px;
|
||||
min-width: 0;
|
||||
overflow-x: clip;
|
||||
.tahesab-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-6);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
.section-card {
|
||||
padding: var(--sp-6);
|
||||
}
|
||||
|
||||
.connections-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-4);
|
||||
}
|
||||
|
||||
.connection-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
gap: var(--sp-4);
|
||||
padding: var(--sp-4) var(--sp-5);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.head-copy {
|
||||
min-width: 0;
|
||||
.connection-row:hover {
|
||||
border-color: var(--color-brand-300, #5eead4);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.3rem;
|
||||
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.lead,
|
||||
.muted {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
||||
padding: 1.1rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.row-main {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.health.ok {
|
||||
color: #15803d;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.health.bad {
|
||||
color: #b91c1c;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
.connection-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-1);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
label {
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
input:not([type]),
|
||||
input[type='password'],
|
||||
input[type='url'],
|
||||
input[type='number'] {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: var(--radius);
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0.7rem;
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
font: inherit;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
select {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: var(--radius);
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0.7rem;
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
font: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.check {
|
||||
.connection-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.check.highlight {
|
||||
padding: 0.65rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
background: #fff7ed;
|
||||
border: 1px solid #fdba74;
|
||||
font-weight: 700;
|
||||
color: #9a3412;
|
||||
}
|
||||
|
||||
.banner.warn {
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 14px;
|
||||
background: #fff7ed;
|
||||
border: 1px solid #fdba74;
|
||||
color: #9a3412;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
gap: var(--sp-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
.connection-label {
|
||||
font-size: var(--fs-md);
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.connection-url {
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--color-text-secondary);
|
||||
direction: ltr;
|
||||
text-align: right;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.connection-meta {
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.meta-dot {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.health-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0.9rem;
|
||||
border-radius: var(--radius);
|
||||
font-weight: 700;
|
||||
border: 1px solid transparent;
|
||||
gap: var(--sp-2);
|
||||
font-size: var(--fs-xs);
|
||||
margin-top: var(--sp-1);
|
||||
}
|
||||
|
||||
.health-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.health-status.ok {
|
||||
color: var(--color-success-text, #15803d);
|
||||
}
|
||||
|
||||
.health-status.ok .health-indicator {
|
||||
background: var(--color-success-text, #15803d);
|
||||
}
|
||||
|
||||
.health-status.bad {
|
||||
color: var(--color-danger-text, #b91c1c);
|
||||
}
|
||||
|
||||
.health-status.bad .health-indicator {
|
||||
background: var(--color-danger-text, #b91c1c);
|
||||
}
|
||||
|
||||
.connection-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-2);
|
||||
margin: var(--sp-2) 0;
|
||||
}
|
||||
|
||||
.custom-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: #14b8a6;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
||||
}
|
||||
|
||||
.btn.ghost {
|
||||
background: #fff;
|
||||
color: #0f766e;
|
||||
border-color: #99f6e4;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background: #fff;
|
||||
color: #b91c1c;
|
||||
border-color: #fecaca;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.ok {
|
||||
margin: 0;
|
||||
color: #0f766e;
|
||||
.custom-check.highlight {
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-warning-bg, #fffbeb);
|
||||
border: 1px solid var(--color-warning-border, #fef3c7);
|
||||
color: var(--color-warning-text, #92400e);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sync-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sync-grid.range {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.sync-actions {
|
||||
.form-actions-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
gap: var(--sp-2);
|
||||
margin-top: var(--sp-2);
|
||||
}
|
||||
|
||||
.subhead {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 0.95rem;
|
||||
color: #0f766e;
|
||||
.sync-desc {
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.muted.small {
|
||||
font-size: 0.85rem;
|
||||
.sync-sub-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-3);
|
||||
padding: var(--sp-4);
|
||||
background: var(--color-surface-soft);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.linked {
|
||||
display: inline-block;
|
||||
margin-inline-start: 0.35rem;
|
||||
font-size: 0.78rem;
|
||||
.sync-subhead {
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 700;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.sync-results {
|
||||
list-style: none;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sync-results li {
|
||||
.sync-sub-desc {
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sync-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
gap: var(--sp-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.sync-results-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-3);
|
||||
margin-top: var(--sp-2);
|
||||
}
|
||||
|
||||
.sync-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.sync-results-title {
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.row-actions,
|
||||
.form-actions,
|
||||
.sync-results li {
|
||||
.font-mono {
|
||||
font-family: var(--font-mono, monospace);
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.connection-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.connection-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user