All files / src/views HomeView.vue

0% Statements 0/45
0% Branches 0/13
0% Functions 0/9
0% Lines 0/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109                                                                                                                                                                                                                         
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ApiLoader from '@/components/common/ApiLoader.vue'
import MetricCard from '@/components/feature/MetricCard.vue'
import TodoCard from '@/components/feature/TodoCard.vue'
import { fetchMetricSummary, fetchTodos, todoDataSource } from '@/api/modules/dashboard'
import type { MetricSummary, TodoItem, TodoType } from '@/api/types'
import { formatHkd } from '@/utils/money'
 
const { t } = useI18n()
const loading = ref(true)
const error = ref<string | null>(null)
const metrics = ref<MetricSummary | null>(null)
const todos = ref<TodoItem[]>([])
 
const heroCard = computed(() =>
  metrics.value
    ? { label: t('metric.thisMonthPaid'), value: formatHkd(metrics.value.thisMonthPaidHkd) }
    : null
)
 
const restCards = computed(() => {
  if (!metrics.value) return []
  return [
    { label: t('metric.taxYearPaid'), value: formatHkd(metrics.value.taxYearPaidHkd) },
    { label: t('metric.pipeline'), value: formatHkd(metrics.value.pipelineHkd) },
    { label: t('metric.inforceCount'), value: String(metrics.value.inforceCount) },
    { label: t('metric.firstYearFyp'), value: formatHkd(metrics.value.firstYearFypHkd) }
  ]
})
 
// 一次遍历统计,原实现为三次 filter(每类各扫一遍)
const countByType = computed(() =>
  todos.value.reduce(
    (acc, todo) => {
      acc[todo.type] += 1
      return acc
    },
    { renewal: 0, lapse: 0, birthday: 0 } as Record<TodoType, number>
  )
)
 
async function load() {
  loading.value = true
  error.value = null
  try {
    const [summary, todoList] = await Promise.all([fetchMetricSummary(), fetchTodos()])
    metrics.value = summary
    todos.value = todoList
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'error'
  } finally {
    loading.value = false
  }
}
 
onMounted(load)
</script>
 
<template>
  <div class="space-y-7">
    <ApiLoader :loading="loading" :error="error" @retry="load">
      <MetricCard v-if="heroCard" feature :label="heroCard.label" :value="heroCard.value" />
      <div class="mt-3 grid grid-cols-2 gap-3">
        <MetricCard v-for="card in restCards" :key="card.label" :label="card.label" :value="card.value" />
      </div>
 
      <router-link
        v-if="metrics"
        to="/customers"
        class="mt-3 flex items-center justify-between rounded-2xl border border-silver-200 bg-white p-4 shadow-card transition-shadow hover:shadow-soft"
      >
        <div>
          <div class="text-xs text-slate-500">{{ t('metric.myCustomers') }}</div>
          <div class="tnum mt-2 text-xl font-semibold tracking-tight text-slate-900">{{ metrics.caseCount }}</div>
        </div>
        <div class="flex items-center gap-1 text-xs text-slate-400">
          {{ t('customer.followingHint', { count: metrics.followingCount }) }}
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
            <polyline points="9 18 15 12 9 6" />
          </svg>
        </div>
      </router-link>
 
      <section class="mt-7">
        <div class="mb-3 flex items-center gap-2">
          <h2 class="text-[13px] font-semibold tracking-wide text-silver-600">{{ t('dashboard.actions') }}</h2>
          <span
            v-if="todoDataSource === 'mock'"
            class="rounded-full bg-amber-50 px-2 py-0.5 text-[11px] font-medium text-amber-700"
          >{{ t('todo.sample') }}</span>
        </div>
        <div class="mb-3 flex flex-wrap gap-2 text-xs text-slate-500">
          <span class="rounded-full bg-slate-100 px-2.5 py-1">{{ t('todo.renewal') }} {{ countByType.renewal }}</span>
          <span class="rounded-full bg-slate-100 px-2.5 py-1">{{ t('todo.lapse') }} {{ countByType.lapse }}</span>
          <span class="rounded-full bg-slate-100 px-2.5 py-1">{{ t('todo.birthday') }} {{ countByType.birthday }}</span>
        </div>
        <div class="space-y-3">
          <TodoCard v-for="todo in todos" :key="todo.id" :todo="todo" />
          <div class="rounded-2xl border border-dashed border-silver-200 bg-white/60 p-4">
            <div class="text-sm font-medium">{{ t('todo.claim') }}</div>
            <div class="mt-1 text-xs text-slate-400">{{ t('todo.claimPlaceholder') }}</div>
          </div>
        </div>
      </section>
    </ApiLoader>
  </div>
</template>