feat: port QA/money fixes from tehran_taj (atomic document confirm, validated inputs, expert customer edit/toggle/weights, made-to-order, grid sort/customer column)

Order-revision workflow kept as is: the direct admin line-edit routes were not ported.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
saeed
2026-10-29 18:43:22 +03:30
co-authored by Claude Sonnet 5.5
parent 756ecef6db
commit 3eaecde876
65 changed files with 3810 additions and 216 deletions
@@ -303,9 +303,13 @@ const filterableOptions = computed(() =>
);
/** legacy `order` computed: '+field' / '-field' */
const order = computed(() =>
sort.value ? `${sortDirection.value === 'asc' ? '+' : '-'}${sort.value}` : undefined,
);
const order = computed(() => {
if (!sort.value) return undefined;
// A column may be keyed by the reference's name for it; its filter key, when it
// has one, is the real field it stands for, so that is what is sorted on.
const col = cols.value.find((c) => c.key === sort.value);
return `${sortDirection.value === 'asc' ? '+' : '-'}${col?.__fkey ?? sort.value}`;
});
const antColumns = computed(() =>
visibleCols.value.map((c) => ({
+14
View File
@@ -0,0 +1,14 @@
/**
* The text of a slot-style `<a-select-option>`, for a custom `filter-option`.
*
* ant-design-vue 4 hands `filterOption` the option's default slot as a
* function, not as an array of vnodes (the shape the legacy filters indexed
* into), so `option.children[0].children` is always undefined and a typed
* search matched nothing. This calls the slot first.
*/
export function optionText(option: any): string {
const children = typeof option?.children === 'function' ? option.children() : option?.children;
const first = Array.isArray(children) ? children[0] : children;
if (typeof first === 'string') return first;
return String(first?.children ?? '');
}
@@ -136,6 +136,7 @@
</template>
<script setup lang="ts">
import { optionText } from '../../../utils/select';
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
@@ -197,7 +198,7 @@ watch(
);
function filterOption(input: string, option: any) {
return String(option?.children?.[0]?.children ?? '').includes(input);
return optionText(option).includes(input);
}
function validate(): boolean {
@@ -299,6 +299,7 @@
</template>
<script setup lang="ts">
import { optionText } from '../../../utils/select';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
@@ -380,7 +381,7 @@ const userTypeOptions = computed(() =>
);
function filterUserType(input: string, option: any) {
return String(option?.children?.[0]?.children ?? '').includes(input);
return optionText(option).includes(input);
}
const err = reactive<Record<string, string>>({});
@@ -502,6 +502,7 @@
</template>
<script setup lang="ts">
import { optionText } from '../../../utils/select';
import { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Empty, message } from 'ant-design-vue';
@@ -672,7 +673,7 @@ function saveVariant() {
}
function filterSelectOption(input: string, option: any) {
return String(option?.children?.[0]?.children ?? option?.title ?? '').toLowerCase().includes(input.toLowerCase());
return (optionText(option) || String(option?.title ?? '')).toLowerCase().includes(input.toLowerCase());
}
function handleBack() {
@@ -61,6 +61,7 @@
</template>
<script setup lang="ts">
import { optionText } from '../../../utils/select';
import { onMounted, reactive, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import api from '../../../api/client';
@@ -77,7 +78,7 @@ const form = reactive<Record<string, any>>({
});
function filterOption(input: string, option: any) {
return String(option?.children?.[0]?.children ?? '')
return optionText(option)
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0;
}
@@ -88,6 +88,7 @@
</template>
<script setup lang="ts">
import { optionText } from '../../utils/select';
import { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
@@ -124,7 +125,7 @@ const form = reactive<Record<string, any>>({
const err = reactive<Record<string, string>>({});
function filterOption(input: string, option: any) {
return String(option?.children?.[0]?.children ?? '').includes(input);
return optionText(option).includes(input);
}
async function handleSubmit() {
@@ -118,6 +118,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';
@@ -194,7 +195,7 @@ const totalWeight = computed(() => {
});
function filterOption(input: string, option: any) {
return String(option?.children?.[0]?.children ?? '')
return optionText(option)
.toLowerCase()
.includes(input.toLowerCase());
}
@@ -207,10 +208,8 @@ async function submitOrder() {
}
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 };
});
// No `percent`: the server prices each line like the app does for this customer.
const order_items = items.value.map((x) => ({ vid: x.vid, qty: x.qty }));
await api.post('order', {
customer_id: form.customer_id,
@@ -220,6 +220,7 @@
</template>
<script setup lang="ts">
import { optionText } from '../../utils/select';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
@@ -286,7 +287,7 @@ const cashFinalWeight = computed(() => {
});
function filterOption(input: string, option: any) {
return String(option?.children?.[0]?.children ?? '')
return optionText(option)
.toLowerCase()
.includes(input.toLowerCase());
}
@@ -161,6 +161,7 @@
</template>
<script setup lang="ts">
import { optionText } from '../../utils/select';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
@@ -215,7 +216,7 @@ function addToCart(record: Record<string, any>) {
}
function filterOption(input: string, option: any) {
return String(option?.children?.[0]?.children ?? '').includes(input);
return optionText(option).includes(input);
}
async function handleSubmit() {
@@ -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('ویرایش موفق آمیز');
@@ -333,4 +333,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());
@@ -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());
+4 -10
View File
@@ -191,6 +191,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';
@@ -350,7 +351,7 @@ async function editGram() {
}
function filterOption(input: string, option: any) {
return String(option?.children?.[0]?.children ?? '')
return optionText(option)
.toLowerCase()
.includes(input.toLowerCase());
}
@@ -367,15 +368,8 @@ 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,
};
});
// No `percent`: the server prices each line at what the app would charge this customer.
const order_items = items.value.map((x) => ({ vid: x.vid, qty: x.qty, promotion_id: x.promotion_id }));
// Legacy also sent browser-computed total_price / total_weight /
// gold_price_original. The server recomputes those from the variant
@@ -249,6 +249,7 @@
</template>
<script setup lang="ts">
import { optionText } from '../../utils/select';
import { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
@@ -313,7 +314,7 @@ function changeType() {
}
function filterOption(input: string, option: any) {
return String(option?.children?.[0]?.children ?? '')
return optionText(option)
.toLowerCase()
.includes(input.toLowerCase());
}
+2 -1
View File
@@ -42,6 +42,7 @@
</template>
<script setup lang="ts">
import { optionText } from '../../utils/select';
import { onMounted, ref } from 'vue';
import { jdateTime } from '../../utils/filters';
import api from '../../api/client';
@@ -58,7 +59,7 @@ const columns = [
];
function filterOption(input: string, option: any) {
return String(option?.children?.[0]?.children ?? '').includes(input);
return optionText(option).includes(input);
}
async function load() {