559 lines
15 KiB
Vue
559 lines
15 KiB
Vue
<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>
|