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:
co-authored by
Claude Sonnet 5.5
parent
756ecef6db
commit
3eaecde876
@@ -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) => ({
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user