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.
|
it confirms first, without it fires immediately.
|
||||||
|
|
||||||
Legacy rendered <feather-icon icon="ListIcon">. vue-feather has no Vue 3
|
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
|
build, so the feather NAME is mapped to an ant-design icon via the shared
|
||||||
keep writing icon="ListIcon" unchanged.
|
map in components/icons.ts (also used by the sidebar). Ported views keep
|
||||||
|
writing icon="ListIcon" unchanged.
|
||||||
-->
|
-->
|
||||||
<div>
|
<div>
|
||||||
<a-tooltip :title="title">
|
<a-tooltip :title="title">
|
||||||
@@ -33,18 +34,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import {
|
import { UnorderedListOutlined } from '@ant-design/icons-vue';
|
||||||
CameraOutlined,
|
import { resolveIcon } from '../icons';
|
||||||
CheckOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
EyeOutlined,
|
|
||||||
FileOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
UnorderedListOutlined,
|
|
||||||
CloseOutlined,
|
|
||||||
} from '@ant-design/icons-vue';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -58,21 +49,7 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const emit = defineEmits<{ (e: 'click'): void }>();
|
const emit = defineEmits<{ (e: 'click'): void }>();
|
||||||
|
|
||||||
/** feather name -> ant component */
|
const iconComponent = computed(() => resolveIcon(props.icon) ?? UnorderedListOutlined);
|
||||||
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);
|
|
||||||
|
|
||||||
function onClick() {
|
function onClick() {
|
||||||
emit('click');
|
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"
|
placement="top"
|
||||||
ok-text="بله"
|
ok-text="بله"
|
||||||
cancel-text="خیر"
|
cancel-text="خیر"
|
||||||
@confirm="confirmDelete(record.id)"
|
@confirm="confirmDelete(`category/${record.id}`)"
|
||||||
>
|
>
|
||||||
<template #title><p>آیا مطمئن به حذف دسته مورد نظر هستید ؟</p></template>
|
<template #title><p>آیا مطمئن به حذف دسته مورد نظر هستید ؟</p></template>
|
||||||
<DeleteOutlined class="cursor-pointer mx-1 text-danger" />
|
<DeleteOutlined class="cursor-pointer mx-1 text-danger" />
|
||||||
@@ -40,11 +40,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
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 { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||||
import XTable from '../../../components/x-table/XTable.vue';
|
import XTable from '../../../components/x-table/XTable.vue';
|
||||||
import { buildCategoryModel } from './CategoryTbl';
|
import { buildCategoryModel } from './CategoryTbl';
|
||||||
import { useUsersStore } from '../../../stores/users';
|
import { useUsersStore } from '../../../stores/users';
|
||||||
|
import { useDeleteHandler } from '../../../composables/useDeleteHandler';
|
||||||
import api from '../../../api/client';
|
import api from '../../../api/client';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -55,16 +56,13 @@ const noImage = '/img/no-items.png';
|
|||||||
const model = buildCategoryModel();
|
const model = buildCategoryModel();
|
||||||
const tbl = ref<{ fetch: () => Promise<void> } | null>(null);
|
const tbl = ref<{ fetch: () => Promise<void> } | null>(null);
|
||||||
|
|
||||||
async function confirmDelete(id: string) {
|
// The backend refuses while products or child categories still reference it.
|
||||||
try {
|
const { remove: confirmDelete } = useDeleteHandler({
|
||||||
await api.delete(`category/${id}`);
|
onSuccess: () => tbl.value?.fetch(),
|
||||||
message.info('دسته حذف شد');
|
successMessage: 'دسته حذف شد',
|
||||||
tbl.value?.fetch();
|
successType: 'info',
|
||||||
} catch (e: any) {
|
fallbackErrorMessage: 'خطا در حذف دسته',
|
||||||
// The backend refuses while products or child categories still reference it.
|
});
|
||||||
Modal.error({ title: e?.response?.data?.message ?? 'خطا در حذف دسته', okText: 'بستن' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toEdit(id: string) {
|
function toEdit(id: string) {
|
||||||
router.push(`/category/edit/${id}`);
|
router.push(`/category/edit/${id}`);
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { message, Modal } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import {
|
import {
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -105,6 +105,7 @@ import XTable from '../../../components/x-table/XTable.vue';
|
|||||||
import { buildProductListModel } from './productListModel';
|
import { buildProductListModel } from './productListModel';
|
||||||
import { useUsersStore } from '../../../stores/users';
|
import { useUsersStore } from '../../../stores/users';
|
||||||
import { isBranch } from '../../../utils';
|
import { isBranch } from '../../../utils';
|
||||||
|
import { useDeleteHandler } from '../../../composables/useDeleteHandler';
|
||||||
import api from '../../../api/client';
|
import api from '../../../api/client';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -132,19 +133,17 @@ async function showProductModal(record: Record<string, any>) {
|
|||||||
visibleProduct.value = true;
|
visibleProduct.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDelete(id: string) {
|
// The backend refuses when the product appears in past orders; show its
|
||||||
try {
|
// Farsi explanation rather than a generic failure.
|
||||||
await api.post(`delete/product/${id}`);
|
const { remove: confirmDeleteById } = useDeleteHandler({
|
||||||
message.info('محصول حذف شد');
|
onSuccess: () => tblProducts.value?.fetch(),
|
||||||
tblProducts.value?.fetch();
|
successMessage: 'محصول حذف شد',
|
||||||
} catch (e: any) {
|
successType: 'info',
|
||||||
// The backend refuses when the product appears in past orders; show its
|
fallbackErrorMessage: 'خطا در حذف محصول',
|
||||||
// Farsi explanation rather than a generic failure.
|
});
|
||||||
Modal.error({
|
|
||||||
title: e?.response?.data?.message ?? 'خطا در حذف محصول',
|
function confirmDelete(id: string) {
|
||||||
okText: 'بستن',
|
return confirmDeleteById(`delete/product/${id}`, { method: 'post' });
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toEdit(id: string) {
|
function toEdit(id: string) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
placement="top"
|
placement="top"
|
||||||
ok-text="بله"
|
ok-text="بله"
|
||||||
cancel-text="خیر"
|
cancel-text="خیر"
|
||||||
@confirm="confirmDelete(record.id)"
|
@confirm="confirmDelete(`cities/${record.id}`)"
|
||||||
>
|
>
|
||||||
<template #title><p>آیا مطمئن به حذف شهر مورد نظر هستید ؟</p></template>
|
<template #title><p>آیا مطمئن به حذف شهر مورد نظر هستید ؟</p></template>
|
||||||
<DeleteOutlined class="cursor-pointer mx-1 text-danger" />
|
<DeleteOutlined class="cursor-pointer mx-1 text-danger" />
|
||||||
@@ -53,11 +53,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref } from 'vue';
|
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 { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||||
import XTable from '../../components/x-table/XTable.vue';
|
import XTable from '../../components/x-table/XTable.vue';
|
||||||
import { buildCityListModel } from './CityListModel';
|
import { buildCityListModel } from './CityListModel';
|
||||||
import { useUsersStore } from '../../stores/users';
|
import { useUsersStore } from '../../stores/users';
|
||||||
|
import { useDeleteHandler } from '../../composables/useDeleteHandler';
|
||||||
import api from '../../api/client';
|
import api from '../../api/client';
|
||||||
|
|
||||||
const users = useUsersStore();
|
const users = useUsersStore();
|
||||||
@@ -106,15 +107,11 @@ async function save() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDelete(id: string) {
|
// The backend refuses while customers, branches or addresses reference it.
|
||||||
try {
|
const { remove: confirmDelete } = useDeleteHandler({
|
||||||
await api.delete(`cities/${id}`);
|
onSuccess: () => citiesTable.value?.fetch(),
|
||||||
message.success('شهر حذف شد');
|
successMessage: 'شهر حذف شد',
|
||||||
citiesTable.value?.fetch();
|
fallbackErrorMessage: 'خطا در حذف شهر',
|
||||||
} catch (e: any) {
|
});
|
||||||
// The backend refuses while customers, branches or addresses reference it.
|
|
||||||
Modal.error({ title: e?.response?.data?.message ?? 'خطا در حذف شهر', okText: 'بستن' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { message, Modal } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import {
|
import {
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
@@ -74,6 +74,7 @@ import {
|
|||||||
import XTable from '../../components/x-table/XTable.vue';
|
import XTable from '../../components/x-table/XTable.vue';
|
||||||
import { buildDocumentListModel } from './list_model';
|
import { buildDocumentListModel } from './list_model';
|
||||||
import { useUsersStore } from '../../stores/users';
|
import { useUsersStore } from '../../stores/users';
|
||||||
|
import { useDeleteHandler } from '../../composables/useDeleteHandler';
|
||||||
import api from '../../api/client';
|
import api from '../../api/client';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -99,16 +100,16 @@ async function changeConfirme(record: Record<string, any>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCash(record: Record<string, any>) {
|
// The backend refuses to delete a confirmed entry — it has already moved the
|
||||||
try {
|
// customer's balance. Surface its Farsi explanation.
|
||||||
await api.delete(`cash/${record.id}`);
|
const { remove: deleteCashById } = useDeleteHandler({
|
||||||
message.success('سند حذف شد');
|
onSuccess: () => tbl.value?.fetch(),
|
||||||
tbl.value?.fetch();
|
successMessage: 'سند حذف شد',
|
||||||
} catch (e: any) {
|
fallbackErrorMessage: 'خطا در حذف سند',
|
||||||
// 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: 'بستن' });
|
function deleteCash(record: Record<string, any>) {
|
||||||
}
|
return deleteCashById(`cash/${record.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toEdit(id: string) {
|
function toEdit(id: string) {
|
||||||
|
|||||||
@@ -167,9 +167,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
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 { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||||
import { useUsersStore } from '../../stores/users';
|
import { useUsersStore } from '../../stores/users';
|
||||||
|
import { useDeleteHandler } from '../../composables/useDeleteHandler';
|
||||||
import api from '../../api/client';
|
import api from '../../api/client';
|
||||||
import ImageUploadInput from '../../components/upload/ImageUploadInput.vue';
|
import ImageUploadInput from '../../components/upload/ImageUploadInput.vue';
|
||||||
|
|
||||||
@@ -241,15 +242,15 @@ async function saveBrandEdit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeBrand(id: string) {
|
// refused while products reference it
|
||||||
try {
|
const { remove: removeBrandById } = useDeleteHandler({
|
||||||
await api.delete(`brand/${id}`);
|
onSuccess: loadBrands,
|
||||||
await loadBrands();
|
successMessage: 'برند حذف شد',
|
||||||
message.success('برند حذف شد');
|
fallbackErrorMessage: 'خطا در حذف برند',
|
||||||
} catch (e: any) {
|
});
|
||||||
// refused while products reference it
|
|
||||||
Modal.error({ title: e?.response?.data?.message ?? 'خطا در حذف برند', okText: 'بستن' });
|
function removeBrand(id: string) {
|
||||||
}
|
return removeBrandById(`brand/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── tags
|
// ── tags
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
placement="top"
|
placement="top"
|
||||||
ok-text="بله"
|
ok-text="بله"
|
||||||
cancel-text="خیر"
|
cancel-text="خیر"
|
||||||
@confirm="confirmDelete(record.id)"
|
@confirm="confirmDelete(`users/${record.id}`)"
|
||||||
>
|
>
|
||||||
<template #title><p>آیا مطمئن به حذف کاربر مورد نظر هستید ؟</p></template>
|
<template #title><p>آیا مطمئن به حذف کاربر مورد نظر هستید ؟</p></template>
|
||||||
<DeleteOutlined class="cursor-pointer mx-1 text-danger" />
|
<DeleteOutlined class="cursor-pointer mx-1 text-danger" />
|
||||||
@@ -50,11 +50,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
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 { DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
|
||||||
import XTable from '../../components/x-table/XTable.vue';
|
import XTable from '../../components/x-table/XTable.vue';
|
||||||
import { buildUserListModel } from './userTbl';
|
import { buildUserListModel } from './userTbl';
|
||||||
import { useUsersStore } from '../../stores/users';
|
import { useUsersStore } from '../../stores/users';
|
||||||
|
import { useDeleteHandler } from '../../composables/useDeleteHandler';
|
||||||
import api from '../../api/client';
|
import api from '../../api/client';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -74,17 +75,13 @@ function edit(record: Record<string, any>) {
|
|||||||
router.push(`/user/edit/${record.id}`);
|
router.push(`/user/edit/${record.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDelete(id: string) {
|
// The backend refuses when the admin owns ledger entries or status events —
|
||||||
try {
|
// deleting them would erase the audit trail.
|
||||||
await api.delete(`users/${id}`);
|
const { remove: confirmDelete } = useDeleteHandler({
|
||||||
message.success('کاربر حذف شد');
|
onSuccess: () => tbl.value?.fetch(),
|
||||||
tbl.value?.fetch();
|
successMessage: 'کاربر حذف شد',
|
||||||
} catch (e: any) {
|
fallbackErrorMessage: 'خطا در حذف کاربر',
|
||||||
// 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: 'بستن' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user