Files
CRM_Montzar/apps/web/src/views/WooCommerceSettingsView.vue
T
2026-10-05 11:56:49 +03:30

358 lines
10 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 { computed, onMounted, reactive, ref } from 'vue';
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
import axios from 'axios';
import { api } from '@/lib/api';
import type {
CreateWooCommerceConnectionRequest,
WooCommerceConnectionDto,
WooCommerceOrderLinkDto,
WooCommerceSyncResultDto,
} from '@mento/shared';
import {
UiButton,
UiCard,
UiEmptyState,
UiField,
UiInput,
UiPageHeader,
} from '@/components/ui';
import UiIcon from '@/components/UiIcon.vue';
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
const loading = ref(false);
const saving = ref(false);
const syncing = ref(false);
const error = ref('');
const ok = ref('');
const items = ref<WooCommerceConnectionDto[]>([]);
const orders = ref<WooCommerceOrderLinkDto[]>([]);
const lastSync = ref<WooCommerceSyncResultDto | null>(null);
const form = reactive({
label: 'فروشگاه اصلی',
storeUrl: '',
consumerKey: '',
consumerSecret: '',
webhookSecret: '',
enabled: true,
});
const webhookUrl = computed(() => {
if (!items.value[0]) return '';
const origin = typeof window !== 'undefined' ? window.location.origin : '';
return `${origin}/api/public/integrations/woocommerce/${items.value[0].id}/webhook`;
});
async function load() {
loading.value = true;
error.value = '';
try {
const [connRes, orderRes] = await Promise.all([
api.get<WooCommerceConnectionDto[]>('/integrations/woocommerce/connections'),
api.get<WooCommerceOrderLinkDto[]>('/integrations/woocommerce/orders'),
]);
items.value = connRes.data;
orders.value = orderRes.data;
} catch (e) {
if (axios.isAxiosError(e) && e.response?.status === 403) {
error.value = 'ماژول ووکامرس برای این محیط فعال نیست.';
} else {
error.value = 'بارگذاری ناموفق بود.';
}
} finally {
loading.value = false;
}
}
async function create() {
if (!form.storeUrl.trim() || !form.consumerKey.trim() || !form.consumerSecret.trim()) {
error.value = 'آدرس فروشگاه و کلیدهای API را کامل کنید.';
return;
}
saving.value = true;
error.value = '';
ok.value = '';
try {
const payload: CreateWooCommerceConnectionRequest = {
label: form.label.trim() || 'فروشگاه',
storeUrl: form.storeUrl.trim(),
consumerKey: form.consumerKey.trim(),
consumerSecret: form.consumerSecret.trim(),
webhookSecret: form.webhookSecret.trim() || undefined,
enabled: form.enabled,
};
await api.post('/integrations/woocommerce/connections', payload);
form.consumerKey = '';
form.consumerSecret = '';
form.webhookSecret = '';
ok.value = 'اتصال ذخیره شد.';
await load();
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'ذخیره ناموفق بود.')
: 'ذخیره ناموفق بود.';
} finally {
saving.value = false;
}
}
async function test(id: string) {
try {
await api.post(`/integrations/woocommerce/connections/${id}/test`);
ok.value = 'اتصال به فروشگاه موفق بود.';
await load();
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'تست ناموفق بود.')
: 'تست ناموفق بود.';
}
}
async function sync(id: string) {
syncing.value = true;
error.value = '';
try {
const { data } = await api.post<WooCommerceSyncResultDto>(
`/integrations/woocommerce/connections/${id}/sync`,
);
lastSync.value = data;
ok.value = `همگام‌سازی: ${toFaDigits(data.created)} جدید، ${toFaDigits(data.updated)} به‌روز.`;
await load();
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'همگام‌سازی ناموفق بود.')
: 'همگام‌سازی ناموفق بود.';
} finally {
syncing.value = false;
}
}
async function remove(id: string) {
if (!confirm('این اتصال حذف شود؟ لینک سفارش‌های قبلی می‌ماند ولی همگام‌سازی قطع می‌شود.')) {
return;
}
try {
await api.delete(`/integrations/woocommerce/connections/${id}`);
await load();
} catch {
error.value = 'حذف ناموفق بود.';
}
}
async function copyWebhook() {
if (!webhookUrl.value) return;
try {
await navigator.clipboard.writeText(webhookUrl.value);
ok.value = 'آدرس وب‌هوک کپی شد.';
} catch {
error.value = 'کپی ممکن نشد.';
}
}
onMounted(() => {
void load();
});
</script>
<template>
<div class="page">
<UiPageHeader
title="ووکامرس"
description="همگام‌سازی سفارش‌های فروشگاه با فاکتور و مشتری در CRM"
>
<template #actions>
<UiButton variant="secondary" :disabled="loading" @click="load">
<UiIcon name="refresh" />
بروزرسانی
</UiButton>
</template>
</UiPageHeader>
<p v-if="error" class="error">{{ error }}</p>
<p v-if="ok" class="ok">{{ ok }}</p>
<UiCard title="اتصال جدید">
<p class="hint">
در ووکامرس: ووکامرس ← تنظیمات ← پیشرفته ← REST API یک کلید بسازید.
برای وب‌هوک: ووکامرس ← تنظیمات ← پیشرفته ← وب‌هوک‌ها، موضوع «Order created/updated».
</p>
<div class="form-grid">
<UiField label="نام">
<UiInput v-model="form.label" placeholder="فروشگاه اصلی" />
</UiField>
<UiField label="آدرس فروشگاه">
<UiInput v-model="form.storeUrl" placeholder="https://shop.example.com" />
</UiField>
<UiField label="Consumer Key">
<UiInput v-model="form.consumerKey" placeholder="ck_…" />
</UiField>
<UiField label="Consumer Secret">
<UiInput v-model="form.consumerSecret" placeholder="cs_…" />
</UiField>
<UiField label="Webhook Secret (اختیاری)">
<UiInput v-model="form.webhookSecret" placeholder="برای تأیید امضا" />
</UiField>
</div>
<UiButton :loading="saving" @click="create">ذخیره اتصال</UiButton>
</UiCard>
<UiCard title="اتصال‌های ذخیره‌شده">
<UiEmptyState
v-if="!loading && !items.length"
title="اتصالی نیست"
description="کلید REST API فروشگاه را وارد کنید."
/>
<div v-else class="list">
<article v-for="c in items" :key="c.id" class="item">
<div>
<strong>{{ c.label }}</strong>
<p class="muted">{{ c.storeUrl }}</p>
<p class="muted">
کلید {{ c.consumerKeyMasked }}
· سفارش‌های واردشده {{ toFaDigits(c.importedOrders) }}
·
{{
c.lastSyncAt
? `آخرین همگام‌سازی ${formatJalaliNumeric(new Date(c.lastSyncAt))}`
: 'هنوز همگام نشده'
}}
</p>
<p v-if="c.lastError" class="warn">{{ c.lastError }}</p>
</div>
<div class="actions">
<UiButton variant="secondary" @click="test(c.id)">تست</UiButton>
<UiButton :loading="syncing" @click="sync(c.id)">همگام‌سازی</UiButton>
<UiButton variant="secondary" @click="remove(c.id)">حذف</UiButton>
</div>
</article>
</div>
<div v-if="webhookUrl" class="webhook">
<p class="hint">آدرس وب‌هوک این اتصال:</p>
<code>{{ webhookUrl }}</code>
<UiButton variant="secondary" @click="copyWebhook">کپی</UiButton>
</div>
</UiCard>
<UiCard title="آخرین سفارش‌های واردشده">
<UiEmptyState
v-if="!orders.length"
title="سفارشی نیست"
description="همگام‌سازی را بزنید یا وب‌هوک را وصل کنید."
/>
<div v-else class="table-wrap">
<div class="tc-row">
<TableColumnsMenu table-key="woocommerce.orders" align="end" />
</div>
<table v-table-cols="'woocommerce.orders'">
<thead>
<tr>
<th>سفارش ووکامرس</th>
<th>فاکتور</th>
<th>وضعیت</th>
<th>مبلغ</th>
<th>زمان</th>
</tr>
</thead>
<tbody>
<tr v-for="o in orders" :key="o.id">
<td>#{{ o.wooOrderNumber || o.wooOrderId }}</td>
<td>{{ o.invoiceNumber }}</td>
<td>{{ o.status }}</td>
<td>{{ toFaDigits(o.total) }} {{ o.currency }}</td>
<td>{{ formatJalaliNumeric(new Date(o.syncedAt)) }}</td>
</tr>
</tbody>
</table>
</div>
</UiCard>
</div>
</template>
<style scoped>
.page {
display: grid;
gap: 1rem;
}
.hint,
.muted {
color: #64748b;
font-size: 0.88rem;
line-height: 1.7;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin: 0.75rem 0 1rem;
}
.list {
display: grid;
gap: 0.75rem;
}
.item {
display: flex;
justify-content: space-between;
gap: 0.75rem;
padding: 0.85rem;
border: 1px solid #eef2f7;
border-radius: var(--radius);
}
.actions {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
align-items: start;
}
.webhook {
margin-top: 1rem;
padding: 0.75rem;
background: #f8fafc;
border-radius: var(--radius);
display: grid;
gap: 0.5rem;
}
.webhook code {
word-break: break-all;
font-size: 0.85rem;
}
.warn {
color: #b45309;
font-size: 0.85rem;
}
.error {
color: #b91c1c;
font-weight: 600;
}
.ok {
color: #0f766e;
font-weight: 600;
}
.table-wrap {
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th,
td {
text-align: right;
padding: 0.55rem;
border-bottom: 1px solid #eef2f7;
}
th {
color: #64748b;
font-size: 0.8rem;
}
@media (max-width: 800px) {
.form-grid,
.item {
grid-template-columns: 1fr;
flex-direction: column;
}
}
</style>