Files
fabric_trade_user/src/views/checkout/CheckListView.vue
T

559 lines
15 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useCartStore } from '@/stores/cart'
import { useOrderStore } from '@/stores/order'
import { useAuthStore } from '@/stores/auth'
import BaseButton from '@/components/ui/BaseButton.vue'
import EmptyState from '@/components/ui/EmptyState.vue'
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
import { formatCurrency, formatDate, formatNumber, toPersianDigits } from '@/utils/format'
import { calculateCheckRass } from '@/utils/checkRass'
import { confirm } from '@/composables/useConfirm'
import type { CheckData } from '@/types'
const route = useRoute()
const router = useRouter()
const cartStore = useCartStore()
const orderStore = useOrderStore()
const authStore = useAuthStore()
const isCheckout = computed(() => route.name === 'check-list')
const checks = computed<CheckData[]>(() => {
if (orderStore.draftChecks.length > 0) return orderStore.draftChecks
if (!authStore.user) return []
return orderStore
.userOrders(authStore.user.id)
.filter((order) => order.settlementType === 'check' && order.checks?.length)
.flatMap((order) => order.checks ?? [])
})
const rass = computed(() => {
if (orderStore.draftChecks.length > 0) return orderStore.getDraftRass()
if (checks.value.length === 0) {
return calculateCheckRass([], new Date())
}
return calculateCheckRass(checks.value, new Date())
})
const invoiceAmount = computed(() => {
if (!authStore.user) return 0
const activeOrder = orderStore
.userOrders(authStore.user.id)
.find((order) => order.settlementType === 'check' && order.finalAmount)
if (activeOrder?.finalAmount) return activeOrder.finalAmount
if (isCheckout.value && rass.value.totalAmount > 0) return rass.value.totalAmount
return rass.value.totalAmount
})
const isAmountComplete = computed(
() => invoiceAmount.value > 0 && rass.value.totalAmount >= invoiceAmount.value,
)
const openMenuId = computed({
get: () => (route.query.menu as string) ?? '',
set: (id: string) => {
router.replace({ query: id ? { menu: id } : {} })
},
})
function goBack() {
router.push(isCheckout.value ? '/checkout/check-form' : '/profile')
}
function addAnother() {
router.push('/checkout/check-form')
}
async function removeCheck(id: string) {
openMenuId.value = ''
const ok = await confirm({
title: 'حذف چک',
message: 'آیا از حذف این چک مطمئن هستید؟',
confirmLabel: 'حذف',
cancelLabel: 'انصراف',
tone: 'danger',
})
if (!ok) return
orderStore.removeDraftCheck(id)
}
function toggleMenu(id: string) {
openMenuId.value = openMenuId.value === id ? '' : id
}
function submitOrder() {
if (!authStore.user || !cartStore.activeCart || checks.value.length === 0) return
const order = orderStore.submitOrder(
authStore.user.id,
cartStore.activeCart,
'check',
[...orderStore.draftChecks],
orderStore.draftCheckMode ?? 'brief',
)
cartStore.clearActiveCart()
router.replace(`/orders/${order.id}/success`)
}
</script>
<template>
<!-- Mobile -->
<div class="check-list-page mobile-only">
<header class="check-list-page__nav">
<button type="button" class="nav-btn" aria-label="بازگشت" @click="goBack">
<IconChevronBack :size="20" />
</button>
<h1 class="check-list-page__title">چک‌های ثبت شده</h1>
<button type="button" class="nav-btn nav-btn--ghost" aria-hidden="true" tabindex="-1">
<IconChevronBack :size="18" class="nav-btn__flip" />
</button>
</header>
<div v-if="checks.length === 0" class="check-list-page__empty page__scroll page__scroll--with-nav">
<p>چکی ثبت نشده است.</p>
<BaseButton v-if="isCheckout" @click="addAnother">ثبت چک جدید</BaseButton>
<BaseButton v-else variant="outline" @click="router.push('/')">ثبت سفارش جدید</BaseButton>
</div>
<template v-else>
<div class="check-list-page__scroll page__scroll">
<div class="summary-cards">
<div class="summary-card">
<span class="summary-card__label">مجموع مبلغ چک‌ها</span>
<strong class="summary-card__value">{{ formatNumber(rass.totalAmount) }}</strong>
<span class="summary-card__unit">تومان</span>
</div>
<div class="summary-card">
<span class="summary-card__label">مبلغ فاکتور</span>
<strong class="summary-card__value">{{ formatNumber(invoiceAmount) }}</strong>
<span class="summary-card__unit">تومان</span>
<p v-if="isAmountComplete" class="summary-card__status">
<span class="summary-card__status-icon">✓</span>
مبلغ کامل است
</p>
</div>
</div>
<div class="checks-list">
<article v-for="(check, index) in checks" :key="check.id" class="check-card">
<div class="check-card__menu-wrap">
<button
v-if="isCheckout && orderStore.draftChecks.length > 0"
type="button"
class="check-card__menu-btn"
aria-label="گزینه‌ها"
@click="toggleMenu(check.id)"
>
⋮
</button>
<div v-if="openMenuId === check.id" class="check-card__menu">
<button type="button" @click="removeCheck(check.id)">حذف</button>
</div>
</div>
<div class="check-card__body">
<h3 class="check-card__title">چک {{ toPersianDigits(String(index + 1)) }}</h3>
<p class="check-card__amount">{{ formatCurrency(check.amount) }}</p>
<p class="check-card__date">تاریخ: {{ formatDate(check.dueDate) }}</p>
</div>
<div class="check-card__thumb-wrap">
<img
v-if="check.frontImage"
:src="check.frontImage"
class="check-card__thumb"
alt="تصویر چک"
/>
<div v-else class="check-card__thumb check-card__thumb--empty">چک</div>
</div>
</article>
</div>
<div class="rass-panel">
<h3 class="rass-panel__title">رأس چک‌ها</h3>
<div class="rass-panel__days">{{ toPersianDigits(String(rass.rassDays)) }} روز</div>
<p v-if="rass.rassDate" class="rass-panel__date">
تاریخ رأس تقریبی: {{ formatDate(rass.rassDate) }}
</p>
</div>
</div>
<div v-if="isCheckout && orderStore.draftChecks.length > 0" class="check-list-page__footer">
<BaseButton block :disabled="checks.length === 0" @click="submitOrder">
ارسال برای بررسی
</BaseButton>
</div>
</template>
</div>
<!-- Desktop -->
<div v-if="checks.length > 0" class="desktop-checkout-shell check-list-desktop desktop-only">
<header class="desktop-page__head">
<h1 class="desktop-page__title">چک‌های ثبت شده</h1>
<p class="desktop-page__subtitle">خلاصه و فهرست چک‌های ثبت‌شده</p>
</header>
<div class="desktop-two-col">
<div class="desktop-two-col__main">
<div class="desktop-table-wrap">
<table class="desktop-table">
<thead>
<tr>
<th scope="col">تصویر</th>
<th scope="col">مبلغ (تومان)</th>
<th scope="col">تاریخ سررسید</th>
<th scope="col">شناسه</th>
<th v-if="isCheckout" scope="col">عملیات</th>
</tr>
</thead>
<tbody>
<tr v-for="(check, index) in checks" :key="check.id">
<td>
<img
v-if="check.frontImage"
:src="check.frontImage"
class="check-list-desktop__thumb"
alt="تصویر چک"
/>
<span v-else>—</span>
</td>
<td>{{ formatNumber(check.amount) }}</td>
<td>{{ formatDate(check.dueDate) }}</td>
<td>چک {{ toPersianDigits(String(index + 1)) }}</td>
<td v-if="isCheckout && orderStore.draftChecks.length > 0">
<button type="button" class="orders-table__action" @click="removeCheck(check.id)">
حذف
</button>
</td>
</tr>
</tbody>
</table>
</div>
<BaseButton
v-if="isCheckout"
variant="outline"
class="desktop-btn-compact check-list-desktop__add"
@click="addAnother"
>
ثبت چک دیگر
</BaseButton>
</div>
<aside class="desktop-summary-panel">
<h2 class="desktop-summary-panel__title">خلاصه مالی</h2>
<div class="desktop-summary-panel__row">
<span>مجموع چک‌ها</span>
<strong>{{ formatNumber(rass.totalAmount) }} تومان</strong>
</div>
<div class="desktop-summary-panel__row">
<span>مبلغ فاکتور</span>
<strong>{{ formatNumber(invoiceAmount) }} تومان</strong>
</div>
<div class="desktop-summary-panel__row">
<span>رأس چک‌ها</span>
<strong>{{ toPersianDigits(String(rass.rassDays)) }} روز</strong>
</div>
<div v-if="isCheckout && orderStore.draftChecks.length > 0" class="desktop-summary-panel__action">
<BaseButton block :disabled="checks.length === 0" @click="submitOrder">
ارسال برای بررسی
</BaseButton>
</div>
</aside>
</div>
</div>
<div v-else class="desktop-checkout-shell desktop-only">
<EmptyState message="چکی ثبت نشده است">
<BaseButton v-if="isCheckout" @click="addAnother">ثبت چک جدید</BaseButton>
</EmptyState>
</div>
</template>
<style scoped>
.check-list-desktop__thumb {
width: 48px;
height: 32px;
object-fit: cover;
border-radius: 4px;
}
.check-list-desktop__add {
margin-top: 16px;
}
.check-list-page {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
padding: 12px var(--page-padding) 0;
background: #ffffff;
overflow: hidden;
}
.check-list-page__nav {
flex-shrink: 0;
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
margin-bottom: 16px;
}
.check-list-page__title {
margin: 0;
font-size: 1.0625rem;
font-weight: 700;
text-align: center;
}
.nav-btn {
width: 40px;
height: 40px;
border: none;
background: none;
color: var(--color-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.nav-btn--ghost {
visibility: hidden;
pointer-events: none;
}
.nav-btn__flip {
transform: scaleX(-1);
}
.check-list-page__scroll {
flex: 1;
min-height: 0;
padding-bottom: 12px;
}
.check-list-page__empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
color: var(--color-text-muted);
}
.summary-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 16px;
}
.summary-card {
padding: 14px 12px;
border-radius: 14px;
background: #f1f5f9;
text-align: center;
}
.summary-card__label {
display: block;
font-size: 0.75rem;
color: var(--color-text-muted);
margin-bottom: 8px;
}
.summary-card__value {
display: block;
font-size: 1rem;
font-weight: 700;
color: var(--color-text);
line-height: 1.2;
}
.summary-card__unit {
display: block;
margin-top: 2px;
font-size: 0.75rem;
color: var(--color-text-muted);
}
.summary-card__status {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
margin: 8px 0 0;
font-size: 0.6875rem;
font-weight: 600;
color: var(--color-success);
}
.summary-card__status-icon {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--color-success-light);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.625rem;
}
.checks-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 16px;
}
.check-card {
display: grid;
grid-template-columns: 28px 1fr 72px;
gap: 10px;
align-items: center;
padding: 12px;
border-radius: 14px;
background: #f8fafc;
border: 1px solid #eef2f7;
}
.check-card__menu-wrap {
position: relative;
}
.check-card__menu-btn {
width: 28px;
height: 28px;
border: none;
background: none;
color: var(--color-text-muted);
font-size: 1.125rem;
line-height: 1;
cursor: pointer;
padding: 0;
}
.check-card__menu {
position: absolute;
top: 100%;
right: 0;
z-index: 10;
min-width: 88px;
padding: 4px;
border-radius: var(--radius-sm);
background: #fff;
border: 1px solid var(--color-border);
box-shadow: var(--shadow-md);
}
.check-card__menu button {
width: 100%;
border: none;
background: none;
padding: 8px 10px;
text-align: right;
font-size: 0.8125rem;
color: var(--color-danger);
cursor: pointer;
border-radius: 6px;
}
.check-card__menu button:hover {
background: var(--color-danger-light);
}
.check-card__body {
min-width: 0;
}
.check-card__title {
margin: 0 0 4px;
font-size: 0.875rem;
font-weight: 700;
color: var(--color-text);
}
.check-card__amount {
margin: 0 0 4px;
font-size: 0.8125rem;
font-weight: 600;
color: var(--color-text);
}
.check-card__date {
margin: 0;
font-size: 0.75rem;
color: var(--color-text-muted);
}
.check-card__thumb-wrap {
flex-shrink: 0;
}
.check-card__thumb {
width: 72px;
height: 48px;
border-radius: 8px;
object-fit: cover;
border: 1px solid var(--color-border);
background: #fff;
}
.check-card__thumb--empty {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6875rem;
color: var(--color-text-muted);
}
.rass-panel {
padding: 16px;
border-radius: 14px;
background: #ecfdf3;
border: 1px solid #bbf7d0;
margin-bottom: 12px;
text-align: center;
}
.rass-panel__title {
margin: 0 0 6px;
font-size: 0.875rem;
font-weight: 600;
color: #166534;
}
.rass-panel__days {
font-size: 1.75rem;
font-weight: 700;
color: #15803d;
line-height: 1.2;
}
.rass-panel__date {
margin: 6px 0 0;
font-size: 0.8125rem;
color: #166534;
}
.check-list-page__footer {
flex-shrink: 0;
padding: 12px 0 calc(12px + env(safe-area-inset-bottom, 0));
background: #fff;
border-top: 1px solid var(--color-border);
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.06);
margin: 0 calc(-1 * var(--page-padding));
padding-left: var(--page-padding);
padding-right: var(--page-padding);
}
</style>