Bỏ qua

Hồ Sơ Khách Hàng (Customer Profile)

Trang Hồ Sơ Khách Hàng (Customer Profile) cung cấp thông tin chi tiết toàn diện về từng khách hàng, bao gồm thông tin cá nhân, lịch sử đặt phòng, tương tác, loyalty membership, preferences và khả năng quản lý thống kê khách hàng dành cho Admin.


Tổng Quan

Customer Profile Overview - Tổng quan hồ sơ khách hàng 📸 Chụp từ: http://localhost:8082/crm/customer-profiles.html

Trang tổng quan hiển thị:

  • Customer Header: Thông tin tổng quan với avatar, tên, email, số điện thoại
  • Customer Type Tags: Phân loại khách hàng (Booking User, Guest User, Corporate, Group)
  • Loyalty Tier Badge: Cấp độ thành viên loyalty (nếu có)
  • Quick Stats Cards: 4 KPI cards chính (Total Bookings, Total Spent, Avg Stay, Satisfaction Score)
  • Tabbed Interface: 5 tabs chi tiết (Customer Details, Booking History, Interaction Timeline, Loyalty Information, Admin Settings)
  • Edit Profile Button: Chỉnh sửa thông tin khách hàng

Key Components

graph TD
    A[Customer Profile Page] --> B[Header Section]
    A --> C[Quick Stats]
    A --> D[Tabbed Content]

    B --> B1[Avatar & Name]
    B --> B2[Customer Type]
    B --> B3[Loyalty Tier]
    B --> B4[Contact Info]

    C --> C1[Total Bookings]
    C --> C2[Total Spent]
    C --> C3[Avg Stay Duration]
    C --> C4[Satisfaction Score]

    D --> D1[Customer Details Tab]
    D --> D2[Booking History Tab]
    D --> D3[Interaction Timeline Tab]
    D --> D4[Loyalty Information Tab]
    D --> D5[Admin Settings Tab]

Customer Header Section

Customer Header - Thông tin header khách hàng 📸 Chụp từ: http://localhost:8082/crm/customer-profiles.html

Header Information Display

Left Section: - Avatar: Icon 80x80px (desktop) hoặc 60px (mobile) - Customer Name: Họ tên đầy đủ (firstName + lastName) - Customer Type Tag: - BOOKING_USER (Blue) - GUEST_USER (Green) - CORPORATE (Purple) - GROUP (Orange) - Loyalty Tier Badge: Crown icon với tier name và color - Contact Details: - Email với MailOutlined icon - Phone với PhoneOutlined icon

Right Section: - Edit Profile Button: Primary button với EditOutlined icon - Desktop: "Edit Profile" - Mobile: "Edit" (rút gọn)

Mobile Optimization: - Avatar size giảm xuống 60px - Text size nhỏ hơn (16px title) - Email truncate nếu > 20 ký tự - Layout stack vertical trên màn hình nhỏ


Quick Stats Cards

Quick Stats - Thống kê nhanh 📸 Chụp từ: http://localhost:8082/crm/customer-profiles.html

4 KPI Cards

Card 1: Total Bookings

{
  title: "Total Bookings" | "Bookings" (mobile)
  value: customer.totalBookings
  prefix: CalendarOutlined icon | 📅 (mobile)
  description: "Tổng số lần đặt phòng"
}

Card 2: Total Spent

{
  title: "Total Spent" | "Spent" (mobile)
  value: parseCurrencyFromAPI(customer.totalSpent).toVND()
  prefix: DollarOutlined icon | 💰 (mobile)
  description: "Tổng chi tiêu"
  mobileFormat: "XXX K" (rút gọn nghìn)
}

Card 3: Average Stay Duration

{
  title: "Avg Stay Duration" | "Avg Stay" (mobile)
  value: customer.averageStayDuration
  suffix: "nights" | "n" (mobile)
  prefix: HomeOutlined icon | 🏠 (mobile)
  description: "Thời gian lưu trú trung bình"
}

Card 4: Satisfaction Score

