Files
CRM_Montzar/apps/web/src/views/TahesabSettingsView.vue
T
MahdiandCursor 092767b6f2 Align older CRM pages with the Mento visual language.
Restyle list, form, settings, and secondary modules to match contacts/activities patterns with teal chrome, soft panels, and mobile layouts — without changing already-redesigned screens.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 02:29:41 +03:30

571 lines
14 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import type {
CreateTahesabConnectionRequest,
TahesabConnectionDto,
UpdateTahesabConnectionRequest,
} from '@mento/shared';
const loading = ref(false);
const saving = ref(false);
const error = ref('');
const ok = ref('');
const items = ref<TahesabConnectionDto[]>([]);
const editingId = ref<string | null>(null);
const form = reactive({
label: '',
baseUrl: '',
dbName: 'DB',
token: '',
isDefault: false,
allowInsecureTls: false,
enabled: true,
});
const sync = reactive({
connectionId: '',
phone: '',
name: '',
moshtariCode: '' as string | number,
});
const syncResults = ref<
Array<{ moshtariCode: number | null; name: string; tel: string }>
>([]);
function resetForm() {
editingId.value = null;
form.label = '';
form.baseUrl = '';
form.dbName = 'DB';
form.token = '';
form.isDefault = items.value.length === 0;
form.allowInsecureTls = false;
form.enabled = true;
}
function startEdit(row: TahesabConnectionDto) {
editingId.value = row.id;
form.label = row.label;
form.baseUrl = row.baseUrl;
form.dbName = row.dbName;
form.token = '';
form.isDefault = row.isDefault;
form.allowInsecureTls = row.allowInsecureTls;
form.enabled = row.enabled;
}
async function load() {
loading.value = true;
error.value = '';
try {
const { data } = await api.get<TahesabConnectionDto[]>(
'/integrations/tahesab/connections',
);
items.value = data;
if (!editingId.value) form.isDefault = data.length === 0;
if (!sync.connectionId && data[0]) sync.connectionId = data[0].id;
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
(e.response?.data as { message?: string })?.message ??
'بارگذاری اتصالات ناموفق بود.',
)
: 'بارگذاری اتصالات ناموفق بود.';
} finally {
loading.value = false;
}
}
async function save() {
saving.value = true;
error.value = '';
ok.value = '';
try {
if (editingId.value) {
const body: UpdateTahesabConnectionRequest = {
label: form.label.trim(),
baseUrl: form.baseUrl.trim(),
dbName: form.dbName.trim(),
isDefault: form.isDefault,
allowInsecureTls: form.allowInsecureTls,
enabled: form.enabled,
};
if (form.token.trim()) body.token = form.token.trim();
await api.patch(
`/integrations/tahesab/connections/${editingId.value}`,
body,
);
ok.value = 'اتصال به‌روزرسانی شد.';
} else {
if (!form.token.trim()) {
error.value = 'توکن الزامی است.';
return;
}
const body: CreateTahesabConnectionRequest = {
label: form.label.trim(),
baseUrl: form.baseUrl.trim(),
dbName: form.dbName.trim(),
token: form.token.trim(),
isDefault: form.isDefault,
allowInsecureTls: form.allowInsecureTls,
enabled: form.enabled,
};
await api.post('/integrations/tahesab/connections', body);
ok.value = 'اتصال افزوده شد.';
}
resetForm();
await load();
} catch (e) {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'ذخیره ناموفق بود');
} else {
error.value = 'ذخیره ناموفق بود';
}
} finally {
saving.value = false;
}
}
async function testConn(id: string) {
error.value = '';
ok.value = '';
try {
const { data } = await api.post<{ ok: boolean; error?: string }>(
`/integrations/tahesab/connections/${id}/test`,
);
if (data.ok) ok.value = 'اتصال سالم است.';
else error.value = data.error ?? 'تست اتصال ناموفق بود.';
await load();
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'تست ناموفق')
: 'تست ناموفق';
}
}
async function removeConn(id: string) {
if (!confirm('این اتصال حذف شود؟ لینک مشتریان مرتبط هم پاک می‌شود.')) return;
error.value = '';
try {
await api.delete(`/integrations/tahesab/connections/${id}`);
ok.value = 'اتصال حذف شد.';
if (editingId.value === id) resetForm();
await load();
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'حذف ناموفق')
: 'حذف ناموفق';
}
}
async function searchTahesab() {
error.value = '';
ok.value = '';
syncResults.value = [];
try {
const body: Record<string, unknown> = {
connectionId: sync.connectionId,
};
if (sync.moshtariCode !== '' && sync.moshtariCode != null) {
body.moshtariCode = Number(sync.moshtariCode);
} else if (sync.phone.trim()) {
body.phone = sync.phone.trim();
} else if (sync.name.trim()) {
body.name = sync.name.trim();
} else {
error.value = 'موبایل، نام یا کد مشتری را وارد کنید.';
return;
}
const { data } = await api.post<{
results: Array<{ moshtariCode: number | null; name: string; tel: string }>;
}>('/integrations/tahesab/sync/search', body);
syncResults.value = data.results ?? [];
if (!syncResults.value.length) ok.value = 'نتیجه‌ای یافت نشد.';
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'جستجو ناموفق')
: 'جستجو ناموفق';
}
}
async function importOne(moshtariCode: number) {
error.value = '';
ok.value = '';
try {
const { data } = await api.post<{ personId: string }>(
'/integrations/tahesab/sync/import',
{ connectionId: sync.connectionId, moshtariCode },
);
ok.value = `وارد شد — شخص ${data.personId}`;
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'وارد کردن ناموفق')
: 'وارد کردن ناموفق';
}
}
onMounted(load);
</script>
<template>
<section class="page">
<header class="page-head">
<div class="head-copy">
<h1>اتصال ته‌حساب</h1>
<p class="lead">
هر نسخه/دیتابیس ته‌حساب یک اتصال جدا است (آدرس وب‌سرویس + توکن + نام DB).
اگر فقط یک اتصال دارید، همان به‌عنوان پیش‌فرض استفاده می‌شود.
</p>
</div>
</header>
<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="!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>
</span>
<span
v-if="row.lastHealthAt"
class="health"
:class="row.lastHealthOk ? 'ok' : 'bad'"
>
آخرین تست:
{{ row.lastHealthOk ? 'موفق' : 'ناموفق' }}
<template v-if="row.lastError"> — {{ row.lastError }}</template>
</span>
</div>
<div class="row-actions">
<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>
</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"
/>
</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">
<input v-model="form.allowInsecureTls" type="checkbox" />
پذیرش گواهی SSL خودامضا (فقط شبکه داخلی)
</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>
<section class="card sync">
<h2>جستجو / وارد کردن از ته‌حساب</h2>
<p class="muted">
مشتری را در یک اتصال جستجو کنید و به‌عنوان شخص جدید وارد CRM کنید یا به شخص موجود لینک دهید.
</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>
<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>
<button type="button" class="btn ghost" :disabled="!sync.connectionId" @click="searchTahesab">
جستجو
</button>
<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>
<button
type="button"
class="btn"
:disabled="!r.moshtariCode"
@click="importOne(r.moshtariCode!)"
>
وارد کردن
</button>
</li>
</ul>
</section>
</section>
</template>
<style scoped>
.page {
display: grid;
gap: 1rem;
max-width: 720px;
min-width: 0;
overflow-x: clip;
}
.page-head {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
}
.head-copy {
min-width: 0;
}
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 {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.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: 12px;
padding: 0.55rem 0.7rem;
background: #fff;
color: #0f172a;
font: inherit;
width: 100%;
}
select {
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 0.55rem 0.7rem;
background: #fff;
color: #0f172a;
font: inherit;
width: 100%;
}
.check {
display: flex;
align-items: center;
gap: 0.45rem;
color: #0f172a;
}
.form-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 0.55rem 0.9rem;
border-radius: 12px;
font-weight: 700;
border: 1px solid transparent;
cursor: pointer;
font: inherit;
}
.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;
font-weight: 600;
}
.sync-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.sync-results {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.5rem;
}
.sync-results li {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
border-bottom: 1px solid #e2e8f0;
}
@media (max-width: 700px) {
.row {
grid-template-columns: 1fr;
}
.sync-grid {
grid-template-columns: 1fr;
}
.row-actions,
.form-actions,
.sync-results li {
flex-direction: column;
align-items: stretch;
}
}
</style>