fix pargar transaction 2
This commit is contained in:
@@ -386,6 +386,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
|||||||
support_messengers: 'longtext',
|
support_messengers: 'longtext',
|
||||||
show_to_users_chat: 'switch',
|
show_to_users_chat: 'switch',
|
||||||
has_landing: 'switch',
|
has_landing: 'switch',
|
||||||
|
has_charts: 'switch',
|
||||||
has_chat: 'switch',
|
has_chat: 'switch',
|
||||||
landing_site: 'longtext',
|
landing_site: 'longtext',
|
||||||
landing_header: 'longtext',
|
landing_header: 'longtext',
|
||||||
@@ -803,6 +804,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
|||||||
'روشن: قابلیت چت پشتیبانی برای این دیپلوی فعال است (منوی ادمین و پیشنیاز نمایش به کاربر). خاموش: چت در ادمین و اپ کاربر مخفی است و سوییچ «چت پشتیبانی» (has_chat) از تنظیمات عادی هم برداشته میشود.',
|
'روشن: قابلیت چت پشتیبانی برای این دیپلوی فعال است (منوی ادمین و پیشنیاز نمایش به کاربر). خاموش: چت در ادمین و اپ کاربر مخفی است و سوییچ «چت پشتیبانی» (has_chat) از تنظیمات عادی هم برداشته میشود.',
|
||||||
has_landing:
|
has_landing:
|
||||||
'روشن: منوی تنظیمات وبسایت در ادمین و تنظیمات لندینگ برای اپ کاربر فعال است. خاموش: منو مخفی است.',
|
'روشن: منوی تنظیمات وبسایت در ادمین و تنظیمات لندینگ برای اپ کاربر فعال است. خاموش: منو مخفی است.',
|
||||||
|
has_charts:
|
||||||
|
'روشن: بخش نمودارها در اپلیکیشن فعال است. خاموش: نمودارها غیرفعال هستند.',
|
||||||
landing_site: 'محتوای تنظیمات کلی لندینگ وبسایت.',
|
landing_site: 'محتوای تنظیمات کلی لندینگ وبسایت.',
|
||||||
landing_header: 'محتوای هدر و ناوبری لندینگ.',
|
landing_header: 'محتوای هدر و ناوبری لندینگ.',
|
||||||
landing_hero: 'محتوای سکشن هیرو لندینگ.',
|
landing_hero: 'محتوای سکشن هیرو لندینگ.',
|
||||||
@@ -1103,6 +1106,7 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
|||||||
support_messengers: 'پیامرسانهای پشتیبانی',
|
support_messengers: 'پیامرسانهای پشتیبانی',
|
||||||
show_to_users_chat: 'نمایش چت به کاربران',
|
show_to_users_chat: 'نمایش چت به کاربران',
|
||||||
has_landing: 'لندینگ وبسایت',
|
has_landing: 'لندینگ وبسایت',
|
||||||
|
has_charts: 'نمودارها',
|
||||||
has_chat: 'چت پشتیبانی',
|
has_chat: 'چت پشتیبانی',
|
||||||
landing_site: 'تنظیمات کلی سایت',
|
landing_site: 'تنظیمات کلی سایت',
|
||||||
landing_header: 'هدر و ناوبری',
|
landing_header: 'هدر و ناوبری',
|
||||||
@@ -1206,7 +1210,7 @@ export const SETTING_LOCKED_KEYS = new Set([
|
|||||||
'unit',
|
'unit',
|
||||||
'wss_enabled',
|
'wss_enabled',
|
||||||
'zaarbox_api_url',
|
'zaarbox_api_url',
|
||||||
'show_to_users_chat', 'has_landing', 'chat_api', 'chat_api_key', 'chat_upload_url', 'chat_download_url'
|
'show_to_users_chat', 'has_landing', 'has_charts', 'chat_api', 'chat_api_key', 'chat_upload_url', 'chat_download_url'
|
||||||
])
|
])
|
||||||
|
|
||||||
/** قابل ویرایش در پنل — همتراز deploy_settings_const.sql */
|
/** قابل ویرایش در پنل — همتراز deploy_settings_const.sql */
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const KHANEGI_SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
|||||||
title: 'دسترسی مشتری به قابلیتها',
|
title: 'دسترسی مشتری به قابلیتها',
|
||||||
icon: 'TeamOutlined',
|
icon: 'TeamOutlined',
|
||||||
match: (k) =>
|
match: (k) =>
|
||||||
/^(enable_automation|enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|banner_enabled|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|customer_special_pricing|enable_gold_credit|tracking_systems_enabled|permision__enabled|show_to_users_chat|has_landing)$/.test(
|
/^(enable_automation|enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|banner_enabled|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|customer_special_pricing|enable_gold_credit|tracking_systems_enabled|permision__enabled|show_to_users_chat|has_landing|has_charts)$/.test(
|
||||||
k,
|
k,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const SUPERADMIN_SYSTEM_SETTING_KEYS = new Set<string>([
|
|||||||
"enable_gold_credit",
|
"enable_gold_credit",
|
||||||
"enable_product_funds",
|
"enable_product_funds",
|
||||||
"group_sms",
|
"group_sms",
|
||||||
|
"has_charts",
|
||||||
"has_landing",
|
"has_landing",
|
||||||
"has_page_earn_money",
|
"has_page_earn_money",
|
||||||
"has_page_gift_card",
|
"has_page_gift_card",
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import {
|
import {
|
||||||
ArrowRightOutlined,
|
ArrowRightOutlined,
|
||||||
DeleteOutlined,
|
|
||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
@@ -38,12 +37,9 @@ const router = useRouter()
|
|||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const canWrite = computed(() => auth.can('khanegi.settings:write'))
|
const canWrite = computed(() => auth.can('khanegi.settings:write'))
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const importing = ref(false)
|
|
||||||
const purging = ref(false)
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const filterType = ref<string | undefined>()
|
const filterType = ref<string | undefined>()
|
||||||
const tenant = ref<TenantMeta | null>(null)
|
const tenant = ref<TenantMeta | null>(null)
|
||||||
const apiBase = ref<string | null>(null)
|
|
||||||
const items = ref<KhanegiSettingRow[]>([])
|
const items = ref<KhanegiSettingRow[]>([])
|
||||||
const drafts = ref<Record<string, string>>({})
|
const drafts = ref<Record<string, string>>({})
|
||||||
const savingKeys = ref<Set<string>>(new Set())
|
const savingKeys = ref<Set<string>>(new Set())
|
||||||
@@ -51,32 +47,11 @@ const savedFlashKeys = ref<Set<string>>(new Set())
|
|||||||
const saveTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
const saveTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
const flashTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
const flashTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
const creds = reactive({
|
|
||||||
mobile: '',
|
|
||||||
password: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
type ImportPreviewRow = {
|
|
||||||
key: string
|
|
||||||
title_fa: string
|
|
||||||
khanegi_value: string
|
|
||||||
superadmin_value: string | null
|
|
||||||
status: 'new_in_khanegi' | 'both'
|
|
||||||
scope: 'none' | 'this' | 'all'
|
|
||||||
}
|
|
||||||
|
|
||||||
const previewRows = ref<ImportPreviewRow[]>([])
|
|
||||||
const onlySaKeys = ref<string[]>([])
|
|
||||||
const previewOpen = ref(false)
|
|
||||||
const applying = ref(false)
|
|
||||||
|
|
||||||
const typeSelectOptions = SETTING_TYPE_OPTIONS.map((t) => ({
|
const typeSelectOptions = SETTING_TYPE_OPTIONS.map((t) => ({
|
||||||
value: t.key,
|
value: t.key,
|
||||||
label: t.value,
|
label: t.value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const count = computed(() => items.value.length)
|
|
||||||
|
|
||||||
function asRow(raw: Record<string, unknown>): KhanegiSettingRow {
|
function asRow(raw: Record<string, unknown>): KhanegiSettingRow {
|
||||||
return normalizeSettingRow({
|
return normalizeSettingRow({
|
||||||
id: typeof raw.id === 'number' ? raw.id : undefined,
|
id: typeof raw.id === 'number' ? raw.id : undefined,
|
||||||
@@ -240,7 +215,6 @@ async function load() {
|
|||||||
try {
|
try {
|
||||||
const { data } = await api.get(`/tenants/${route.params.id}/program-settings`)
|
const { data } = await api.get(`/tenants/${route.params.id}/program-settings`)
|
||||||
tenant.value = data.tenant
|
tenant.value = data.tenant
|
||||||
apiBase.value = data.apiBase || null
|
|
||||||
applyItems(Array.isArray(data.items) ? data.items : [])
|
applyItems(Array.isArray(data.items) ? data.items : [])
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(errText(e) || 'خطا در بارگذاری تنظیمات برنامه')
|
message.error(errText(e) || 'خطا در بارگذاری تنظیمات برنامه')
|
||||||
@@ -252,99 +226,6 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importSettings() {
|
|
||||||
if (!canWrite.value) {
|
|
||||||
message.warning('دسترسی نوشتن ندارید')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!creds.mobile.trim() || !creds.password) {
|
|
||||||
message.warning('نام کاربری و رمز عبور ادمین خانگی را وارد کنید')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
importing.value = true
|
|
||||||
try {
|
|
||||||
const { data } = await api.post(`/tenants/${route.params.id}/program-settings/import`, {
|
|
||||||
mobile: creds.mobile.trim(),
|
|
||||||
password: creds.password,
|
|
||||||
preview: true,
|
|
||||||
})
|
|
||||||
previewRows.value = (data.decisionsNeeded || []).map((row: ImportPreviewRow) => ({
|
|
||||||
...row,
|
|
||||||
scope: 'this',
|
|
||||||
}))
|
|
||||||
onlySaKeys.value = data.onlySuperAdminKeys || []
|
|
||||||
apiBase.value = data.apiBase || apiBase.value
|
|
||||||
previewOpen.value = true
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(errText(e) || 'پیشنمایش ایمپورت ناموفق بود')
|
|
||||||
} finally {
|
|
||||||
importing.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function applyImport() {
|
|
||||||
applying.value = true
|
|
||||||
try {
|
|
||||||
const { data } = await api.post(`/tenants/${route.params.id}/program-settings/import`, {
|
|
||||||
mobile: creds.mobile.trim(),
|
|
||||||
password: creds.password,
|
|
||||||
preview: false,
|
|
||||||
decisions: previewRows.value.map((row) => ({ key: row.key, scope: row.scope })),
|
|
||||||
})
|
|
||||||
tenant.value = data.tenant
|
|
||||||
apiBase.value = data.apiBase || apiBase.value
|
|
||||||
applyItems(Array.isArray(data.items) ? data.items : [])
|
|
||||||
creds.password = ''
|
|
||||||
previewOpen.value = false
|
|
||||||
message.success('مقدارهای can_edit=0 خانگی روی این پروژه نوشته شد.')
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(errText(e) || 'اعمال ایمپورت ناموفق بود')
|
|
||||||
} finally {
|
|
||||||
applying.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* بعد از restore لوکال روی سرور همان پروژه: بقیهٔ کلیدها (غیر دسترسی مشتری)
|
|
||||||
* را از جدول settings همین tenant در سوپرادمین حذف میکند.
|
|
||||||
*/
|
|
||||||
function confirmPurgeNonOwned() {
|
|
||||||
if (!canWrite.value) {
|
|
||||||
message.warning('دسترسی نوشتن ندارید')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Modal.confirm({
|
|
||||||
title: 'حذف تنظیمات غیر دسترسی مشتری؟',
|
|
||||||
content:
|
|
||||||
'فقط کلیدهای «دسترسی مشتری به قابلیتها» روی این پروژه در سوپرادمین میماند. ' +
|
|
||||||
'بقیه از جدول settings همین پروژه در سوپرادمین پاک میشود. ' +
|
|
||||||
'قبل از این کار باید روی سرور همان نصب، settings:restore-from-backup را اجرا کرده باشی.',
|
|
||||||
okText: 'حذف کن',
|
|
||||||
okType: 'danger',
|
|
||||||
cancelText: 'انصراف',
|
|
||||||
onOk: () => purgeNonOwned(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function purgeNonOwned() {
|
|
||||||
purging.value = true
|
|
||||||
try {
|
|
||||||
const { data } = await api.post(`/tenants/${route.params.id}/program-settings/purge-non-owned`)
|
|
||||||
tenant.value = data.tenant
|
|
||||||
applyItems(Array.isArray(data.items) ? data.items : [])
|
|
||||||
const removed = Number(data.removed || 0)
|
|
||||||
message.success(
|
|
||||||
removed > 0
|
|
||||||
? `${removed} کلید غیر دسترسی از این پروژه حذف شد.`
|
|
||||||
: 'چیزی برای حذف نبود؛ همین حالا فقط کلیدهای دسترسی مشتری روی این پروژه است.',
|
|
||||||
)
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(errText(e) || 'حذف ناموفق بود')
|
|
||||||
} finally {
|
|
||||||
purging.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -375,97 +256,6 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section v-if="canWrite" class="form-section purge-section">
|
|
||||||
<h3>مهاجرت مالکیت به دیتابیس نصب</h3>
|
|
||||||
<p class="field-hint">
|
|
||||||
ترتیب: ۱) روی سرور همان پروژه
|
|
||||||
<code dir="ltr">php artisan settings:restore-from-backup --fill-from-superadmin</code>
|
|
||||||
۲) همین دکمه را بزن تا همهٔ کلیدها غیر از «دسترسی مشتری به قابلیتها»
|
|
||||||
از جدول settings این پروژه در سوپرادمین حذف شوند.
|
|
||||||
</p>
|
|
||||||
<a-button danger type="primary" html-type="button" size="large" :loading="purging" @click="confirmPurgeNonOwned">
|
|
||||||
<DeleteOutlined />
|
|
||||||
حذف تنظیمات غیر دسترسی از این پروژه
|
|
||||||
</a-button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="form-section">
|
|
||||||
<h3>انتقال از اپ خانگی</h3>
|
|
||||||
<p class="field-hint">
|
|
||||||
مقدار کلیدهای <b>جدید</b> خانگی را میآورد — نه کلیدهایی که از قبل در قالب
|
|
||||||
<code dir="ltr">deploy_settings_const</code> هستند.
|
|
||||||
اگر سوپرادمین کلیدی داشته باشد که خانگی ندارد، پیشفرض قالب برای همین پروژه گذاشته میشود.
|
|
||||||
</p>
|
|
||||||
<a-form :model="creds" layout="vertical">
|
|
||||||
<div class="import-grid">
|
|
||||||
<a-form-item label="نام کاربری / موبایل ادمین" name="mobile">
|
|
||||||
<a-input
|
|
||||||
v-model:value="creds.mobile"
|
|
||||||
size="large"
|
|
||||||
dir="ltr"
|
|
||||||
autocomplete="username"
|
|
||||||
placeholder="09xxxxxxxxx"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="رمز عبور ادمین" name="password">
|
|
||||||
<a-input-password
|
|
||||||
v-model:value="creds.password"
|
|
||||||
size="large"
|
|
||||||
autocomplete="current-password"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</div>
|
|
||||||
<a-button type="primary" html-type="button" size="large" :loading="importing" @click="importSettings">
|
|
||||||
پیشنمایش ایمپورت
|
|
||||||
</a-button>
|
|
||||||
<p v-if="apiBase" class="import-meta">
|
|
||||||
از <span dir="ltr">{{ apiBase }}</span> · {{ count }} مورد
|
|
||||||
</p>
|
|
||||||
</a-form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<a-modal
|
|
||||||
v-model:open="previewOpen"
|
|
||||||
title="تصمیم برای هر کلید"
|
|
||||||
:confirm-loading="applying"
|
|
||||||
ok-text="اعمال"
|
|
||||||
cancel-text="انصراف"
|
|
||||||
width="760px"
|
|
||||||
@ok="applyImport"
|
|
||||||
>
|
|
||||||
<p class="field-hint">
|
|
||||||
کلیدهای قالب
|
|
||||||
<code dir="ltr">deploy_settings_const</code>
|
|
||||||
بدون سؤال روی همین پروژه نوشته میشوند (مقدار
|
|
||||||
<code dir="ltr">can_edit=0</code>
|
|
||||||
خانگی). اینجا فقط کلیدهایی میآیند که در آن فایل نیستند.
|
|
||||||
بدون انتخاب، پیشفرض «فقط این پروژه» است
|
|
||||||
<template v-if="onlySaKeys.length">
|
|
||||||
· {{ onlySaKeys.length }} کلید قالب فقط در سوپرادمین با پیشفرض میماند
|
|
||||||
</template>.
|
|
||||||
</p>
|
|
||||||
<div v-if="previewRows.length" class="preview-list">
|
|
||||||
<div v-for="row in previewRows" :key="row.key" class="preview-row">
|
|
||||||
<div>
|
|
||||||
<code dir="ltr">{{ row.key }}</code>
|
|
||||||
<div class="preview-title">{{ row.title_fa }}</div>
|
|
||||||
<div class="preview-vals" dir="ltr">خانگی: {{ row.khanegi_value || '—' }}</div>
|
|
||||||
<div v-if="row.superadmin_value != null" class="preview-vals" dir="ltr">
|
|
||||||
سوپرادمین: {{ row.superadmin_value || '—' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a-radio-group v-model:value="row.scope" size="small">
|
|
||||||
<a-radio-button value="none">خیر</a-radio-button>
|
|
||||||
<a-radio-button value="this">فقط این پروژه</a-radio-button>
|
|
||||||
<a-radio-button value="all">همه پروژهها</a-radio-button>
|
|
||||||
</a-radio-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a-empty
|
|
||||||
v-else
|
|
||||||
description="کلید جدیدی خارج از قالب نیست؛ اعمال همان can_edit=0 خانگی را طبق قالب روی این پروژه مینویسد"
|
|
||||||
/>
|
|
||||||
</a-modal>
|
|
||||||
|
|
||||||
<section class="form-section">
|
<section class="form-section">
|
||||||
<div class="settings-toolbar">
|
<div class="settings-toolbar">
|
||||||
@@ -578,20 +368,6 @@ onMounted(load)
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
.import-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px 16px;
|
|
||||||
}
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.import-grid {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.import-meta {
|
|
||||||
margin: 12px 0 0;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.settings-toolbar {
|
.settings-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -702,29 +478,4 @@ onMounted(load)
|
|||||||
.settings-grid {
|
.settings-grid {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.preview-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
max-height: 52vh;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
.preview-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid var(--line, #e2e8f0);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.preview-title {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
.preview-vals {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user