|
|
@@ -1,4 +1,4 @@
|
|
|
-import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
|
+import { useCallback, useEffect, useState } from 'react'
|
|
|
import { Link } from 'react-router-dom'
|
|
|
import { Helmet } from 'react-helmet-async'
|
|
|
import {
|
|
|
@@ -18,41 +18,76 @@ const emptySummary: CustomerDashboardSummary = {
|
|
|
weeklyRecords: 0,
|
|
|
}
|
|
|
|
|
|
-const STYLE_API_MAP: Record<string, string> = {
|
|
|
- '短发': 'Short',
|
|
|
- '中长发': 'Medium',
|
|
|
- '长发': 'Long',
|
|
|
- 'ショート': 'Short',
|
|
|
- 'ミディアム': 'Medium',
|
|
|
- 'ロング': 'Long',
|
|
|
+const VISIT_STATUS_API_MAP: Record<string, string | undefined> = {
|
|
|
+ '全部': undefined,
|
|
|
+ '今天到店': 'TODAY',
|
|
|
+ '本周到店': 'THIS_WEEK',
|
|
|
+ '30日以上未到店': 'OVER_30_DAYS_NO_VISIT',
|
|
|
+ '全て': undefined,
|
|
|
+ '本日来店': 'TODAY',
|
|
|
+ '今週来店': 'THIS_WEEK',
|
|
|
+ '30日以上未来店': 'OVER_30_DAYS_NO_VISIT',
|
|
|
}
|
|
|
|
|
|
-function parseDate(value?: string): Date | null {
|
|
|
- if (!value) return null
|
|
|
- const normalized = value.replace(/\//g, '-')
|
|
|
- const date = new Date(normalized)
|
|
|
- return Number.isNaN(date.getTime()) ? null : date
|
|
|
+const FOLLOW_STATUS_API_MAP: Record<string, string | undefined> = {
|
|
|
+ '全部': undefined,
|
|
|
+ '需跟进': 'NEED_FOLLOW_UP',
|
|
|
+ '已发送': 'SENT',
|
|
|
+ '全て': undefined,
|
|
|
+ '要フォロー': 'NEED_FOLLOW_UP',
|
|
|
+ '送信済み': 'SENT',
|
|
|
}
|
|
|
|
|
|
-function daysBetween(a: Date, b: Date): number {
|
|
|
- const ms = Math.abs(a.getTime() - b.getTime())
|
|
|
- return Math.floor(ms / (1000 * 60 * 60 * 24))
|
|
|
+const HAIR_LENGTH_API_MAP: Record<string, string | undefined> = {
|
|
|
+ '全部': undefined,
|
|
|
+ '短发': 'SHORT',
|
|
|
+ '中长发': 'MEDIUM',
|
|
|
+ '长发': 'LONG',
|
|
|
+ '全て': undefined,
|
|
|
+ 'ショート': 'SHORT',
|
|
|
+ 'ミディアム': 'MEDIUM',
|
|
|
+ 'ロング': 'LONG',
|
|
|
}
|
|
|
|
|
|
+function getFollowStatusLabel(status: string | undefined, pageCopy: CustomerListPageCopy) {
|
|
|
+ if (status === 'NEED_FOLLOW_UP') return pageCopy.filters.followOptions[1]
|
|
|
+ if (status === 'SENT') return pageCopy.filters.followOptions[2]
|
|
|
+ return '—'
|
|
|
+}
|
|
|
+
|
|
|
+type CustomerListPageCopy = ReturnType<typeof usePageContext>['copy']['customerListPage']
|
|
|
+
|
|
|
export function CustomerListPage() {
|
|
|
const { locale, path, copy } = usePageContext()
|
|
|
const [items, setItems] = useState<CustomerEntity[]>([])
|
|
|
const [summary, setSummary] = useState<CustomerDashboardSummary>(emptySummary)
|
|
|
const [query, setQuery] = useState('')
|
|
|
+ const [page] = useState(1)
|
|
|
+ const [pageSize] = useState(20)
|
|
|
+ const [total, setTotal] = useState(0)
|
|
|
const [visitFilter, setVisitFilter] = useState(copy.customerListPage.filters.visitOptions[0])
|
|
|
const [followFilter, setFollowFilter] = useState(copy.customerListPage.filters.followOptions[0])
|
|
|
const [styleFilter, setStyleFilter] = useState(copy.customerListPage.filters.styleOptions[0])
|
|
|
const pageCopy = copy.customerListPage
|
|
|
|
|
|
const loadCustomers = useCallback(() => {
|
|
|
- const style = STYLE_API_MAP[styleFilter] || undefined
|
|
|
- listCustomers({ q: query.trim() || undefined, style }).then(setItems).catch(() => setItems([]))
|
|
|
- }, [query, styleFilter])
|
|
|
+ listCustomers({
|
|
|
+ page,
|
|
|
+ pageSize,
|
|
|
+ q: query.trim() || undefined,
|
|
|
+ visitStatus: VISIT_STATUS_API_MAP[visitFilter],
|
|
|
+ followStatus: FOLLOW_STATUS_API_MAP[followFilter],
|
|
|
+ hairLength: HAIR_LENGTH_API_MAP[styleFilter],
|
|
|
+ })
|
|
|
+ .then((result) => {
|
|
|
+ setItems(result.items)
|
|
|
+ setTotal(result.total)
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ setItems([])
|
|
|
+ setTotal(0)
|
|
|
+ })
|
|
|
+ }, [followFilter, page, pageSize, query, styleFilter, visitFilter])
|
|
|
|
|
|
useEffect(() => {
|
|
|
const timer = setTimeout(loadCustomers, 300)
|
|
|
@@ -63,35 +98,6 @@ export function CustomerListPage() {
|
|
|
getCustomerDashboardSummary().then(setSummary).catch(() => setSummary(emptySummary))
|
|
|
}, [])
|
|
|
|
|
|
- const filteredItems = useMemo(() => {
|
|
|
- const today = new Date()
|
|
|
- return items.filter((customer) => {
|
|
|
- if (visitFilter !== pageCopy.filters.visitOptions[0]) {
|
|
|
- const lastVisit = parseDate(customer.lastVisit)
|
|
|
- if (!lastVisit) return false
|
|
|
- const days = daysBetween(today, lastVisit)
|
|
|
- if (visitFilter === pageCopy.filters.visitOptions[1]) {
|
|
|
- if (days !== 0) return false
|
|
|
- } else if (visitFilter === pageCopy.filters.visitOptions[2]) {
|
|
|
- if (days > 7) return false
|
|
|
- } else if (visitFilter === pageCopy.filters.visitOptions[3]) {
|
|
|
- if (days < 30) return false
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (followFilter !== pageCopy.filters.followOptions[0]) {
|
|
|
- const tags = customer.tags || []
|
|
|
- const needsFollow = tags.some((tag) => tag.includes('跟进') || tag.includes('フォロー'))
|
|
|
- || (customer.followStatus && customer.followStatus.includes('计划'))
|
|
|
- const sent = customer.followStatus?.includes('已发送') || customer.followStatus?.includes('送信')
|
|
|
- if (followFilter === pageCopy.filters.followOptions[1] && !needsFollow) return false
|
|
|
- if (followFilter === pageCopy.filters.followOptions[2] && !sent) return false
|
|
|
- }
|
|
|
-
|
|
|
- return true
|
|
|
- })
|
|
|
- }, [items, visitFilter, followFilter, pageCopy.filters])
|
|
|
-
|
|
|
return (
|
|
|
<DashboardLayout>
|
|
|
<Helmet>
|
|
|
@@ -159,7 +165,7 @@ export function CustomerListPage() {
|
|
|
<div className="d-flex align-items-center justify-content-between mb-25px">
|
|
|
<div>
|
|
|
<span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.resultBadge}</span>
|
|
|
- <h2 className="fw-700 text-medium-gray fs-26 mb-0">{pageCopy.resultTitle.replace('{{count}}', String(filteredItems.length))}</h2>
|
|
|
+ <h2 className="fw-700 text-medium-gray fs-26 mb-0">{pageCopy.resultTitle.replace('{{count}}', String(total))}</h2>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -167,20 +173,30 @@ export function CustomerListPage() {
|
|
|
<div className="customer-row customer-head">
|
|
|
{pageCopy.tableHeaders.map((label: string) => <span key={label}>{label}</span>)}
|
|
|
</div>
|
|
|
- {filteredItems.length === 0 ? (
|
|
|
+ {items.length === 0 ? (
|
|
|
<p className="fs-15 text-medium-gray mb-0 mt-3">{copy.common.empty}</p>
|
|
|
- ) : filteredItems.map((customer, index) => (
|
|
|
+ ) : items.map((customer, index) => (
|
|
|
<div key={customer.id} className="customer-row">
|
|
|
<div className="customer-profile">
|
|
|
<img src={customer.image || '/images/aitori-before-photo.png'} alt={customer.name} className="customer-avatar" />
|
|
|
<div>
|
|
|
<span className="fw-700 text-dark-gray d-block">{customer.name}</span>
|
|
|
- <span className="fs-14 text-medium-gray">{customer.phone} / {customer.hairLength === 'Medium' ? pageCopy.hairLengthMap.medium : customer.hairLength === 'Long' ? pageCopy.hairLengthMap.long : pageCopy.hairLengthMap.short}</span>
|
|
|
+ <span className="fs-14 text-medium-gray">
|
|
|
+ {customer.phone}
|
|
|
+ {' / '}
|
|
|
+ {customer.hairLength === 'Medium'
|
|
|
+ ? pageCopy.hairLengthMap.medium
|
|
|
+ : customer.hairLength === 'Long'
|
|
|
+ ? pageCopy.hairLengthMap.long
|
|
|
+ : customer.hairLength === 'Short'
|
|
|
+ ? pageCopy.hairLengthMap.short
|
|
|
+ : '—'}
|
|
|
+ </span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<span>{customer.lastVisit || '—'}</span>
|
|
|
- <span>{customer.tags?.[0] || customer.concern || '—'}</span>
|
|
|
- <span><span className="workbench-badge">{customer.followStatus || pageCopy.rowStatuses[index] || pageCopy.rowStatuses[5]}</span></span>
|
|
|
+ <span>{customer.concern || '—'}</span>
|
|
|
+ <span><span className="workbench-badge">{getFollowStatusLabel(customer.followStatus, pageCopy)}</span></span>
|
|
|
<Link to={path(`/customers/${customer.id}`)} className={`btn btn-very-small btn-rounded ${index === 0 ? 'btn-base-color' : 'btn-transparent-light-gray border border-1 border-color-dark-gray'}`}>
|
|
|
{pageCopy.detail}
|
|
|
</Link>
|