admin-panel: add shared modal/header/icon/delete-handler components
New src/components/common/: - BaseFormModal.vue — the standard shell every converted add/edit modal will use: header=title, footer=buttons, body=form. Replaces the ad-hoc 60%/70%/80% <a-modal> widths used across the codebase with 4 named sizes. - ListPageHeader.vue — the add button that goes above every list page, gated on the same rid the create route used (modelled on the one place this already existed, ListUser.vue's add-user button). New src/components/icons.ts: the legacy-Feather-name -> ant-icon map, extracted out of TooltipBtn.vue so the sidebar can resolve the same names through the same map instead of a second copy. New src/composables/useDeleteHandler.ts: the delete-then-refetch-then-toast handler that was copy-pasted verbatim in 6 list views (city/cityList.vue, user/ListUser.vue, apps/category/List.vue, finical/list.vue, apps/product/ProductList.vue, manage/Const.vue). Preserves each site's exact success-message wording and message.success/info choice, and ProductList's irregular POST-based delete endpoint. Verified: pnpm build passes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f0de478940
commit
02d92fc465
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<!--
|
||||
Standard shell for every "add/edit" modal in the panel: header = title,
|
||||
footer = action buttons, body = the form (passed in via the default slot).
|
||||
The 16 forms converted from full pages to modals all use this instead of
|
||||
hand-rolling <a-modal> + <a-card> + a trailing button, which used to be
|
||||
duplicated 16 times with inconsistent widths (60%/70%/80%/default).
|
||||
-->
|
||||
<a-modal
|
||||
:open="open"
|
||||
:title="id ? editTitle : addTitle"
|
||||
:width="WIDTH[width]"
|
||||
:confirm-loading="submitting"
|
||||
:ok-text="okText"
|
||||
cancel-text="انصراف"
|
||||
:mask-closable="false"
|
||||
@update:open="onUpdateOpen"
|
||||
@ok="$emit('submit')"
|
||||
@cancel="onCancel"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<a-form layout="vertical">
|
||||
<slot />
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
open: boolean;
|
||||
/** Presence of `id` switches the modal into edit mode (title only — the
|
||||
* form body decides its own create/edit behaviour from the same prop). */
|
||||
id?: string;
|
||||
addTitle: string;
|
||||
editTitle: string;
|
||||
/** Drives the inner a-spin, e.g. while loading the record being edited. */
|
||||
loading?: boolean;
|
||||
/** Drives the OK button's loading state while a submit is in flight. */
|
||||
submitting?: boolean;
|
||||
/** 520 / 720 / 900 / 1140 — replaces the old ad-hoc 60%/70%/80% widths. */
|
||||
width?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
okText?: string;
|
||||
}>(),
|
||||
{
|
||||
id: undefined,
|
||||
loading: false,
|
||||
submitting: false,
|
||||
width: 'md',
|
||||
okText: 'ثبت اطلاعات',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void;
|
||||
(e: 'submit'): void;
|
||||
(e: 'cancel'): void;
|
||||
}>();
|
||||
|
||||
const WIDTH: Record<'sm' | 'md' | 'lg' | 'xl', number> = {
|
||||
sm: 520,
|
||||
md: 720,
|
||||
lg: 900,
|
||||
xl: 1140,
|
||||
};
|
||||
|
||||
function onUpdateOpen(value: boolean) {
|
||||
emit('update:open', value);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
emit('update:open', false);
|
||||
emit('cancel');
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<!--
|
||||
Standard header for every list page: the section title plus, when the
|
||||
caller has one, an "add" button ABOVE the list — same rid the create
|
||||
route used to gate on, so permissions are not re-derived by hand.
|
||||
Modelled on the one place this already existed (ListUser.vue's add-user
|
||||
button) and generalised so every list page looks the same.
|
||||
-->
|
||||
<div class="list-page-header">
|
||||
<a-button v-if="!rid || hasPermission(rid)" type="primary" @click="$emit('add')">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
{{ addLabel }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
/** Permission id gating the button; omit if the action has no rid of its own. */
|
||||
rid?: string;
|
||||
addLabel?: string;
|
||||
}>(),
|
||||
{ rid: undefined, addLabel: 'افزودن' },
|
||||
);
|
||||
|
||||
defineEmits<{ (e: 'add'): void }>();
|
||||
|
||||
const users = useUsersStore();
|
||||
const hasPermission = (id: string) => Boolean(users.hasPermission(id));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-page-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
// Shared legacy-Feather-name -> ant-design-vue icon map.
|
||||
//
|
||||
// Originally lived only inside TooltipBtn.vue (row-action icons). Extracted
|
||||
// here so the sidebar (navMenuItems.ts icon: values) can resolve the SAME
|
||||
// names through the SAME map instead of duplicating it — one source of
|
||||
// truth for "legacy Feather icon name" -> "ant component" across the app.
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
BarChartOutlined,
|
||||
BellOutlined,
|
||||
BlockOutlined,
|
||||
BookOutlined,
|
||||
CameraOutlined,
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
CreditCardOutlined,
|
||||
DeleteOutlined,
|
||||
DollarOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
FileOutlined,
|
||||
FileTextOutlined,
|
||||
GiftOutlined,
|
||||
HomeOutlined,
|
||||
InboxOutlined,
|
||||
MailOutlined,
|
||||
MessageOutlined,
|
||||
PercentageOutlined,
|
||||
PlusOutlined,
|
||||
SettingOutlined,
|
||||
ShoppingOutlined,
|
||||
StopOutlined,
|
||||
TeamOutlined,
|
||||
UnorderedListOutlined,
|
||||
UserAddOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
/** legacy Feather name -> ant component. */
|
||||
export const ICONS: Record<string, unknown> = {
|
||||
// row-action icons (TooltipBtn.vue's original set)
|
||||
ListIcon: UnorderedListOutlined,
|
||||
FileIcon: FileOutlined,
|
||||
TrashIcon: DeleteOutlined,
|
||||
EditIcon: EditOutlined,
|
||||
CheckIcon: CheckOutlined,
|
||||
CameraIcon: CameraOutlined,
|
||||
EyeIcon: EyeOutlined,
|
||||
PlusIcon: PlusOutlined,
|
||||
XIcon: CloseOutlined,
|
||||
FileTextIcon: FileTextOutlined,
|
||||
|
||||
// sidebar icons (navMenuItems.ts), added when the sidebar started
|
||||
// rendering them for the first time
|
||||
HomeIcon: HomeOutlined,
|
||||
PackageIcon: InboxOutlined,
|
||||
UserIcon: UserOutlined,
|
||||
UsersIcon: TeamOutlined,
|
||||
UserPlusIcon: UserAddOutlined,
|
||||
ShoppingBagIcon: ShoppingOutlined,
|
||||
BoxIcon: InboxOutlined,
|
||||
GridIcon: AppstoreOutlined,
|
||||
LayersIcon: BlockOutlined,
|
||||
CreditCardIcon: CreditCardOutlined,
|
||||
DollarSignIcon: DollarOutlined,
|
||||
PercentIcon: PercentageOutlined,
|
||||
GiftIcon: GiftOutlined,
|
||||
SlashIcon: StopOutlined,
|
||||
BookIcon: BookOutlined,
|
||||
MessageSquareIcon: MessageOutlined,
|
||||
MailIcon: MailOutlined,
|
||||
BellIcon: BellOutlined,
|
||||
BarChartIcon: BarChartOutlined,
|
||||
SettingsIcon: SettingOutlined,
|
||||
InboxIcon: InboxOutlined,
|
||||
};
|
||||
|
||||
export function resolveIcon(name: string | undefined): unknown | undefined {
|
||||
if (!name) return undefined;
|
||||
return ICONS[name];
|
||||
}
|
||||
@@ -5,8 +5,9 @@
|
||||
it confirms first, without it fires immediately.
|
||||
|
||||
Legacy rendered <feather-icon icon="ListIcon">. vue-feather has no Vue 3
|
||||
build, so the feather NAME is mapped to an ant-design icon here. Ported views
|
||||
keep writing icon="ListIcon" unchanged.
|
||||
build, so the feather NAME is mapped to an ant-design icon via the shared
|
||||
map in components/icons.ts (also used by the sidebar). Ported views keep
|
||||
writing icon="ListIcon" unchanged.
|
||||
-->
|
||||
<div>
|
||||
<a-tooltip :title="title">
|
||||
@@ -33,18 +34,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
CameraOutlined,
|
||||
CheckOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
FileOutlined,
|
||||
FileTextOutlined,
|
||||
PlusOutlined,
|
||||
UnorderedListOutlined,
|
||||
CloseOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { UnorderedListOutlined } from '@ant-design/icons-vue';
|
||||
import { resolveIcon } from '../icons';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -58,21 +49,7 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits<{ (e: 'click'): void }>();
|
||||
|
||||
/** feather name -> ant component */
|
||||
const ICONS: Record<string, unknown> = {
|
||||
ListIcon: UnorderedListOutlined,
|
||||
FileIcon: FileOutlined,
|
||||
TrashIcon: DeleteOutlined,
|
||||
EditIcon: EditOutlined,
|
||||
CheckIcon: CheckOutlined,
|
||||
CameraIcon: CameraOutlined,
|
||||
EyeIcon: EyeOutlined,
|
||||
PlusIcon: PlusOutlined,
|
||||
XIcon: CloseOutlined,
|
||||
FileTextIcon: FileTextOutlined,
|
||||
};
|
||||
|
||||
const iconComponent = computed(() => ICONS[props.icon ?? ''] ?? UnorderedListOutlined);
|
||||
const iconComponent = computed(() => resolveIcon(props.icon) ?? UnorderedListOutlined);
|
||||
|
||||
function onClick() {
|
||||
emit('click');
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Shared "delete a row, refresh the list, show the server's refusal reason"
|
||||
// handler. This exact try/catch body — api.delete, then either a success
|
||||
// toast + refetch, or Modal.error with the server's message — was copy-pasted
|
||||
// verbatim in 6 list views (city/cityList.vue, user/ListUser.vue,
|
||||
// apps/category/List.vue, finical/list.vue, apps/product/ProductList.vue,
|
||||
// manage/Const.vue). Centralised here as a composable rather than a component
|
||||
// since it is pure behaviour with no template.
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
import api from '../api/client';
|
||||
|
||||
export function useDeleteHandler(options: {
|
||||
/** Called after a confirmed, successful delete — typically `table.value?.fetch()`. */
|
||||
onSuccess: () => void | Promise<void>;
|
||||
successMessage?: string;
|
||||
/** Some call sites used message.info rather than message.success; preserved
|
||||
* per-site rather than silently unified. */
|
||||
successType?: 'success' | 'info';
|
||||
/** Shown when the server didn't return its own message. */
|
||||
fallbackErrorMessage?: string;
|
||||
}) {
|
||||
const {
|
||||
onSuccess,
|
||||
successMessage = 'با موفقیت حذف شد',
|
||||
successType = 'success',
|
||||
fallbackErrorMessage = 'خطا در حذف',
|
||||
} = options;
|
||||
|
||||
async function remove(url: string, opts?: { method?: 'delete' | 'post' }) {
|
||||
try {
|
||||
// ProductList's delete is a legacy irregular endpoint (POST, not
|
||||
// DELETE) — see docs/ADMIN-PORT-PLAN.md's endpoint-mapping notes.
|
||||
// `method` lets that call site keep using this composable too.
|
||||
if (opts?.method === 'post') {
|
||||
await api.post(url);
|
||||
} else {
|
||||
await api.delete(url);
|
||||
}
|
||||
message[successType](successMessage);
|
||||
await onSuccess();
|
||||
} catch (e: any) {
|
||||
// The backend refuses deletion while other records still reference the
|
||||
// row (e.g. a city with customers/branches, a brand with products).
|
||||
Modal.error({
|
||||
title: e?.response?.data?.message ?? fallbackErrorMessage,
|
||||
okText: 'بستن',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { remove };
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
placement="top"
|
||||
ok-text="بله"
|
||||
cancel-text="خیر"
|
||||
@confirm="confirmDelete(record.id)"
|
||||
@confirm="confirmDelete(`category/${record.id}`)"
|
||||
>
|
||||
<template #title><p>آیا مطمئن به حذف دسته مورد نظر هستید ؟</p></template>
|
||||
<DeleteOutlined class="cursor-pointer mx-1 text-danger" />
|
||||
@@ -40,11 +40,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import XTable from '../../../components/x-table/XTable.vue';
|
||||
import { buildCategoryModel } from './CategoryTbl';
|
||||
import { useUsersStore } from '../../../stores/users';
|
||||
import { useDeleteHandler } from '../../../composables/useDeleteHandler';
|
||||
import api from '../../../api/client';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -55,16 +56,13 @@ const noImage = '/img/no-items.png';
|
||||
const model = buildCategoryModel();
|
||||
const tbl = ref<{ fetch: () => Promise<void> } | null>(null);
|
||||
|
||||
async function confirmDelete(id: string) {
|
||||
try {
|
||||
await api.delete(`category/${id}`);
|
||||
message.info('دسته حذف شد');
|
||||
tbl.value?.fetch();
|
||||
} catch (e: any) {
|
||||
// The backend refuses while products or child categories still reference it.
|
||||
Modal.error({ title: e?.response?.data?.message ?? 'خطا در حذف دسته', okText: 'بستن' });
|
||||
}
|
||||
}
|
||||
// The backend refuses while products or child categories still reference it.
|
||||
const { remove: confirmDelete } = useDeleteHandler({
|
||||
onSuccess: () => tbl.value?.fetch(),
|
||||
successMessage: 'دسته حذف شد',
|
||||
successType: 'info',
|
||||
fallbackErrorMessage: 'خطا در حذف دسته',
|
||||
});
|
||||
|
||||
function toEdit(id: string) {
|
||||
router.push(`/category/edit/${id}`);
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
@@ -105,6 +105,7 @@ import XTable from '../../../components/x-table/XTable.vue';
|
||||
import { buildProductListModel } from './productListModel';
|
||||
import { useUsersStore } from '../../../stores/users';
|
||||
import { isBranch } from '../../../utils';
|
||||
import { useDeleteHandler } from '../../../composables/useDeleteHandler';
|
||||
import api from '../../../api/client';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -132,19 +133,17 @@ async function showProductModal(record: Record<string, any>) {
|
||||
visibleProduct.value = true;
|
||||
}
|
||||
|
||||
async function confirmDelete(id: string) {
|
||||
try {
|
||||
await api.post(`delete/product/${id}`);
|
||||
message.info('محصول حذف شد');
|
||||
tblProducts.value?.fetch();
|
||||
} catch (e: any) {
|
||||
// The backend refuses when the product appears in past orders; show its
|
||||
// Farsi explanation rather than a generic failure.
|
||||
Modal.error({
|
||||
title: e?.response?.data?.message ?? 'خطا در حذف محصول',
|
||||
okText: 'بستن',
|
||||
});
|
||||
}
|
||||
// The backend refuses when the product appears in past orders; show its
|
||||
// Farsi explanation rather than a generic failure.
|
||||
const { remove: confirmDeleteById } = useDeleteHandler({
|
||||
onSuccess: () => tblProducts.value?.fetch(),
|
||||
successMessage: 'محصول حذف شد',
|
||||
successType: 'info',
|
||||
fallbackErrorMessage: 'خطا در حذف محصول',
|
||||
});
|
||||
|
||||
function confirmDelete(id: string) {
|
||||
return confirmDeleteById(`delete/product/${id}`, { method: 'post' });
|
||||
}
|
||||
|
||||
function toEdit(id: string) {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
placement="top"
|
||||
ok-text="بله"
|
||||
cancel-text="خیر"
|
||||
@confirm="confirmDelete(record.id)"
|
||||
@confirm="confirmDelete(`cities/${record.id}`)"
|
||||
>
|
||||
<template #title><p>آیا مطمئن به حذف شهر مورد نظر هستید ؟</p></template>
|
||||
<DeleteOutlined class="cursor-pointer mx-1 text-danger" />
|
||||
@@ -53,11 +53,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import XTable from '../../components/x-table/XTable.vue';
|
||||
import { buildCityListModel } from './CityListModel';
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
import { useDeleteHandler } from '../../composables/useDeleteHandler';
|
||||
import api from '../../api/client';
|
||||
|
||||
const users = useUsersStore();
|
||||
@@ -106,15 +107,11 @@ async function save() {
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDelete(id: string) {
|
||||
try {
|
||||
await api.delete(`cities/${id}`);
|
||||
message.success('شهر حذف شد');
|
||||
citiesTable.value?.fetch();
|
||||
} catch (e: any) {
|
||||
// The backend refuses while customers, branches or addresses reference it.
|
||||
Modal.error({ title: e?.response?.data?.message ?? 'خطا در حذف شهر', okText: 'بستن' });
|
||||
}
|
||||
}
|
||||
// The backend refuses while customers, branches or addresses reference it.
|
||||
const { remove: confirmDelete } = useDeleteHandler({
|
||||
onSuccess: () => citiesTable.value?.fetch(),
|
||||
successMessage: 'شهر حذف شد',
|
||||
fallbackErrorMessage: 'خطا در حذف شهر',
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
import XTable from '../../components/x-table/XTable.vue';
|
||||
import { buildDocumentListModel } from './list_model';
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
import { useDeleteHandler } from '../../composables/useDeleteHandler';
|
||||
import api from '../../api/client';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -99,16 +100,16 @@ async function changeConfirme(record: Record<string, any>) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCash(record: Record<string, any>) {
|
||||
try {
|
||||
await api.delete(`cash/${record.id}`);
|
||||
message.success('سند حذف شد');
|
||||
tbl.value?.fetch();
|
||||
} catch (e: any) {
|
||||
// The backend refuses to delete a confirmed entry — it has already moved the
|
||||
// customer's balance. Surface its Farsi explanation.
|
||||
Modal.error({ title: e?.response?.data?.message ?? 'خطا در حذف سند', okText: 'بستن' });
|
||||
}
|
||||
// The backend refuses to delete a confirmed entry — it has already moved the
|
||||
// customer's balance. Surface its Farsi explanation.
|
||||
const { remove: deleteCashById } = useDeleteHandler({
|
||||
onSuccess: () => tbl.value?.fetch(),
|
||||
successMessage: 'سند حذف شد',
|
||||
fallbackErrorMessage: 'خطا در حذف سند',
|
||||
});
|
||||
|
||||
function deleteCash(record: Record<string, any>) {
|
||||
return deleteCashById(`cash/${record.id}`);
|
||||
}
|
||||
|
||||
function toEdit(id: string) {
|
||||
|
||||
@@ -167,9 +167,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
import { useDeleteHandler } from '../../composables/useDeleteHandler';
|
||||
import api from '../../api/client';
|
||||
import ImageUploadInput from '../../components/upload/ImageUploadInput.vue';
|
||||
|
||||
@@ -241,15 +242,15 @@ async function saveBrandEdit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function removeBrand(id: string) {
|
||||
try {
|
||||
await api.delete(`brand/${id}`);
|
||||
await loadBrands();
|
||||
message.success('برند حذف شد');
|
||||
} catch (e: any) {
|
||||
// refused while products reference it
|
||||
Modal.error({ title: e?.response?.data?.message ?? 'خطا در حذف برند', okText: 'بستن' });
|
||||
}
|
||||
// refused while products reference it
|
||||
const { remove: removeBrandById } = useDeleteHandler({
|
||||
onSuccess: loadBrands,
|
||||
successMessage: 'برند حذف شد',
|
||||
fallbackErrorMessage: 'خطا در حذف برند',
|
||||
});
|
||||
|
||||
function removeBrand(id: string) {
|
||||
return removeBrandById(`brand/${id}`);
|
||||
}
|
||||
|
||||
// ── tags
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
placement="top"
|
||||
ok-text="بله"
|
||||
cancel-text="خیر"
|
||||
@confirm="confirmDelete(record.id)"
|
||||
@confirm="confirmDelete(`users/${record.id}`)"
|
||||
>
|
||||
<template #title><p>آیا مطمئن به حذف کاربر مورد نظر هستید ؟</p></template>
|
||||
<DeleteOutlined class="cursor-pointer mx-1 text-danger" />
|
||||
@@ -50,11 +50,12 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||
import XTable from '../../components/x-table/XTable.vue';
|
||||
import { buildUserListModel } from './userTbl';
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
import { useDeleteHandler } from '../../composables/useDeleteHandler';
|
||||
import api from '../../api/client';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -74,17 +75,13 @@ function edit(record: Record<string, any>) {
|
||||
router.push(`/user/edit/${record.id}`);
|
||||
}
|
||||
|
||||
async function confirmDelete(id: string) {
|
||||
try {
|
||||
await api.delete(`users/${id}`);
|
||||
message.success('کاربر حذف شد');
|
||||
tbl.value?.fetch();
|
||||
} catch (e: any) {
|
||||
// The backend refuses when the admin owns ledger entries or status events —
|
||||
// deleting them would erase the audit trail.
|
||||
Modal.error({ title: e?.response?.data?.message ?? 'خطا در حذف کاربر', okText: 'بستن' });
|
||||
}
|
||||
}
|
||||
// The backend refuses when the admin owns ledger entries or status events —
|
||||
// deleting them would erase the audit trail.
|
||||
const { remove: confirmDelete } = useDeleteHandler({
|
||||
onSuccess: () => tbl.value?.fetch(),
|
||||
successMessage: 'کاربر حذف شد',
|
||||
fallbackErrorMessage: 'خطا در حذف کاربر',
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user