Files
nademitex_user/src/views/reminders/ReminderDetailView.vue
T
safavimohadeseh78 bbb3a15d1c Polish customer app UI and streamline CRM overlays.
Replace standalone customer create and meeting report pages with overlays, add TimeInput, remove opaque input backgrounds app-wide, improve desktop layouts/skeletons, and refresh shopping, customer, and reminder screens.
2026-09-15 21:41:48 +03:30

403 lines
9.8 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useCrmStore } from '@/stores/crm'
import { useAuthStore } from '@/stores/auth'
import type { CrmCustomer, CrmMeeting, CrmReminder, MeetingInteractionType } from '@/types/crm'
import PageHeader from '@/components/layout/PageHeader.vue'
import PageSkeleton from '@/components/ui/PageSkeleton.vue'
const route = useRoute()
const router = useRouter()
const crm = useCrmStore()
const auth = useAuthStore()
const id = computed(() => String(route.query.id ?? ''))
const reminder = ref<CrmReminder | null>(null)
const customer = ref<CrmCustomer | null>(null)
const history = ref<CrmMeeting[]>([])
const loading = ref(true)
const saving = ref(false)
const error = ref<string | null>(null)
async function load() {
if (!id.value) {
reminder.value = null
loading.value = false
return
}
loading.value = true
error.value = null
try {
const data = (await crm.loadReminderDetail(id.value)) as {
reminder: CrmReminder
customer: CrmCustomer
history: CrmMeeting[]
}
reminder.value = data.reminder
customer.value = data.customer
history.value = data.history
} catch (e) {
reminder.value = null
error.value = e instanceof Error ? e.message : 'یادآور یافت نشد.'
} finally {
loading.value = false
}
}
onMounted(load)
watch(id, load)
function typeLabel(type: MeetingInteractionType) {
if (type === 'in_person') return 'ملاقات حضوری'
if (type === 'call') return 'تماس تلفنی'
if (type === 'message') return 'پیام'
return 'یادداشت'
}
async function complete() {
if (!reminder.value || reminder.value.completed) return
saving.value = true
error.value = null
try {
await crm.markReminderComplete(reminder.value.id)
reminder.value = { ...reminder.value, completed: true }
router.push('/reminders?filter=today')
} catch (e) {
error.value = e instanceof Error ? e.message : 'تکمیل یادآور ناموفق بود.'
} finally {
saving.value = false
}
}
const agentName = computed(() => auth.fullName || 'واسطه')
const interactionCounts = computed(() => {
const list = history.value
return {
total: list.length,
inPerson: list.filter((m) => m.type === 'in_person').length,
call: list.filter((m) => m.type === 'call').length,
other: list.filter((m) => m.type === 'message' || m.type === 'note').length,
}
})
const chartBars = computed(() => {
const bars = Array.from({ length: 6 }, () => ({ a: 0, b: 0, c: 0 }))
history.value.forEach((meeting, index) => {
const bar = bars[index % 6]
if (!bar) return
if (meeting.type === 'in_person') bar.a += 1
else if (meeting.type === 'call') bar.b += 1
else bar.c += 1
})
const max = Math.max(1, ...bars.map((b) => b.a + b.b + b.c))
return bars.map((b) => ({
a: `${(b.a / max) * 100}%`,
b: `${(b.b / max) * 100}%`,
c: `${(b.c / max) * 100}%`,
}))
})
</script>
<template>
<div v-if="reminder && customer" class="page reminder-detail">
<PageHeader title="جزئیات یادآور" show-back centered @back="router.back()" />
<div class="page__scroll detail-scroll">
<p v-if="error" class="form-error">{{ error }}</p>
<div class="layout">
<section class="panel">
<h2>جزئیات یادآور</h2>
<div class="field">
<span>عنوان</span>
<strong>{{ reminder.title }} — {{ reminder.description }}</strong>
</div>
<div class="field">
<span>مشتری</span>
<div class="customer-line">
<div class="avatar" :style="{ background: customer.avatarColor }">
{{ customer.avatarLetter }}
</div>
<div>
<strong>{{ customer.name }}</strong>
<p>{{ customer.contactName }}</p>
</div>
</div>
</div>
<div class="field">
<span>فروشنده حامل حضوری</span>
<strong>{{ agentName }}</strong>
</div>
<div class="field">
<span>تاریخ و زمان</span>
<strong>{{ reminder.date }} — {{ reminder.time }}</strong>
</div>
<div class="field">
<span>یادداشت</span>
<strong>{{ reminder.note || '—' }}</strong>
</div>
<div class="actions">
<button type="button" class="btn-outline" @click="router.back()">ویرایش</button>
<button
type="button"
class="btn-primary"
:disabled="saving || reminder.completed"
@click="complete"
>
{{ reminder.completed ? 'تکمیل‌شده' : saving ? '...' : 'تکمیل شد' }}
</button>
</div>
</section>
<section class="panel">
<h2>تاریخچه تعاملات</h2>
<article v-for="item in history" :key="item.id" class="history-row">
<div>
<strong>{{ typeLabel(item.type) }}</strong>
<p>{{ item.description }}</p>
</div>
<span>{{ item.date }}</span>
</article>
<button
type="button"
class="see-all"
@click="router.push({ path: '/customer', query: { id: customer.id, tab: 'reports' } })"
>
مشاهده همه
</button>
</section>
<section class="panel chart-panel">
<div class="chart-panel__head">
<h2>نمودار فعالیت</h2>
<span>تعاملات ثبت‌شده</span>
</div>
<div class="bars">
<div v-for="(bar, index) in chartBars" :key="index" class="bar">
<span v-if="bar.c !== '0%'" class="seg seg--c" :style="{ height: bar.c }" />
<span v-if="bar.b !== '0%'" class="seg seg--b" :style="{ height: bar.b }" />
<span v-if="bar.a !== '0%'" class="seg seg--a" :style="{ height: bar.a }" />
</div>
</div>
<div class="stats-grid">
<div><span>کل تعاملات</span><strong>{{ interactionCounts.total.toLocaleString('fa-IR') }} مورد</strong></div>
<div><span>ملاقات حضوری</span><strong>{{ interactionCounts.inPerson.toLocaleString('fa-IR') }} مورد</strong></div>
<div><span>تماس تلفنی</span><strong>{{ interactionCounts.call.toLocaleString('fa-IR') }} مورد</strong></div>
<div><span>یادداشت و پیام</span><strong>{{ interactionCounts.other.toLocaleString('fa-IR') }} مورد</strong></div>
</div>
</section>
</div>
</div>
</div>
<div v-else-if="loading" class="page empty">
<PageSkeleton variant="detail" />
</div>
<div v-else class="page empty">
<p>{{ error || 'یادآور یافت نشد.' }}</p>
</div>
</template>
<style scoped>
.reminder-detail {
background: var(--color-bg);
}
.detail-scroll {
padding: 12px var(--page-padding) 24px;
}
.form-error {
margin: 0 0 12px;
color: var(--color-danger, #b91c1c);
font-size: 0.85rem;
}
.layout {
display: flex;
flex-direction: column;
gap: 12px;
}
@media (min-width: 1024px) {
.layout {
display: grid;
grid-template-columns: 1.1fr 1fr 1fr;
align-items: start;
}
}
.panel {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 16px;
}
.panel h2 {
margin: 0 0 14px;
font-size: 1rem;
}
.field {
margin-bottom: 14px;
}
.field > span {
display: block;
color: var(--color-text-muted);
font-size: 0.78rem;
margin-bottom: 4px;
}
.customer-line {
display: flex;
gap: 10px;
align-items: center;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 999px;
display: grid;
place-items: center;
font-weight: 700;
}
.customer-line p {
margin: 2px 0 0;
color: var(--color-text-muted);
font-size: 0.8rem;
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 8px;
}
.btn-outline,
.btn-primary {
min-height: 44px;
border-radius: 12px;
font-weight: 700;
cursor: pointer;
}
.btn-outline {
border: 1px solid var(--color-border);
background: var(--color-surface);
}
.btn-primary {
border: none;
background: var(--color-primary);
color: #fff;
}
.btn-primary:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.history-row {
display: flex;
justify-content: space-between;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid var(--color-border);
}
.history-row p {
margin: 2px 0 0;
color: var(--color-text-muted);
font-size: 0.78rem;
}
.see-all {
width: 100%;
margin-top: 12px;
border: none;
border-radius: 12px;
background: #eef1ef;
min-height: 44px;
cursor: pointer;
font-weight: 600;
}
.chart-panel__head {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-panel__head span {
color: var(--color-text-muted);
font-size: 0.8rem;
}
.bars {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
height: 140px;
align-items: end;
margin: 16px 0;
}
.bar {
display: flex;
flex-direction: column-reverse;
gap: 2px;
height: 100%;
}
.seg {
display: block;
width: 100%;
border-radius: 4px 4px 0 0;
}
.seg--a {
background: var(--color-primary);
}
.seg--b {
background: #3b82f6;
}
.seg--c {
background: #a78bfa;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.stats-grid div {
background: var(--color-bg);
border-radius: 12px;
padding: 10px;
}
.stats-grid span {
display: block;
color: var(--color-text-muted);
font-size: 0.72rem;
}
.stats-grid strong {
display: block;
margin-top: 4px;
}
.empty {
padding: 40px;
text-align: center;
}
</style>