@@ -0,0 +1,353 @@
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref } from '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" >
< table >
< 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 : 1 rem ;
}
. hint ,
. muted {
color : # 64748 b ;
font - size : 0.88 rem ;
line - height : 1.7 ;
}
. form - grid {
display : grid ;
grid - template - columns : 1 fr 1 fr ;
gap : 0.75 rem ;
margin : 0.75 rem 0 1 rem ;
}
. list {
display : grid ;
gap : 0.75 rem ;
}
. item {
display : flex ;
justify - content : space - between ;
gap : 0.75 rem ;
padding : 0.85 rem ;
border : 1 px solid # eef2f7 ;
border - radius : var ( -- radius ) ;
}
. actions {
display : flex ;
gap : 0.4 rem ;
flex - wrap : wrap ;
align - items : start ;
}
. webhook {
margin - top : 1 rem ;
padding : 0.75 rem ;
background : # f8fafc ;
border - radius : var ( -- radius ) ;
display : grid ;
gap : 0.5 rem ;
}
. webhook code {
word - break : break - all ;
font - size : 0.85 rem ;
}
. warn {
color : # b45309 ;
font - size : 0.85 rem ;
}
. error {
color : # b91c1c ;
font - weight : 600 ;
}
. ok {
color : # 0 f766e ;
font - weight : 600 ;
}
. table - wrap {
overflow : auto ;
}
table {
width : 100 % ;
border - collapse : collapse ;
font - size : 0.9 rem ;
}
th ,
td {
text - align : right ;
padding : 0.55 rem ;
border - bottom : 1 px solid # eef2f7 ;
}
th {
color : # 64748 b ;
font - size : 0.8 rem ;
}
@ media ( max - width : 800 px ) {
. form - grid ,
. item {
grid - template - columns : 1 fr ;
flex - direction : column ;
}
}
< / style >