Edit import excel

This commit is contained in:
2026-09-19 15:30:31 +03:30
parent 9c7dad3677
commit 4776d1a3c3
22 changed files with 731 additions and 261 deletions
+36 -46
View File
@@ -17,6 +17,7 @@ import {
UiPagination,
} from '@/components/ui';
import UiIcon from '@/components/UiIcon.vue';
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
import ActivityFormView from '@/views/ActivityFormView.vue';
import type {
ActivityDto,
@@ -78,7 +79,6 @@ const activityModal = ref<{
const menuId = ref<string | null>(null);
const busy = ref(false);
const importMsg = ref('');
const fileInput = ref<HTMLInputElement | null>(null);
const counts = ref<Record<ActivityFilter, number>>({
today: 0,
overdue: 0,
@@ -286,44 +286,31 @@ async function exportExcel() {
}
}
function pickImportFile() {
fileInput.value?.click();
}
async function onImportFile(ev: Event) {
const input = ev.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file) return;
function onImportStart() {
busy.value = true;
importMsg.value = '';
error.value = '';
try {
const form = new FormData();
form.append('file', file);
const { data } = await api.post<ImportContactsResult>('/activities/import', form, {
headers: { 'Content-Type': undefined },
});
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}`;
if (data.errors.length) {
error.value = data.errors
.slice(0, 3)
.map((e) => `ردیف ${e.row}: ${e.message}`)
.join(' | ');
}
await Promise.all([load(), loadCounts()]);
} catch (e) {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'ایمپورت ناموفق بود');
} else {
error.value = 'ایمپورت ناموفق بود';
}
} finally {
busy.value = false;
}
async function onImportDone(data: ImportContactsResult) {
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}`;
if (data.errors.length) {
error.value = data.errors
.slice(0, 3)
.map((e) => `ردیف ${e.row}: ${e.message}`)
.join(' | ');
}
await Promise.all([load(), loadCounts()]);
}
function onImportFail(message: string) {
error.value = message;
}
function onImportSettled() {
busy.value = false;
}
watch(
@@ -357,17 +344,20 @@ onMounted(() => {
<span class="btn-full">خروجی اکسل</span>
<span class="btn-short">خروجی</span>
</UiButton>
<UiButton variant="ghost" type="button" :disabled="busy" @click.stop="pickImportFile">
<span class="btn-full">ورود از اکسل</span>
<span class="btn-short">ورود</span>
</UiButton>
<input
ref="fileInput"
type="file"
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
hidden
@change="onImportFile"
/>
<ExcelImportFlow
preview-path="/activities/import/preview"
import-path="/activities/import"
:disabled="busy"
@start="onImportStart"
@done="onImportDone"
@fail="onImportFail"
@settled="onImportSettled"
>
<UiButton variant="ghost" type="button" :disabled="busy">
<span class="btn-full">ورود از اکسل</span>
<span class="btn-short">ورود</span>
</UiButton>
</ExcelImportFlow>
<UiButton variant="primary" @click="activityModal = {}">
<UiIcon name="plus" :size="16" />
فعالیت جدید
+42 -47
View File
@@ -5,6 +5,7 @@ import axios from 'axios';
import { api } from '@/lib/api';
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
import CustomerMoodFace from '@/components/CustomerMoodFace.vue';
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
import {
UiButton,
UiModal,
@@ -52,7 +53,6 @@ const error = ref('');
const importMsg = ref('');
const people = ref<PaginatedResponse<PersonDto> | null>(null);
const companies = ref<PaginatedResponse<CompanyDto> | null>(null);
const fileInput = ref<HTMLInputElement | null>(null);
const menuId = ref<string | null>(null);
const sortOpen = ref(false);
const filterOpen = ref(false);
@@ -187,46 +187,38 @@ async function exportExcel() {
}
}
function pickImportFile() {
fileInput.value?.click();
}
const importPreviewPath = computed(() =>
tab.value === 'people' ? '/people/import/preview' : '/companies/import/preview',
);
const importPath = computed(() =>
tab.value === 'people' ? '/people/import' : '/companies/import',
);
async function onImportFile(ev: Event) {
const input = ev.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file) return;
function onImportStart() {
busy.value = true;
importMsg.value = '';
error.value = '';
try {
const form = new FormData();
form.append('file', file);
const path = tab.value === 'people' ? '/people/import' : '/companies/import';
const { data } = await api.post<ImportContactsResult>(path, form, {
headers: { 'Content-Type': undefined },
});
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}`;
if (data.errors.length) {
const sample = data.errors
.slice(0, 3)
.map((e) => `ردیف ${e.row}: ${e.message}`)
.join(' | ');
error.value = sample;
}
await load();
} catch (e) {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'ایمپورت ناموفق بود');
} else {
error.value = 'ایمپورت ناموفق بود';
}
} finally {
busy.value = false;
}
async function onImportDone(data: ImportContactsResult) {
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}`;
if (data.errors.length) {
error.value = data.errors
.slice(0, 3)
.map((e) => `ردیف ${e.row}: ${e.message}`)
.join(' | ');
}
await load();
}
function onImportFail(message: string) {
error.value = message;
}
function onImportSettled() {
busy.value = false;
}
watch(
@@ -251,17 +243,20 @@ onMounted(load);
<span class="btn-full">خروجی اکسل</span>
<span class="btn-short">خروجی</span>
</UiButton>
<UiButton variant="ghost" size="sm" :disabled="busy" @click.stop="pickImportFile">
<span class="btn-full">ورود از اکسل</span>
<span class="btn-short">ورود</span>
</UiButton>
<input
ref="fileInput"
type="file"
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
hidden
@change="onImportFile"
/>
<ExcelImportFlow
:preview-path="importPreviewPath"
:import-path="importPath"
:disabled="busy"
@start="onImportStart"
@done="onImportDone"
@fail="onImportFail"
@settled="onImportSettled"
>
<UiButton variant="ghost" size="sm" :disabled="busy">
<span class="btn-full">ورود از اکسل</span>
<span class="btn-short">ورود</span>
</UiButton>
</ExcelImportFlow>
<UiButton
variant="primary"
@click="contactModal = tab === 'companies' ? 'company' : 'person'"
+4 -4
View File
@@ -196,12 +196,12 @@ onMounted(load);
<form v-else class="crm-form" @submit.prevent="submit">
<UiCard>
<div class="crm-form-grid-2">
<UiField label="نام" required for-id="firstName">
<UiInput id="firstName" v-model="firstName" required />
<UiField label="نام" for-id="firstName">
<UiInput id="firstName" v-model="firstName" />
</UiField>
<UiField label="نام خانوادگی" required for-id="lastName">
<UiInput id="lastName" v-model="lastName" required />
<UiField label="نام خانوادگی" for-id="lastName">
<UiInput id="lastName" v-model="lastName" />
</UiField>
<UiField label="موبایل" for-id="mobile">
+39 -44
View File
@@ -16,6 +16,7 @@ import {
moveStageToIndex,
updateDealStage,
} from '@/lib/pipeline-board';
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
import UiButton from '@/components/ui/UiButton.vue';
import UiInput from '@/components/ui/UiInput.vue';
import UiModal from '@/components/ui/UiModal.vue';
@@ -47,7 +48,6 @@ const deleteStageId = ref<string | null>(null);
const stageBusy = ref(false);
const busy = ref(false);
const importMsg = ref('');
const fileInput = ref<HTMLInputElement | null>(null);
const draggingId = ref<string | null>(null);
const draggingColumnId = ref<string | null>(null);
@@ -390,43 +390,35 @@ async function exportExcel() {
}
}
function pickImportFile() {
fileInput.value?.click();
}
async function onImportFile(ev: Event) {
const input = ev.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file) return;
function onImportStart() {
busy.value = true;
importMsg.value = '';
error.value = '';
try {
const form = new FormData();
form.append('file', file);
const { data } = await api.post<ImportContactsResult>('/deals/import', form, {
headers: { 'Content-Type': undefined },
});
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}${data.boards?.length ? ` · کاریز: ${data.boards.join('، ')}` : ''}`;
if (data.errors.length) {
error.value = data.errors
.slice(0, 3)
.map((e) => `ردیف ${e.row}: ${e.message}`)
.join(' | ');
}
if (data.boardId && data.boardId !== selectedPipelineId.value) {
await router.replace({ name: 'pipeline', query: { pipelineId: data.boardId } });
} else {
await load();
}
} catch (e) {
error.value = apiMessage(e, 'ایمپورت ناموفق بود');
} finally {
busy.value = false;
}
async function onImportDone(data: ImportContactsResult) {
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}${data.boards?.length ? ` · کاریز: ${data.boards.join('، ')}` : ''}`;
if (data.errors.length) {
error.value = data.errors
.slice(0, 3)
.map((e) => `ردیف ${e.row}: ${e.message}`)
.join(' | ');
}
if (data.boardId && data.boardId !== selectedPipelineId.value) {
await router.replace({ name: 'pipeline', query: { pipelineId: data.boardId } });
} else {
await load();
}
}
function onImportFail(message: string) {
error.value = message;
}
function onImportSettled() {
busy.value = false;
}
function onMenuDeal(dealId: string) {
@@ -472,16 +464,19 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
<button class="btn-ghost-sm" type="button" :disabled="loading || busy" @click="exportExcel">
خروجی اکسل
</button>
<button class="btn-ghost-sm" type="button" :disabled="loading || busy" @click="pickImportFile">
ورود از اکسل
</button>
<input
ref="fileInput"
type="file"
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
hidden
@change="onImportFile"
/>
<ExcelImportFlow
preview-path="/deals/import/preview"
import-path="/deals/import"
:disabled="loading || busy"
@start="onImportStart"
@done="onImportDone"
@fail="onImportFail"
@settled="onImportSettled"
>
<button class="btn-ghost-sm" type="button" :disabled="loading || busy">
ورود از اکسل
</button>
</ExcelImportFlow>
</template>
</PipelineToolbar>
+33 -45
View File
@@ -15,6 +15,7 @@ import {
UiPagination,
UiSelect,
} from '@/components/ui';
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
import UiIcon from '@/components/UiIcon.vue';
import ProductFormView from '@/views/ProductFormView.vue';
import type { ImportContactsResult, ProductDto } from '@mento/shared';
@@ -41,7 +42,6 @@ const moreOpen = ref(false);
const productModal = ref(false);
const busy = ref(false);
const importMsg = ref('');
const fileInput = ref<HTMLInputElement | null>(null);
const categories = computed(() => {
const set = new Set<string>();
@@ -187,45 +187,32 @@ async function exportExcel() {
}
}
function pickImportFile() {
function onImportStart() {
closeMenus();
fileInput.value?.click();
}
async function onImportFile(ev: Event) {
const input = ev.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file) return;
busy.value = true;
importMsg.value = '';
error.value = '';
try {
const form = new FormData();
form.append('file', file);
const { data } = await api.post<ImportContactsResult>('/products/import', form, {
headers: { 'Content-Type': undefined },
});
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}`;
if (data.errors.length) {
error.value = data.errors
.slice(0, 3)
.map((e) => `ردیف ${e.row}: ${e.message}`)
.join(' | ');
}
await load();
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
(e.response?.data as { message?: string | string[] })?.message ??
'ایمپورت ناموفق بود',
)
: 'ایمپورت ناموفق بود';
} finally {
busy.value = false;
}
async function onImportDone(data: ImportContactsResult) {
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}`;
if (data.errors.length) {
error.value = data.errors
.slice(0, 3)
.map((e) => `ردیف ${e.row}: ${e.message}`)
.join(' | ');
}
await load();
}
function onImportFail(message: string) {
error.value = message;
}
function onImportSettled() {
busy.value = false;
}
async function toggleActive(product: ProductDto) {
@@ -270,16 +257,17 @@ onMounted(load);
<UiButton variant="ghost" type="button" :disabled="busy" @click="exportExcel">
خروجی اکسل
</UiButton>
<UiButton variant="ghost" type="button" :disabled="busy" @click="pickImportFile">
ورود از اکسل
</UiButton>
<input
ref="fileInput"
type="file"
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
hidden
@change="onImportFile"
/>
<ExcelImportFlow
preview-path="/products/import/preview"
import-path="/products/import"
:disabled="busy"
@start="onImportStart"
@done="onImportDone"
@fail="onImportFail"
@settled="onImportSettled"
>
<UiButton variant="ghost" type="button" :disabled="busy">ورود از اکسل</UiButton>
</ExcelImportFlow>
<UiButton variant="primary" @click="productModal = true">
<UiIcon name="plus" :size="16" />
محصول جدید
+39 -46
View File
@@ -17,6 +17,7 @@ import {
moveStageToIndex,
updateDealStage,
} from '@/lib/pipeline-board';
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
import UiButton from '@/components/ui/UiButton.vue';
import UiInput from '@/components/ui/UiInput.vue';
import UiModal from '@/components/ui/UiModal.vue';
@@ -50,7 +51,6 @@ const deleteStageId = ref<string | null>(null);
const stageBusy = ref(false);
const busy = ref(false);
const importMsg = ref('');
const fileInput = ref<HTMLInputElement | null>(null);
const draggingId = ref<string | null>(null);
const draggingColumnId = ref<string | null>(null);
@@ -387,45 +387,35 @@ async function exportExcel() {
}
}
function pickImportFile() {
fileInput.value?.click();
}
async function onImportFile(ev: Event) {
const input = ev.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file) return;
function onImportStart() {
busy.value = true;
importMsg.value = '';
error.value = '';
try {
const form = new FormData();
form.append('file', file);
const { data } = await api.post<ImportContactsResult>(
'/project-cards/import',
form,
{ headers: { 'Content-Type': undefined } },
);
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}${data.boards?.length ? ` · برد: ${data.boards.join('، ')}` : ''}`;
if (data.errors.length) {
error.value = data.errors
.slice(0, 3)
.map((e) => `ردیف ${e.row}: ${e.message}`)
.join(' | ');
}
if (data.boardId && data.boardId !== selectedBoardId.value) {
await router.replace({ name: 'projects', query: { boardId: data.boardId } });
} else {
await load();
}
} catch (e) {
error.value = apiMessage(e, 'ایمپورت ناموفق بود');
} finally {
busy.value = false;
}
async function onImportDone(data: ImportContactsResult) {
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}${data.boards?.length ? ` · برد: ${data.boards.join('، ')}` : ''}`;
if (data.errors.length) {
error.value = data.errors
.slice(0, 3)
.map((e) => `ردیف ${e.row}: ${e.message}`)
.join(' | ');
}
if (data.boardId && data.boardId !== selectedBoardId.value) {
await router.replace({ name: 'projects', query: { boardId: data.boardId } });
} else {
await load();
}
}
function onImportFail(message: string) {
error.value = message;
}
function onImportSettled() {
busy.value = false;
}
function onMenuCard(cardId: string) {
@@ -470,16 +460,19 @@ onBeforeUnmount(() => {
<button class="btn-ghost-sm" type="button" :disabled="loading || busy" @click="exportExcel">
خروجی اکسل
</button>
<button class="btn-ghost-sm" type="button" :disabled="loading || busy" @click="pickImportFile">
ورود از اکسل
</button>
<input
ref="fileInput"
type="file"
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
hidden
@change="onImportFile"
/>
<ExcelImportFlow
preview-path="/project-cards/import/preview"
import-path="/project-cards/import"
:disabled="loading || busy"
@start="onImportStart"
@done="onImportDone"
@fail="onImportFail"
@settled="onImportSettled"
>
<button class="btn-ghost-sm" type="button" :disabled="loading || busy">
ورود از اکسل
</button>
</ExcelImportFlow>
</template>
</PipelineToolbar>