| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- import { useEffect, useState } from 'react'
- import { Link } from 'react-router-dom'
- import { Helmet } from 'react-helmet-async'
- import {
- getCustomerDashboardSummary,
- listCustomers,
- type CustomerDashboardSummary,
- } from '../../../services/api/customers'
- import type { CustomerEntity } from '../../../services/api/customers'
- import { usePageContext } from '../../../shared/pageContext'
- import { DashboardLayout } from '../../../layouts/DashboardLayout'
- const emptySummary: CustomerDashboardSummary = {
- total: 0,
- today: 0,
- followUp: 0,
- simulated: 0,
- weeklyRecords: 0,
- }
- const HAIR_LENGTH_API_MAP: Record<string, string | undefined> = {
- '全部': undefined,
- '短发': 'SHORT',
- '中长发': 'MEDIUM',
- '长发': 'LONG',
- '未确认': 'UNCONFIRM',
- '全て': undefined,
- 'ショート': 'SHORT',
- 'ミディアム': 'MEDIUM',
- 'ロング': 'LONG',
- '未確認': 'UNCONFIRM',
- }
- function getHairLengthLabel(length: string | undefined, pageCopy: CustomerListPageCopy) {
- if (length === 'SHORT') return pageCopy.hairLengthMap.short
- if (length === 'MEDIUM') return pageCopy.hairLengthMap.medium
- if (length === 'LONG') return pageCopy.hairLengthMap.long
- if (length === 'UNCONFIRM') return pageCopy.statusMap?.none || '—'
- return '—'
- }
- function getVisiblePages(currentPage: number, totalPages: number) {
- if (totalPages <= 5) {
- return Array.from({ length: totalPages }, (_, index) => index + 1)
- }
- const start = Math.max(1, Math.min(currentPage - 2, totalPages - 4))
- return Array.from({ length: 5 }, (_, index) => start + index)
- }
- 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, setPage] = useState(1)
- const [pageSize] = useState(20)
- const [total, setTotal] = useState(0)
- const [styleFilter, setStyleFilter] = useState(copy.customerListPage.filters.styleOptions[0])
- const pageCopy = copy.customerListPage
- useEffect(() => {
- const timer = setTimeout(() => {
- listCustomers({
- page,
- pageSize,
- q: query.trim() || undefined,
- hairLength: HAIR_LENGTH_API_MAP[styleFilter],
- })
- .then((result) => {
- setItems(result.items)
- setTotal(result.total)
- })
- .catch(() => {
- setItems([])
- setTotal(0)
- })
- }, 300)
- return () => clearTimeout(timer)
- }, [page, pageSize, query, styleFilter])
- useEffect(() => {
- setPage(1)
- }, [query, styleFilter])
- useEffect(() => {
- getCustomerDashboardSummary().then(setSummary).catch(() => setSummary(emptySummary))
- }, [])
- const totalPages = Math.max(1, Math.ceil(total / pageSize))
- const visiblePages = getVisiblePages(page, totalPages)
- useEffect(() => {
- if (page > totalPages) {
- setPage(totalPages)
- }
- }, [page, totalPages])
- return (
- <DashboardLayout>
- <Helmet>
- <html lang={locale === 'ja' ? 'ja' : 'zh-CN'} />
- <title>{`Aitori Stylist | ${pageCopy.title}`}</title>
- <meta name="description" content={pageCopy.description} />
- </Helmet>
- <section className="stylist-workbench-top ipad-top-space-margin">
- <div className="container">
- <div className="row align-items-end justify-content-between mb-4">
- <div className="col-lg-7">
- <span className="bg-base-color d-inline-block mb-15px fw-600 text-white text-uppercase border-radius-30px ps-20px pe-20px fs-12">{pageCopy.badge}</span>
- <h1 className="fw-700 text-dark-gray ls-minus-1px mb-10px">{pageCopy.title}</h1>
- <p className="fs-18 lh-30 mb-0">{pageCopy.lead}</p>
- </div>
- <div className="col-lg-4 text-lg-end mt-3 mt-lg-0">
- <Link to={path('/customers/new')} className="btn btn-extra-large btn-rounded btn-base-color btn-box-shadow fw-700">
- <i className="feather icon-feather-user-plus me-8px"></i>{pageCopy.newCustomer}
- </Link>
- </div>
- </div>
- <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-25px mb-4 customer-list-filter-panel">
- <div className="row g-3 align-items-end customer-list-filter-grid">
- <div className="col-lg-8">
- <label htmlFor="customer-search" className="fw-700 text-dark-gray mb-10px">{pageCopy.filters.searchLabel}</label>
- <div className="customer-search-field">
- <i className="feather icon-feather-search"></i>
- <input id="customer-search" type="search" className="form-control" value={query} onChange={(event) => setQuery(event.target.value)} placeholder={pageCopy.filters.searchPlaceholder} />
- </div>
- </div>
- <div className="col-lg-4 col-md-4">
- <label htmlFor="style-filter" className="fw-700 text-dark-gray mb-10px">{pageCopy.filters.styleLabel}</label>
- <select id="style-filter" className="form-select customer-filter-control" value={styleFilter} onChange={(event) => setStyleFilter(event.target.value)}>
- {pageCopy.filters.styleOptions.map((option: string) => <option key={option} value={option}>{option}</option>)}
- </select>
- </div>
- </div>
- </div>
- <div className="row row-cols-2 row-cols-lg-2 row-cols-md-2 g-3 mb-4">
- <div className="col"><div className="workbench-stat bg-white border-radius-8px p-20px"><span className="d-block fs-26 fw-800 text-dark-gray">{summary.total}</span><span className="fs-14">{pageCopy.stats.total}</span></div></div>
- <div className="col"><div className="workbench-stat bg-white border-radius-8px p-20px"><span className="d-block fs-26 fw-800 text-dark-gray">{summary.simulated}</span><span className="fs-14">{pageCopy.stats.simulated}</span></div></div>
- </div>
- </div>
- </section>
- <section className="pt-0">
- <div className="container">
- <div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
- <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(total))}</h2>
- </div>
- </div>
- <div className="customer-table">
- <div className="customer-row customer-head">
- {pageCopy.tableHeaders.map((label: string, index: number) => (
- <span key={label} className={index === pageCopy.tableHeaders.length - 1 ? 'customer-row-action' : undefined}>{label}</span>
- ))}
- </div>
- {items.length === 0 ? (
- <p className="fs-15 text-medium-gray mb-0 mt-3">{copy.common.empty}</p>
- ) : items.map((customer) => (
- <Link key={customer.id} to={path(`/customers/${customer.id}`)} className="customer-row customer-row-link" aria-label={pageCopy.detail}>
- <div className="customer-profile">
- <img src={customer.avatar || '/images/default-user-avatar.svg'} 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}
- {' / '}
- {getHairLengthLabel(customer.hairLength, pageCopy)}
- </span>
- </div>
- </div>
- <span>{customer.trouble || '—'}</span>
- <span className="customer-row-action btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">{pageCopy.detail}</span>
- </Link>
- ))}
- </div>
- <div className="customer-pager">
- <button
- type="button"
- className={`customer-pager-link${page <= 1 ? ' is-disabled' : ''}`}
- aria-label={pageCopy.pagination?.prev || (locale === 'ja' ? '前へ' : '上一页')}
- disabled={page <= 1}
- onClick={() => setPage((current) => Math.max(1, current - 1))}
- >
- {pageCopy.pagination?.prev || (locale === 'ja' ? '前へ' : '上一页')}
- </button>
- {visiblePages.map((pageNumber) => (
- <button
- key={pageNumber}
- type="button"
- className={`customer-pager-link${pageNumber === page ? ' is-active' : ''}`}
- aria-current={pageNumber === page ? 'page' : undefined}
- onClick={() => setPage(pageNumber)}
- >
- {pageNumber}
- </button>
- ))}
- <button
- type="button"
- className={`customer-pager-link${page >= totalPages ? ' is-disabled' : ''}`}
- aria-label={pageCopy.pagination?.next || (locale === 'ja' ? '次へ' : '下一页')}
- disabled={page >= totalPages}
- onClick={() => setPage((current) => Math.min(totalPages, current + 1))}
- >
- {pageCopy.pagination?.next || (locale === 'ja' ? '次へ' : '下一页')}
- </button>
- </div>
- </div>
- </div>
- </section>
- </DashboardLayout>
- )
- }
|