Adding a task generated from the account balance to products

This commit is contained in:
2026-09-21 21:25:34 +03:30
parent be97a067f9
commit 17fb33b3e7
10 changed files with 700 additions and 0 deletions
+1
View File
@@ -23,6 +23,7 @@ import type {
} from '@mento/shared';
const CATEGORIES = [
'کارساخته',
'طلا و جواهر',
'سکه',
'خدمات',
+225
View File
@@ -56,12 +56,35 @@ type SyncResult = {
};
const syncResults = ref<SyncResult[]>([]);
const productSync = reactive({
connectionId: '',
q: '',
});
const productSyncBusy = ref(false);
type ProductSyncResult = {
name: string;
ayar: number | null;
vaznSabet: number | null;
lastSanad: string;
alreadyImported?: boolean;
productId?: string | null;
productName?: string | null;
};
const productSyncResults = ref<ProductSyncResult[]>([]);
const productCategoryLabel = ref('کارساخته');
function pendingImportCodes() {
return syncResults.value
.filter((r) => r.moshtariCode != null && !r.alreadyLinked)
.map((r) => r.moshtariCode as number);
}
function pendingProductNames() {
return productSyncResults.value
.filter((r) => !r.alreadyImported)
.map((r) => r.name);
}
function apiErr(e: unknown, fallback: string) {
if (!axios.isAxiosError(e)) return fallback;
const msg = (e.response?.data as { message?: string | string[] })?.message;
@@ -117,6 +140,7 @@ async function load() {
items.value = data;
if (!editingId.value) form.isDefault = data.length === 0;
if (!sync.connectionId && data[0]) sync.connectionId = data[0].id;
if (!productSync.connectionId && data[0]) productSync.connectionId = data[0].id;
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
@@ -381,6 +405,107 @@ async function importBulk(mode: 'results' | 'range') {
}
}
async function searchProducts() {
error.value = '';
ok.value = '';
if (!productSync.connectionId) {
error.value = 'ابتدا اتصال را برای کارساخته انتخاب کنید.';
return;
}
try {
productSyncBusy.value = true;
const body: Record<string, unknown> = {
connectionId: productSync.connectionId,
};
if (productSync.q.trim()) body.q = productSync.q.trim();
const { data } = await api.post<{
results: ProductSyncResult[];
categoryLabel?: string;
}>('/integrations/tahesab/sync/products/search', body, { timeout: 60_000 });
productSyncResults.value = data.results ?? [];
if (data.categoryLabel) productCategoryLabel.value = data.categoryLabel;
if (!productSyncResults.value.length) {
ok.value = 'کارساخته‌ای یافت نشد.';
} else {
const linked = productSyncResults.value.filter((r) => r.alreadyImported).length;
ok.value =
`${productSyncResults.value.length} ${productCategoryLabel.value} پیدا شد` +
(linked ? ` (${linked} قبلاً وارد شده)` : '');
}
} catch (e) {
error.value = apiErr(e, 'دریافت لیست کارساخته ناموفق');
} finally {
productSyncBusy.value = false;
}
}
async function importProductOne(name: string) {
error.value = '';
ok.value = '';
try {
productSyncBusy.value = true;
const { data } = await api.post<{ productId: string }>(
'/integrations/tahesab/sync/products/import',
{ connectionId: productSync.connectionId, name },
);
ok.value = `وارد شد — در محصولات با شناسه ${data.productId}`;
const row = productSyncResults.value.find((r) => r.name === name);
if (row) {
row.alreadyImported = true;
row.productId = data.productId;
}
} catch (e) {
error.value = apiErr(e, 'وارد کردن محصول ناموفق');
} finally {
productSyncBusy.value = false;
}
}
async function importProductsBulk(mode: 'pending' | 'all') {
error.value = '';
ok.value = '';
if (!productSync.connectionId) {
error.value = 'ابتدا اتصال را انتخاب کنید.';
return;
}
const body: Record<string, unknown> = { connectionId: productSync.connectionId };
if (mode === 'pending') {
const names = pendingProductNames();
if (!names.length) {
error.value = `${productCategoryLabel.value} جدیدی برای وارد کردن نیست.`;
return;
}
if (
!confirm(
`${names.length} ${productCategoryLabel.value} به‌عنوان محصول در ${brand.nameFa} ساخته می‌شود. ادامه؟`,
)
) {
return;
}
body.names = names;
} else if (
!confirm(
`همهٔ نام‌های ${productCategoryLabel.value} از ته‌حساب وارد محصولات ${brand.nameFa} می‌شوند. ادامه؟`,
)
) {
return;
}
try {
productSyncBusy.value = true;
const { data } = await api.post<{
imported: number;
skipped: number;
failed: number;
}>('/integrations/tahesab/sync/products/import-bulk', body, { timeout: 120_000 });
ok.value = `وارد شد: ${data.imported} · قبلاً بود: ${data.skipped} · ناموفق: ${data.failed}`;
await searchProducts();
} catch (e) {
error.value = apiErr(e, 'وارد کردن دسته‌ای محصولات ناموفق');
} finally {
productSyncBusy.value = false;
}
}
onMounted(load);
</script>
@@ -636,6 +761,106 @@ onMounted(load);
</div>
</div>
</UiCard>
<UiCard title="وارد کردن کارساخته از ته‌حساب به محصولات" class="section-card">
<p class="sync-desc">
نام‌های کارساختهٔ ته‌حساب (لیست <span class="font-mono" dir="ltr">DoListNameKarsakhte</span>)
را به‌عنوان محصول با دستهٔ «{{ productCategoryLabel }}» وارد کنید.
موجودی همچنان از ته‌حساب است؛ اینجا فقط کاتالوگ برای معامله/فاکتور ساخته می‌شود.
</p>
<div class="crm-form">
<UiField label="اتصال مورد نظر" required>
<UiSelect v-model="productSync.connectionId">
<option disabled value="">انتخاب اتصال</option>
<option v-for="row in items" :key="row.id" :value="row.id">
{{ row.label }} ({{ row.dbName }})
</option>
</UiSelect>
</UiField>
<div class="sync-sub-section">
<h4 class="sync-subhead">لیست و جستجو</h4>
<div class="crm-form-grid-2">
<UiInput
v-model="productSync.q"
placeholder="جستجو نام یا عیار…"
@keydown.enter.prevent="searchProducts"
/>
<div class="sync-buttons" style="margin-top: 0">
<UiButton
variant="secondary"
:disabled="!productSync.connectionId || productSyncBusy"
@click="searchProducts"
>
دریافت لیست
</UiButton>
<UiButton
:disabled="!productSync.connectionId || productSyncBusy || !pendingProductNames().length"
@click="importProductsBulk('pending')"
>
ورود نتایج جدید ({{ pendingProductNames().length }})
</UiButton>
<UiButton
variant="ghost"
:disabled="!productSync.connectionId || productSyncBusy"
@click="importProductsBulk('all')"
>
ورود همه
</UiButton>
</div>
</div>
</div>
<div v-if="productSyncResults.length" class="sync-results-box">
<h4 class="sync-results-title">
نتایج ({{ productSyncResults.length }}) — دسته: {{ productCategoryLabel }}
</h4>
<div class="crm-table-container">
<table class="crm-table">
<thead>
<tr>
<th>نام</th>
<th>عیار</th>
<th>آخرین سند</th>
<th>وضعیت</th>
<th>عملیات</th>
</tr>
</thead>
<tbody>
<tr v-for="r in productSyncResults" :key="r.name">
<td class="font-bold">{{ r.name }}</td>
<td class="font-mono" dir="ltr">{{ r.ayar ?? '—' }}</td>
<td>{{ r.lastSanad || '—' }}</td>
<td>
<UiBadge v-if="r.alreadyImported" variant="success">
وارد شده{{ r.productName ? ` (${r.productName})` : '' }}
</UiBadge>
<UiBadge v-else variant="neutral">آماده ورود</UiBadge>
</td>
<td>
<UiButton
v-if="!r.alreadyImported"
size="sm"
:disabled="productSyncBusy"
@click="importProductOne(r.name)"
>
وارد کردن
</UiButton>
<RouterLink
v-else-if="r.productId"
:to="{ name: 'product-edit', params: { id: r.productId } }"
>
<UiButton variant="ghost" size="sm">مشاهده محصول</UiButton>
</RouterLink>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</UiCard>
</section>
</template>