{
  title: "Satisfaction Score" | "Rating" (mobile)
  value: customer.satisfactionScore || 0
  suffix: "/5"
  prefix: StarOutlined icon |  (mobile)
  valueColor:
    - >= 4: Green (#3f8600)
    - >= 3: Yellow (#faad14)
    - < 3: Red (#ff4d4f)
  description: "Điểm hài lòng"
}

Layout: - Desktop: Row với 4 columns (Col span={6}) - Mobile: 2x2 grid (Col xs={12}) - Gutter: 16px (desktop), 8px (mobile)


Tab 1: Customer Details

Customer Details - Thông tin chi tiết 📸 Chụp từ: Click tab "Customer Details" hoặc "👤 Details" (mobile)

Personal Information Card

Personal Info - Thông tin cá nhân 📸 Chụp từ: Tab "Customer Details" → Section "Personal Information"

Fields Displayed:

Field Type Format Example
Full Name Text firstName + lastName Nguyễn Văn A
Email Email customer.email nguyenvana@example.com
Phone Phone customer.phone +84 123 456 789
Date of Birth Date MMM DD, YYYY Jan 15, 1990
Gender Select MALE / FEMALE / OTHER Male
Nationality Text customer.nationality Vietnamese
Language Text customer.language Vietnamese

Component:

<Card title="Personal Information">
  <Descriptions column={1} size="small">
    <Descriptions.Item label="Full Name">
      {customer.firstName} {customer.lastName}
    </Descriptions.Item>
    {/* ... other fields */}
  </Descriptions>
</Card>

Contact Information Card

Contact Info - Thông tin liên lạc 📸 Chụp từ: Tab "Customer Details" → Section "Contact Information"

Address Information:

address: {
  street: string
  city: string
  state: string
  country: string
  postalCode: string
}

Display Format:

[Street Address]
[City], [State]
[Country] [PostalCode]

Emergency Contact:

After address, có Divider và section "Emergency Contact":

Field Example
Name Nguyễn Thị B
Phone +84 987 654 321
Relationship Wife

Identification Card

Identification - Giấy tờ tùy thân 📸 Chụp từ: Tab "Customer Details" → Section "Identification"

ID Fields:

Field Type Options
Document Type Select PASSPORT, ID_CARD, DRIVING_LICENSE
Document Number Text VN123456789
Issue Date Date MMM DD, YYYY
Expiry Date Date MMM DD, YYYY
Issue Place Text Hanoi, Vietnam

Mobile Format: - Column: 1 (stack vertical) - Date format: MM/DD/YY (rút gọn) - "Issue Place" ẩn trên mobile

Preferences Card

Preferences - Sở thích 📸 Chụp từ: Tab "Customer Details" → Section "Preferences & Special Requirements"

Preference Fields:

Field Type Options
Preferred Room Type Select STANDARD, DELUXE, SUITE, PRESIDENTIAL
Preferred Floor Text High floor, Low floor
Bed Type Select SINGLE, DOUBLE, TWIN, KING
Smoking Boolean Yes / No
Special Requests Tags Array Multiple tags

Special Requests Display:

{customer.preferences.specialRequests.map((req, index) => (
  <Tag key={index} size={isMobile ? 'small' : 'default'}>
    {isMobile && req.length > 12 ? req.substring(0, 12) + '...' : req}
  </Tag>
))}


Tab 2: Booking History

Booking History - Lịch sử đặt phòng 📸 Chụp từ: Click tab "Booking History" hoặc "📅 Bookings" (mobile)

Booking Table Columns

Column 1: Booking

render: (record: Booking) => (
  <div>
    <Text strong>{record.bookingNumber}</Text>
    <br />
    <Text type="secondary">{record.roomType}</Text>
  </div>
)

Column 2: Dates

render: (record: Booking) => (
  <div>
    <div>{dayjs(record.checkInDate).format('MMM DD, YYYY')}</div>
    <div>{dayjs(record.checkOutDate).format('MMM DD, YYYY')}</div>
    <Text type="secondary">
      {dayjs(record.checkOutDate).diff(dayjs(record.checkInDate), 'days')} nights
    </Text>
  </div>
)

Column 3: Amount

render: (record: Booking) => (
  <Text strong>{parseCurrencyFromAPI(record.totalAmount).toVND()}</Text>
)

Column 4: Status

render: (record: Booking) => (
  <Space direction="vertical" size="small">
    <Tag color={getBookingStatusColor(record.status)}>
      {record.status?.replace('_', ' ')}
    </Tag>
    <Tag>{record.channel}</Tag>
  </Space>
)

Status Colors

Status Color Meaning
CONFIRMED Blue Đặt phòng đã xác nhận
COMPLETED Green Hoàn thành
CANCELLED Red Đã hủy
NO_SHOW Volcano Khách không đến

Channel Types

  • DIRECT: Đặt trực tiếp
  • OTA: Online Travel Agency
  • PHONE: Đặt qua điện thoại
  • WALK_IN: Walk-in booking

Table Pagination: - Desktop: 10 items per page, show size changer, quick jumper - Mobile: 5 items per page, simplified pagination - Show total: "X-Y of Z bookings"


Tab 3: Interaction Timeline

Interaction Timeline - Lịch sử tương tác 📸 Chụp từ: Click tab "Interaction Timeline" hoặc "⏰ Timeline" (mobile)

Timeline Component

Timeline Mode: - Desktop: mode="left" (timeline bên trái) - Mobile: Default mode (vertical center)

Timeline Item Structure:

<Timeline.Item
  key={interaction.id}
  dot={getInteractionIcon(interaction.type)}
  color={interaction.type === 'COMPLAINT' ? 'red' : 'blue'}
>
  {/* Content */}
</Timeline.Item>

Interaction Types & Icons

Type Icon Color Meaning
EMAIL MailOutlined Blue Email communication
PHONE PhoneOutlined Blue Phone call
SMS MailOutlined Blue SMS message
COMPLAINT ExclamationCircleOutlined Red Khiếu nại
FEEDBACK StarOutlined Blue Phản hồi
BOOKING CalendarOutlined Blue Đặt phòng

Interaction Display

Interaction Item - Chi tiết tương tác 📸 Chụp từ: Tab "Interaction Timeline" → Xem từng interaction

Top Section:

<Space>
  <Text strong>{interaction.subject}</Text>
  <Tag color={direction === 'INBOUND' ? 'green' : 'blue'}>
    {interaction.direction}
  </Tag>
  <Tag>{interaction.type}</Tag>
</Space>

Description:

<Paragraph type="secondary">
  {interaction.description}
</Paragraph>

Bottom Metadata:

<Text type="secondary" style={{ fontSize: '12px' }}>
  {dayjs(createdAt).format('MMM DD, YYYY HH:mm')}
  {staffName && ` • by ${staffName}`}
  {rating && (
    <>
      {' • '}
      {Array.from({ length: rating }, (_, i) => (
        <StarOutlined key={i} style={{ color: '#faad14' }} />
      ))}
    </>
  )}
</Text>

Direction Types: - INBOUND (Green): Khách hàng liên hệ vào - OUTBOUND (Blue): Khách sạn liên hệ ra


Tab 4: Loyalty Information

Loyalty Info - Thông tin thành viên 📸 Chụp từ: Click tab "Loyalty Information" hoặc "👑 Loyalty" (mobile)

Conditional Display:

{customer.loyaltyMember && (
  <TabPane tab="Loyalty Information" key="loyalty">
    {/* Loyalty content */}
  </TabPane>
)}

Tab này chỉ hiện khi customer.loyaltyMember tồn tại.

Loyalty Status Card

Loyalty Status - Trạng thái thành viên 📸 Chụp từ: Tab "Loyalty Information" → "Loyalty Status" card

Center Display:

<div style={{ textAlign: 'center' }}>
  <Avatar
    size={isMobile ? 60 : 80}
    icon={<TrophyOutlined />}
    style={{ backgroundColor: tier.color }}
  />
  <Title level={4}>{tier.name}</Title>
  <Text type="secondary">Level {tier.level}</Text>
</div>

Tier Information:

Field Display Example
Tier Name Title Gold Member
Tier Level Text Level 3
Tier Color Avatar bg #FFD700
Membership ID Description LM-2024-001234
Current Points Description (bold) 25,000 points
Lifetime Points Description 150,000 points
Status Tag ACTIVE (Green) / INACTIVE (Red)

Mobile Format: - Membership ID truncate: "LM-2024-0012..." (12 chars) - Points format: "25K" instead of "25,000"

Points Progress Card

Points Progress - Tiến trình điểm 📸 Chụp từ: Tab "Loyalty Information" → "Points Progress" card

Progress Bar:

<Progress
  percent={75}
  strokeColor={tier.color}
  showInfo={!isMobile}
  size={isMobile ? 'small' : 'default'}
/>

Current vs Next Tier:

<div style={{ display: 'flex', justifyContent: 'space-between' }}>
  <Text type="secondary">Current: {currentPoints.toLocaleString()}</Text>
  <Text type="secondary">Next: {nextTierPoints.toLocaleString()}</Text>
</div>

View Point History Button:

<Button
  block
  onClick={() => navigate(`/dashboard/loyalty/members/${loyaltyMemberId}/transactions`)}
  size={isMobile ? 'small' : 'middle'}
>
  {isMobile ? 'Point History' : 'View Point History'}
</Button>


Tab 5: Admin Settings

Admin Settings - Cài đặt Admin 📸 Chụp từ: Click tab "Admin Settings" hoặc "⚙️ Admin" (mobile)

Permission Check:

const isAdmin = user?.role === 'ADMIN' ||
                user?.role === 'SUPER_ADMIN' ||
                user?.permissions?.includes('crm.customer.update_stats');

{isAdmin && (
  <TabPane tab="Admin Settings" key="admin">
    {/* Admin content */}
  </TabPane>
)}

Tab này chỉ hiện với Admin users.

Customer Statistics Management

Stats Management - Quản lý thống kê 📸 Chụp từ: Tab "Admin Settings" → "Customer Statistics Management" card

Warning Banner:

<Text type="secondary">
  This admin interface allows you to manually adjust customer statistics
  and booking totals. All changes are logged for audit purposes.
</Text>

Current Stats Display:

Row 1: 3 Statistic Cards

<Row gutter={16}>
  <Col xs={24} sm={8}>
    <Statistic
      title="Current Total Spent"
      value={parseCurrencyFromAPI(customer.totalSpent).toVND()}
      prefix={<DollarOutlined />}
    />
  </Col>
  <Col xs={24} sm={8}>
    <Statistic
      title="Current Total Bookings"
      value={customer.totalBookings}
      prefix={<CalendarOutlined />}
    />
  </Col>
  <Col xs={24} sm={8}>
    <Statistic
      title="Last Booking"
      value={dayjs(customer.lastBookingDate).format('MMM DD, YYYY')}
      prefix={<ClockCircleOutlined />}
    />
  </Col>
</Row>

Update Stats Button:

<Button
  type="primary"
  icon={<EditOutlined />}
  onClick={() => setStatsModalVisible(true)}
  block={isMobile}
  danger
>
  {isMobile ? 'Update Stats' : 'Update Customer Statistics'}
</Button>

Update Stats Modal

Update Stats Modal - Modal cập nhật thống kê 📸 Chụp từ: Click "Update Customer Statistics" button

Warning Alert:

<div style={{ backgroundColor: '#fff7e6', border: '1px solid #ffd666' }}>
  <Space>
    <ExclamationCircleOutlined style={{ color: '#fa8c16' }} />
    <Text>
      <strong>Admin Only:</strong> This action will permanently modify
      customer statistics and is logged for audit.
    </Text>
  </Space>
</div>

Form Fields:

Field 1: Booking Amount

<Form.Item
  name="bookingAmount"
  label="Booking Amount to Add (VND)"
  rules={[
    { required: true },
    { pattern: /^\d+(\.\d{1,2})?$/ }
  ]}
  extra="Enter the amount to add to customer's total spent (e.g., 2500000.50)"
>
  <Input
    placeholder="2500000.50"
    prefix="₫"
  />
</Form.Item>

Field 2: Reason

<Form.Item
  name="reason"
  label="Reason for Adjustment"
  rules={[{ required: true }]}
>
  <Select>
    <Option value="booking_completion">Booking Completion</Option>
    <Option value="manual_adjustment">Manual Adjustment</Option>
    <Option value="system_correction">System Correction</Option>
    <Option value="refund_reversal">Refund Reversal</Option>
    <Option value="data_migration">Data Migration</Option>
    <Option value="other">Other</Option>
  </Select>
</Form.Item>

Field 3: Additional Notes

<Form.Item
  name="notes"
  label="Additional Notes"
  rules={[{ max: 500 }]}
>
  <Input.TextArea
    rows={isMobile ? 2 : 3}
    placeholder="Optional: Add any additional notes..."
    showCount
    maxLength={500}
  />
</Form.Item>

Success Info:

<div style={{ backgroundColor: '#f6ffed', border: '1px solid #b7eb8f' }}>
  <Text style={{ color: '#52c41a' }}>
     This action will increase total bookings by 1 and update
      the last booking date to current time.
     All changes are recorded in audit logs with your user ID and timestamp.
  </Text>
</div>

Submit Action:

updateStatsMutation.mutate({
  bookingAmount: values.bookingAmount,
  reason: values.reason || 'Manual stats adjustment via admin interface',
  notes: values.notes,
});

Success Response:

onSuccess: (data) => {
  message.success(`Stats updated successfully! Audit ID: ${data.auditId}`);
  setStatsModalVisible(false);
  queryClient.invalidateQueries({ queryKey: ['customer'] });
}


Edit Profile Modal

Edit Profile Modal - Modal chỉnh sửa hồ sơ 📸 Chụp từ: Click "Edit Profile" button

Modal Configuration:

<Modal
  title={isMobile ? 'Edit Profile' : 'Edit Customer Profile'}
  open={editModalVisible}
  onOk={handleUpdateCustomer}
  onCancel={() => setEditModalVisible(false)}
  confirmLoading={updateCustomerMutation.isPending}
  width={isMobile ? '95vw' : 800}
/>

Personal Information Section

Edit Personal Info - Chỉnh sửa thông tin cá nhân 📸 Chụp từ: Edit Profile Modal → Personal section

Row 1: Name Fields

<Row gutter={16}>
  <Col xs={24} sm={12}>
    <Form.Item
      name="firstName"
      label="First Name"
      rules={[{ required: true, message: 'Please enter first name' }]}
    >
      <Input />
    </Form.Item>
  </Col>
  <Col xs={24} sm={12}>
    <Form.Item
      name="lastName"
      label="Last Name"
      rules={[{ required: true }]}
    >
      <Input />
    </Form.Item>
  </Col>
</Row>

Row 2: Contact Fields

<Row gutter={16}>
  <Col xs={24} sm={12}>
    <Form.Item
      name="email"
      label="Email"
      rules={[
        { required: true },
        { type: 'email', message: 'Please enter valid email' }
      ]}
    >
      <Input />
    </Form.Item>
  </Col>
  <Col xs={24} sm={12}>
    <Form.Item name="phone" label="Phone" rules={[{ required: true }]}>
      <Input />
    </Form.Item>
  </Col>
</Row>

Row 3: Additional Info

<Row gutter={16}>
  <Col xs={24} sm={8}>
    <Form.Item name="dateOfBirth" label="Date of Birth">
      <DatePicker style={{ width: '100%' }} />
    </Form.Item>
  </Col>
  <Col xs={12} sm={8}>
    <Form.Item name="gender" label="Gender">
      <Select allowClear>
        <Option value="MALE">Male</Option>
        <Option value="FEMALE">Female</Option>
        <Option value="OTHER">Other</Option>
      </Select>
    </Form.Item>
  </Col>
  <Col xs={12} sm={8}>
    <Form.Item name="nationality" label="Nationality">
      <Input />
    </Form.Item>
  </Col>
</Row>

Address Information Section

Edit Address - Chỉnh sửa địa chỉ 📸 Chụp từ: Edit Profile Modal → Address section (after divider)

<Divider>{isMobile ? 'Address' : 'Address Information'}</Divider>

Street Address:

<Row gutter={16}>
  <Col span={24}>
    <Form.Item name="street" label="Street Address">
      <Input />
    </Form.Item>
  </Col>
</Row>

City, State, Country:

<Row gutter={16}>
  <Col xs={24} sm={8}>
    <Form.Item name="city" label="City">
      <Input />
    </Form.Item>
  </Col>
  <Col xs={12} sm={8}>
    <Form.Item name="state" label={isMobile ? 'State' : 'State/Province'}>
      <Input />
    </Form.Item>
  </Col>
  <Col xs={12} sm={8}>
    <Form.Item name="country" label="Country">
      <Input />
    </Form.Item>
  </Col>
</Row>

Emergency Contact Section

Edit Emergency Contact - Liên hệ khẩn cấp 📸 Chụp từ: Edit Profile Modal → Emergency Contact section

<Divider>{isMobile ? 'Emergency' : 'Emergency Contact'}</Divider>

<Row gutter={16}>
  <Col xs={24} sm={8}>
    <Form.Item name="emergencyContactName" label="Contact Name">
      <Input />
    </Form.Item>
  </Col>
  <Col xs={12} sm={8}>
    <Form.Item name="emergencyContactPhone" label="Contact Phone">
      <Input />
    </Form.Item>
  </Col>
  <Col xs={12} sm={8}>
    <Form.Item name="emergencyContactRelationship" label="Relationship">
      <Input />
    </Form.Item>
  </Col>
</Row>

Preferences Section

Edit Preferences - Chỉnh sửa sở thích 📸 Chụp từ: Edit Profile Modal → Preferences section

<Divider>Preferences</Divider>

<Row gutter={16}>
  <Col span={8}>
    <Form.Item name="roomType" label="Preferred Room Type">
      <Select allowClear>
        <Option value="STANDARD">Standard</Option>
        <Option value="DELUXE">Deluxe</Option>
        <Option value="SUITE">Suite</Option>
        <Option value="PRESIDENTIAL">Presidential</Option>
      </Select>
    </Form.Item>
  </Col>
  <Col span={8}>
    <Form.Item name="bedType" label="Bed Type">
      <Select allowClear>
        <Option value="SINGLE">Single Bed</Option>
        <Option value="DOUBLE">Double Bed</Option>
        <Option value="TWIN">Twin Beds</Option>
        <Option value="KING">King Bed</Option>
      </Select>
    </Form.Item>
  </Col>
  <Col span={8}>
    <Form.Item name="smoking" label="Smoking" valuePropName="checked">
      <Switch checkedChildren="Smoking" unCheckedChildren="Non-smoking" />
    </Form.Item>
  </Col>
</Row>

Submit Handler:

const handleUpdateCustomer = async () => {
  const values = await form.validateFields();
  updateCustomerMutation.mutate({
    firstName: values.firstName,
    lastName: values.lastName,
    email: values.email,
    phone: values.phone,
    dateOfBirth: values.dateOfBirth?.format('YYYY-MM-DD'),
    gender: values.gender,
    nationality: values.nationality,
    language: values.language,
    address: {
      street: values.street,
      city: values.city,
      state: values.state,
      country: values.country,
      postalCode: values.postalCode,
    },
    identification: {
      type: values.identificationType,
      number: values.identificationNumber,
    },
    emergencyContact: {
      name: values.emergencyContactName,
      phone: values.emergencyContactPhone,
      relationship: values.emergencyContactRelationship,
    },
    preferences: {
      roomType: values.roomType,
      floor: values.floor,
      bedType: values.bedType,
      smoking: values.smoking,
    },
  });
};


Mobile Optimization

Mobile View - Giao diện mobile 📸 Chụp từ: http://localhost:8082/crm/customer-profiles.html (trên mobile device)

Mobile-Specific Features

Responsive Breakpoints:

const isMobile = useIsMobile(); // Hook to detect mobile

// Used throughout component:
{isMobile ? 'Short Text' : 'Long Descriptive Text'}

Size Adjustments:

Element Desktop Mobile
Avatar 80px 60px
Title Level 2 4
Title Font Default 16px
Card Size default small
Button Size middle small
Tag Size default small
Form Size middle small
Table Page Size 10 5

Layout Changes:

  1. Quick Stats Cards
  2. Desktop: 4 columns (span={6})
  3. Mobile: 2x2 grid (xs={12})

  4. Contact Info

  5. Desktop: Horizontal space
  6. Mobile: Vertical stack

  7. Tab Labels

  8. Desktop: Full text + icon
  9. Mobile: Emoji + short text

    • "Customer Details" → "👤 Details"
    • "Booking History" → "📅 Bookings"
    • "Interaction Timeline" → "⏰ Timeline"
    • "Loyalty Information" → "👑 Loyalty"
    • "Admin Settings" → "⚙️ Admin"
  10. Text Truncation

  11. Email > 20 chars: Truncate with "..."
  12. Membership ID: Show first 12 chars
  13. Tags: Truncate > 12 chars

  14. Icon Replacement

  15. Desktop: Ant Design icons
  16. Mobile: Emoji icons (📧, 📞, 🏠, ⭐, 💰, etc.)

  17. Number Formatting

  18. Desktop: Full number with locale (25,000)
  19. Mobile: Abbreviated (25K)

  20. Modal Adjustments

  21. Desktop: 800px width
  22. Mobile: 95vw width, max height 70vh, overflow scroll

API Endpoints

Fetch Customer Details

Endpoint:

GET /api/crm/tenants/:tenantId/customers/:customerId

Response:

interface Customer {
  id: string
  type: 'BOOKING_USER' | 'GUEST_USER' | 'CORPORATE' | 'GROUP'
  firstName: string
  lastName: string
  email: string
  phone: string
  dateOfBirth?: string
  gender?: 'MALE' | 'FEMALE' | 'OTHER'
  nationality?: string
  language?: string
  address?: {
    street: string
    city: string
    state: string
    country: string
    postalCode: string
  }
  identification?: {
    type: 'PASSPORT' | 'ID_CARD' | 'DRIVING_LICENSE'
    number: string
    issueDate?: string
    expiryDate?: string
    issuePlace?: string
  }
  preferences?: {
    roomType?: string
    floor?: string
    bedType?: string
    smoking?: boolean
    specialRequests?: string[]
  }
  emergencyContact?: {
    name: string
    phone: string
    relationship: string
  }
  loyaltyMember?: {
    id: string
    membershipId: string
    currentPoints: number
    lifetimePoints: number
    tier: {
      name: string
      level: number
      color: string
    }
    status: string
  }
  totalBookings: number
  totalSpent: string
  lastBookingDate?: string
  averageStayDuration: number
  satisfactionScore?: number
  createdAt: string
  updatedAt: string
}

Fetch Customer Bookings

Endpoint:

GET /api/crm/tenants/:tenantId/customers/:customerId/bookings

Response:

interface Booking {
  id: string
  bookingNumber: string
  checkInDate: string
  checkOutDate: string
  roomType: string
  totalAmount: string
  status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NO_SHOW'
  channel: 'DIRECT' | 'OTA' | 'PHONE' | 'WALK_IN'
  guestCount: number
  specialRequests?: string
}[]

Fetch Customer Interactions

Endpoint:

GET /api/crm/tenants/:tenantId/customers/:customerId/interactions

Response:

interface Interaction {
  id: string
  type: 'EMAIL' | 'PHONE' | 'SMS' | 'COMPLAINT' | 'FEEDBACK' | 'BOOKING'
  direction: 'INBOUND' | 'OUTBOUND'
  subject: string
  description: string
  createdAt: string
  staffName?: string
  rating?: number
}[]

Update Customer

Endpoint:

PATCH /api/crm/tenants/:tenantId/customers/:customerId

Request Body:

{
  firstName: string
  lastName: string
  email: string
  phone: string
  dateOfBirth?: string
  gender?: 'MALE' | 'FEMALE' | 'OTHER'
  nationality?: string
  language?: string
  address?: {
    street: string
    city: string
    state: string
    country: string
    postalCode: string
  }
  identification?: {
    type: 'PASSPORT' | 'ID_CARD' | 'DRIVING_LICENSE'
    number: string
  }
  emergencyContact?: {
    name: string
    phone: string
    relationship: string
  }
  preferences?: {
    roomType?: string
    floor?: string
    bedType?: string
    smoking?: boolean
  }
}

Success Response:

{
  ...updatedCustomer
}

Update Customer Stats (Admin Only)

Endpoint:

PATCH /api/crm/tenants/:tenantId/customers/:customerId/stats

Request Body:

{
  bookingAmount: string  // Decimal format: "2500000.50"
  reason: string         // Reason for adjustment
  notes?: string         // Optional notes (max 500 chars)
}

Success Response:

{
  ...updatedCustomer,
  auditId: string  // Audit log ID for tracking
}

Error Response:

{
  statusCode: 400,
  message: "Error message",
  error: "Bad Request"
}


Data Flow

sequenceDiagram
    participant U as User
    participant C as Component
    participant API as API Gateway
    participant CRM as CRM Service
    participant DB as Database

    U->>C: Open customer profile page
    C->>API: GET /api/crm/tenants/{tenantId}/customers/{customerId}
    API->>CRM: Forward request
    CRM->>DB: Query customer data
    DB-->>CRM: Return customer
    CRM-->>API: Return customer
    API-->>C: Return customer
    C->>API: GET /api/crm/tenants/{tenantId}/customers/{customerId}/bookings
    API->>CRM: Forward request
    CRM->>DB: Query bookings
    DB-->>CRM: Return bookings
    CRM-->>API: Return bookings
    API-->>C: Return bookings
    C->>API: GET /api/crm/tenants/{tenantId}/customers/{customerId}/interactions
    API->>CRM: Forward request
    CRM->>DB: Query interactions
    DB-->>CRM: Return interactions
    CRM-->>API: Return interactions
    API-->>C: Return interactions
    C-->>U: Display complete profile

    U->>C: Click "Edit Profile"
    C-->>U: Show edit modal with current data
    U->>C: Submit changes
    C->>API: PATCH /api/crm/tenants/{tenantId}/customers/{customerId}
    API->>CRM: Forward update
    CRM->>DB: Update customer
    DB-->>CRM: Confirm update
    CRM-->>API: Return updated customer
    API-->>C: Return updated customer
    C->>C: Invalidate query cache
    C->>API: Refetch customer data
    API-->>C: Return fresh data
    C-->>U: Show success message & updated profile

Query & State Management

React Query Hooks

Customer Details Query:

const { data: customer, isLoading } = useQuery({
  queryKey: ['customer', user?.tenantId, customerId],
  queryFn: async () => {
    const response = await apiClient.instance.get(
      `/api/crm/tenants/${user?.tenantId}/customers/${customerId}`
    );
    return response.data as Customer;
  },
  enabled: !!user?.tenantId && !!customerId,
});

Bookings Query:

const { data: bookings } = useQuery({
  queryKey: ['customer-bookings', user?.tenantId, customerId],
  queryFn: async () => {
    const response = await apiClient.instance.get(
      `/api/crm/tenants/${user?.tenantId}/customers/${customerId}/bookings`
    );
    return response.data as Booking[];
  },
  enabled: !!user?.tenantId && !!customerId,
});

Interactions Query:

const { data: interactions } = useQuery({
  queryKey: ['customer-interactions', user?.tenantId, customerId],
  queryFn: async () => {
    const response = await apiClient.instance.get(
      `/api/crm/tenants/${user?.tenantId}/customers/${customerId}/interactions`
    );
    return response.data as Interaction[];
  },
  enabled: !!user?.tenantId && !!customerId,
});

Mutations

Update Customer Mutation:

const updateCustomerMutation = useMutation({
  mutationFn: async (customerData: any) => {
    const response = await apiClient.instance.patch(
      `/api/crm/tenants/${user?.tenantId}/customers/${customerId}`,
      customerData
    );
    return response.data;
  },
  onSuccess: () => {
    message.success('Customer updated successfully!');
    setEditModalVisible(false);
    queryClient.invalidateQueries({ queryKey: ['customer'] });
  },
  onError: (error: any) => {
    message.error(error.response?.data?.message || 'Failed to update customer');
  },
});

Update Stats Mutation (Admin):

const updateStatsMutation = useMutation({
  mutationFn: async (statsData: any) => {
    const response = await apiClient.instance.patch(
      `/api/crm/tenants/${user?.tenantId}/customers/${customerId}/stats`,
      {
        bookingAmount: statsData.bookingAmount,
        reason: statsData.reason,
        notes: statsData.notes,
      }
    );
    return response.data;
  },
  onSuccess: (data) => {
    message.success(`Stats updated successfully! Audit ID: ${data.auditId}`);
    setStatsModalVisible(false);
    statsForm.resetFields();
    queryClient.invalidateQueries({ queryKey: ['customer'] });
  },
  onError: (error: any) => {
    message.error(error.response?.data?.message || 'Failed to update stats');
  },
});


Helper Functions

Status Color Mapping

const getCustomerTypeColor = (type: string) => {
  switch (type) {
    case 'BOOKING_USER': return 'blue';
    case 'GUEST_USER': return 'green';
    case 'CORPORATE': return 'purple';
    case 'GROUP': return 'orange';
    default: return 'default';
  }
};

const getBookingStatusColor = (status: string) => {
  switch (status) {
    case 'CONFIRMED': return 'blue';
    case 'COMPLETED': return 'green';
    case 'CANCELLED': return 'red';
    case 'NO_SHOW': return 'volcano';
    default: return 'default';
  }
};

Icon Mapping

const getInteractionIcon = (type: string) => {
  switch (type) {
    case 'EMAIL': return <MailOutlined />;
    case 'PHONE': return <PhoneOutlined />;
    case 'SMS': return <MailOutlined />;
    case 'COMPLAINT': return <ExclamationCircleOutlined />;
    case 'FEEDBACK': return <StarOutlined />;
    case 'BOOKING': return <CalendarOutlined />;
    default: return <ClockCircleOutlined />;
  }
};

Currency Formatting

// Using parseCurrencyFromAPI utility
parseCurrencyFromAPI(customer.totalSpent).toVND()
// Output: "2,500,000 ₫"

// Mobile shortened format
{isMobile ?
  `${Math.round(parseCurrencyFromAPI(customer.totalSpent).toNumber() / 1000)}K` :
  parseCurrencyFromAPI(customer.totalSpent).toVND()
}
// Mobile output: "2500K"

Permissions & Access Control

Admin-Only Features

Stats Management Tab:

const isAdmin = user?.role === 'ADMIN' ||
                user?.role === 'SUPER_ADMIN' ||
                user?.permissions?.includes('crm.customer.update_stats');

Visible to: - Role: ADMIN - Role: SUPER_ADMIN - Permission: crm.customer.update_stats

Edit Permission

Edit Profile Button:

<RequirePermission resource="customer" action="update" fallback={null}>
  <Button type="primary" icon={<EditOutlined />} onClick={handleEditCustomer}>
    Edit Profile
  </Button>
</RequirePermission>

Required Permission: - Resource: customer - Action: update


Tips & Best Practices

💡 Tip 1: Comprehensive Customer View - Xem toàn bộ customer journey từ lần đặt đầu tiên đến hiện tại - Phân tích booking patterns và preferences để personalize service - Sử dụng satisfaction score để identify at-risk customers

💡 Tip 2: Leverage Loyalty Data - Check loyalty tier để apply appropriate benefits - View current points để suggest redemption opportunities - Track lifetime points để recognize top-spending customers

💡 Tip 3: Interaction Timeline - Review interaction history trước khi contact customer - Identify complaints và unresolved issues - Track staff interactions để ensure consistency

💡 Tip 4: Admin Stats Management - Only use khi có lý do chính đáng (system correction, data migration) - Always document trong "Reason" và "Notes" fields - Review audit logs định kỳ để detect unauthorized changes

💡 Tip 5: Mobile Accessibility - Profile page fully responsive cho mobile staff access - Emoji icons giúp nhận diện nhanh trên màn hình nhỏ - Simplified pagination và truncated text optimize mobile UX

💡 Tip 6: Data Quality - Keep emergency contact updated cho safety compliance - Maintain accurate preferences để improve guest satisfaction - Update identification documents trước expiry date


FAQs

1. Làm thế nào để import customer từ external source?

Câu trả lời:

Hiện tại chưa có bulk import UI, nhưng có thể sử dụng API:

POST /api/crm/tenants/{tenantId}/customers/bulk-import

Body:
{
  "customers": [
    {
      "type": "BOOKING_USER",
      "firstName": "Nguyen",
      "lastName": "Van A",
      "email": "nguyenvana@example.com",
      "phone": "+84123456789",
      ...
    },
    ...
  ]
}

Best Practice: - Validate data trước khi import (email format, phone format) - Check duplicates bằng email hoặc phone - Import theo batches (500-1000 records/batch) - Log errors và retry failed imports

2. Satisfaction score được tính như thế nào?

Câu trả lời:

Satisfaction score được aggregate từ: - Guest feedback ratings (after checkout) - Review scores từ các platforms (Booking.com, Google, TripAdvisor) - Complaint resolution quality - Service interaction ratings

Calculation Formula:

satisfactionScore = (
  sum(feedbackRatings) +
  sum(reviewScores) +
  complaintResolutionScore
) / totalInteractions

// Normalized to 0-5 scale

Improvement Actions: - Score < 3: Immediate manager review required - Score 3-4: Monitor closely, identify issues - Score > 4: Maintain quality, recognize in loyalty program

3. Làm thế nào để merge duplicate customers?

Câu trả lời:

Identify Duplicates: - Same email with different names - Same phone with different emails - Similar name với typos

Merge Process: 1. Admin identifies duplicates manually 2. Review both profiles carefully 3. Use Admin Stats Management để consolidate: - Add booking amounts from duplicate to primary - Reason: "Data consolidation - merge duplicate" - Notes: "Merged from customer ID: XXX" 4. Manually transfer loyalty points (if different programs) 5. Update booking history references 6. Deactivate duplicate profile

Future Enhancement: - Automatic duplicate detection algorithm - Merge wizard với conflict resolution UI - Audit trail của merged customers

4. Customer preferences có ảnh hưởng đến booking process không?

Câu trả lời:

Yes! Preferences được sử dụng trong:

1. Booking Creation: - Auto-suggest preferred room type - Pre-fill floor preference - Default bed type selection - Highlight smoking/non-smoking preference

2. Room Assignment: - Priority matching cho repeat guests - Alert staff nếu không match preferences - Upgrade suggestions based on preferences

3. Marketing: - Targeted promotions cho preferred room types - Personalized email campaigns - Special offers aligned with preferences

Example Workflow:

// During booking creation
if (customer.preferences?.roomType === 'SUITE') {
  suggestedRoom = availableSuites[0];
  showUpgradeOffer = false; // Already preferred type
} else {
  suggestedRoom = customer.preferences?.roomType || 'STANDARD';
  showUpgradeOffer = true; // Upsell opportunity
}

5. Admin có thể revert stats adjustment không?

Câu trả lời:

Không thể auto-revert, nhưng có thể adjust ngược lại:

Revert Process: 1. Find audit log entry (auditId from success message) 2. Review original adjustment:

Audit ID: AUD-2024-001234
Date: 2024-01-22 10:30:00
User: admin@hotel.com
Action: Added 2,500,000 VND
Reason: Booking completion

  1. Create reverse adjustment:
  2. Booking Amount: -2500000.00 (negative value)
  3. Reason: "Revert adjustment AUD-2024-001234"
  4. Notes: "Original adjustment was incorrect - reason: [explain]"

  5. Submit adjustment

Audit Trail:

AUD-2024-001234: +2,500,000 VND (Booking completion)
AUD-2024-001235: -2,500,000 VND (Revert AUD-2024-001234)
Net Impact: 0 VND

Prevention: - Double-check amount before submitting - Require manager approval for adjustments > 10M VND - Regular audit log reviews - Access control (only ADMIN/SUPER_ADMIN)

6. Loyalty member data có sync với loyalty system không?

Câu trả lời:

Yes, real-time sync!

Data Flow:

Loyalty Service <--NATS--> CRM Service <--API--> Customer Profile UI

Synced Fields: - loyaltyMember.currentPoints - Real-time balance - loyaltyMember.lifetimePoints - Cumulative total - loyaltyMember.tier - Current tier (auto-upgrade) - loyaltyMember.status - ACTIVE/INACTIVE/SUSPENDED

Update Triggers: - Point earn/redeem: Instant update - Tier upgrade: Instant update - Status change: Instant update

Refresh Mechanism:

// React Query auto-refetch on window focus
refetchOnWindowFocus: true

// Manual refresh button (if needed)
<Button onClick={() => queryClient.invalidateQueries(['customer'])}>
  Refresh
</Button>

Click "View Point History":

navigate(`/dashboard/loyalty/members/${customer.loyaltyMember.id}/transactions`)
// Opens detailed point transaction history


Liên Kết Liên Quan


Cập nhật lần cuối: 2025-01-29 Phiên bản: 1.0 Tác giả: Claude Code Documentation Team