fix(money,orders): wage/gram-debt parity, atomic document confirm, expert edit, made-to-order, QA fixes
- Rial wage and gram-debt arithmetic matched to the reference; order corrections keep debt in sync - Document confirm/un-confirm is a compare-and-swap (no double-applied cache under concurrency) - Settlement document and expert inputs validated (400 instead of 500) - Expert: edit / activate-deactivate own customers, adjust raw weights only - Made-to-order flow in the app, PreOrder.quantity migration - Order grids: customer column, sort keys; panel select search; pre-order quantity save - Rebuilt release artifacts Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
959a272227
commit
6514b05d18
@@ -195,6 +195,8 @@ async function editW() {
|
||||
color: tempRow.value?.color,
|
||||
color_mina: tempRow.value?.color_mina,
|
||||
color_stone: tempRow.value?.color_stone,
|
||||
// The quantity field sits in the same edit mode; without it the edit was shown but never kept.
|
||||
sum: tempRow.value?.sum,
|
||||
});
|
||||
editMode.value = false;
|
||||
message.info('ویرایش موفق آمیز');
|
||||
|
||||
@@ -109,21 +109,50 @@
|
||||
</a-descriptions>
|
||||
|
||||
<a-card title="آیتم های سفارش">
|
||||
<div v-for="(item, index) in tempRow.items" :key="index">
|
||||
<template v-if="hasPermission('order.omde.view.edit')" #extra>
|
||||
<a-button size="small" :type="editMode ? 'primary' : 'default'" @click="editMode = !editMode">
|
||||
{{ editMode ? 'پایان ویرایش' : 'ویرایش ردیفها' }}
|
||||
</a-button>
|
||||
</template>
|
||||
<div v-for="(item, index) in tempRow.items" :key="item.id ?? index">
|
||||
<!-- Correcting a line (reference order-item/edit): the raw weight, the
|
||||
quantity and the percent actually charged. The order's totals and
|
||||
the customer's debt are recomputed by the server on save. -->
|
||||
<div v-if="editMode && hasPermission('order.omde.view.edit')" class="line-edit">
|
||||
<a-tag color="blue">{{ item.title }} — {{ item.variant_title }}</a-tag>
|
||||
<label>وزن خام (گرم)<a-input-number v-model:value="item.rash" :min="0.001" :step="0.01" /></label>
|
||||
<label>تعداد<a-input-number v-model:value="item.qty" :min="1" :precision="0" /></label>
|
||||
<label>درصد اجرت<a-input-number v-model:value="item.percent" :step="0.1" /></label>
|
||||
<a-button type="primary" size="small" :loading="savingLine === item.id" @click="saveLine(item)">ذخیره</a-button>
|
||||
<a-popconfirm
|
||||
title="این ردیف از سفارش حذف شود؟"
|
||||
ok-text="بله"
|
||||
cancel-text="بی خیال"
|
||||
@confirm="removeLine(item)"
|
||||
>
|
||||
<a-button danger size="small">حذف ردیف</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
<a-descriptions>
|
||||
<a-descriptions-item label="نام محصول">
|
||||
<a-tag color="blue">{{ item.title }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="عنوان تنوع">{{ item.variant_title }}</a-descriptions-item>
|
||||
<a-descriptions-item label="وزن نهایی">
|
||||
<a-descriptions-item label="وزن نهایی (هر عدد)">
|
||||
<a-tag color="red">{{ item.grams }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="تعداد">
|
||||
<a-tag color="orange">{{ item.qty }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="Number(item.flatRialCharge) > 0" label="اجرت ریالی">
|
||||
<a-descriptions-item label="جمع وزن ردیف">
|
||||
<a-tag color="red">{{ formatGrams(item.total_grams) }} گرم</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="Number(item.flatRialCharge) > 0" label="اجرت ریالی (هر عدد)">
|
||||
<a-tag>{{ Number(item.flatRialCharge).toLocaleString('fa-IR') }} ریال</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="Number(item.total_rial) > 0" label="جمع اجرت ریالی ردیف">
|
||||
<a-tag>{{ Number(item.total_rial).toLocaleString('fa-IR') }} ریال</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-divider v-if="index < tempRow.items.length - 1" />
|
||||
</div>
|
||||
@@ -161,6 +190,9 @@ const visibleSts = ref(false);
|
||||
|
||||
const formatDate = (v: string | null) => (v ? jdate(v) : '');
|
||||
|
||||
/** Grams to four places with trailing zeros dropped (the column's own precision). */
|
||||
const formatGrams = (v: unknown) => (v === null || v === undefined ? '-' : String(Number(Number(v).toFixed(4))));
|
||||
|
||||
/** legacy getCity(): resolve a city id through the users store lookup */
|
||||
function getCity(id: string | number) {
|
||||
return users.cityFilters[id] ?? '-';
|
||||
@@ -177,9 +209,49 @@ async function loadDetail(id: string) {
|
||||
|
||||
async function viewOrder(row: Record<string, any>) {
|
||||
tempRow.value = await loadDetail(row.id);
|
||||
editMode.value = false;
|
||||
visibleList.value = true;
|
||||
}
|
||||
|
||||
// ── correcting a line of the order (reference order-item/edit, order_item delete)
|
||||
const editMode = ref(false);
|
||||
const savingLine = ref<string | null>(null);
|
||||
|
||||
function errorText(e: any, fallback: string) {
|
||||
return e?.response?.data?.message ?? fallback;
|
||||
}
|
||||
|
||||
async function saveLine(item: Record<string, any>) {
|
||||
try {
|
||||
savingLine.value = item.id;
|
||||
const { data } = await api.post('admin/orders/item/edit', {
|
||||
id: item.id,
|
||||
rash_weight: item.rash,
|
||||
qty: item.qty,
|
||||
percent: item.percent,
|
||||
});
|
||||
tempRow.value = data;
|
||||
message.info('ردیف سفارش ویرایش شد؛ جمع و بدهی مشتری بهروز شد');
|
||||
tbl.value?.fetch();
|
||||
} catch (e) {
|
||||
message.error(errorText(e, 'خطا در ویرایش ردیف'));
|
||||
if (tempRow.value) tempRow.value = await loadDetail(tempRow.value.id);
|
||||
} finally {
|
||||
savingLine.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeLine(item: Record<string, any>) {
|
||||
try {
|
||||
const { data } = await api.delete(`admin/orders/item/${item.id}`);
|
||||
tempRow.value = data;
|
||||
message.info('ردیف از سفارش حذف شد؛ جمع و بدهی مشتری بهروز شد');
|
||||
tbl.value?.fetch();
|
||||
} catch (e) {
|
||||
message.error(errorText(e, 'خطا در حذف ردیف'));
|
||||
}
|
||||
}
|
||||
|
||||
async function showSts(row: Record<string, any>) {
|
||||
tempRow.value = await loadDetail(row.id);
|
||||
visibleSts.value = true;
|
||||
@@ -237,4 +309,20 @@ function deleteOrder(record: Record<string, any>) {
|
||||
.mr-3 {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
.line-edit {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px dashed var(--ant-color-border, #d9d9d9);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.line-edit label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,7 +22,9 @@ export function buildOmdeListModel(): XTbl {
|
||||
new Xtc('customer.mobile', 'موبایل').filterText().hidden().noSort(),
|
||||
);
|
||||
tbl.add(new Xtc('input', 'جمع کل سفارش').filterKey('totalGoldWeightGrams').filterText());
|
||||
tbl.add(new Xtc('discount', 'جمع کل تخفیف (درصد)').filterText());
|
||||
tbl.add(new Xtc('wage', 'اجرت (گرم)').filterKey('wageWeightGrams').filterText().noSort());
|
||||
tbl.add(new Xtc('rial_input', 'اجرت ریالی (ریال)').filterKey('flatRialCharge').filterText());
|
||||
tbl.add(new Xtc('discount', 'جمع کل تخفیف (درصد)').noSort());
|
||||
// legacy kept the .filterSelect(cityFilters) call commented out — preserved.
|
||||
tbl.add(new Xtc('customer.city_id', 'شهر').hidden().renderSlot('city'));
|
||||
tbl.add(new Xtc('items', 'تعداد آیتم ها').renderSlot('items_len').noSort());
|
||||
|
||||
@@ -102,15 +102,21 @@
|
||||
<a-tag color="blue">{{ item.title }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="عنوان تنوع">{{ item.variant_title }}</a-descriptions-item>
|
||||
<a-descriptions-item label="وزن نهایی">
|
||||
<a-descriptions-item label="وزن نهایی (هر عدد)">
|
||||
<a-tag color="red">{{ item.grams }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="تعداد">
|
||||
<a-tag color="orange">{{ item.qty }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="Number(item.flatRialCharge) > 0" label="اجرت ریالی">
|
||||
<a-descriptions-item label="جمع وزن ردیف">
|
||||
<a-tag color="red">{{ formatGrams(item.total_grams) }} گرم</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="Number(item.flatRialCharge) > 0" label="اجرت ریالی (هر عدد)">
|
||||
<a-tag>{{ Number(item.flatRialCharge).toLocaleString('fa-IR') }} ریال</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="Number(item.total_rial) > 0" label="جمع اجرت ریالی ردیف">
|
||||
<a-tag>{{ Number(item.total_rial).toLocaleString('fa-IR') }} ریال</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-divider v-if="index < tempRow.items.length - 1" />
|
||||
</div>
|
||||
@@ -148,6 +154,9 @@ const visibleSts = ref(false);
|
||||
|
||||
const formatDate = (v: string | null) => (v ? jdate(v) : '');
|
||||
|
||||
/** Grams to four places with trailing zeros dropped (the column's own precision). */
|
||||
const formatGrams = (v: unknown) => (v === null || v === undefined ? '-' : String(Number(Number(v).toFixed(4))));
|
||||
|
||||
function getColorSts(row: { proceed?: boolean }) {
|
||||
return row.proceed ? 'green' : 'gray';
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ export function buildTaminListModel(): XTbl {
|
||||
tbl.add(new Xtc('customer.name', 'مشتری').filterKey('customer.fullName').filterText());
|
||||
tbl.add(new Xtc('customer.mobile', 'موبایل').filterText().hidden().noSort());
|
||||
tbl.add(new Xtc('input', 'ورودی').filterKey('totalGoldWeightGrams').filterText());
|
||||
tbl.add(new Xtc('wage', 'اجرت').filterKey('wageWeightGrams').filterText().noSort());
|
||||
tbl.add(new Xtc('wage', 'اجرت (گرم)').filterKey('wageWeightGrams').filterText().noSort());
|
||||
tbl.add(new Xtc('rial_input', 'اجرت ریالی (ریال)').filterKey('flatRialCharge').filterText());
|
||||
tbl.add(new Xtc('items', 'تعداد آیتم ها').renderSlot('items_len').noSort());
|
||||
tbl.add(new Xtc('caj', 'تاریخ ثبت').filterKey('createdAt').filterDate().noSort());
|
||||
tbl.add(new Xtc('uaj', 'تاریخ ویرایش').hidden().filterKey('updatedAt').filterDate().noSort());
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { optionText } from '../../utils/select';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
@@ -382,7 +383,7 @@ async function editGram() {
|
||||
}
|
||||
|
||||
function filterOption(input: string, option: any) {
|
||||
return String(option?.children?.[0]?.children ?? '')
|
||||
return optionText(option)
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase());
|
||||
}
|
||||
@@ -399,16 +400,15 @@ async function submitOrder() {
|
||||
if (!validate()) return;
|
||||
try {
|
||||
loading.value = true;
|
||||
const order_items = items.value.map((x) => {
|
||||
const v = getVariant(x);
|
||||
return {
|
||||
vid: x.vid,
|
||||
qty: x.qty,
|
||||
percent: v.percentage_pay,
|
||||
promotion_id: x.promotion_id,
|
||||
rial_input: lineRial(x),
|
||||
};
|
||||
});
|
||||
// No `percent`: the server prices each line at what the app would charge
|
||||
// this customer (their percent + the variant's − the largest promotion),
|
||||
// so the same order costs the same from here as from the app.
|
||||
const order_items = items.value.map((x) => ({
|
||||
vid: x.vid,
|
||||
qty: x.qty,
|
||||
promotion_id: x.promotion_id,
|
||||
rial_input: lineRial(x),
|
||||
}));
|
||||
|
||||
// Legacy also sent browser-computed total_price / total_weight /
|
||||
// gold_price_original. The server recomputes those from the variant
|
||||
|
||||
Reference in New Issue
Block a user