-
+
{t('common.appointments.title')}
@@ -412,7 +413,12 @@ export default function Appointments() {
diff --git a/frontend/src/pages/users/dashboard/components/main-menu.tsx b/frontend/src/pages/users/dashboard/components/main-menu.tsx
index 5fbf2c7..9c53d47 100644
--- a/frontend/src/pages/users/dashboard/components/main-menu.tsx
+++ b/frontend/src/pages/users/dashboard/components/main-menu.tsx
@@ -1,5 +1,5 @@
import { Lightbulb, TrendingUp } from 'lucide-react'
-import { useNavigate } from 'react-router-dom'
+import { Link, useNavigate } from 'react-router-dom'
import { DASHBOARD_ACTIVITY, DASHBOARD_DATA } from '@/_mocks/data-dashboard'
import { RECENT_DOCUMENTS, FEATURED_UPDATE } from '@/_mocks/recent.document.mock'
@@ -73,7 +73,7 @@ export default function MainMenu() {
{/* Main Content Grid */}
-
+
{/* Left Column: Recent Updates */}
@@ -130,7 +130,9 @@ export default function MainMenu() {
variant='link'
className='text-primary font-bold p-0 flex items-center gap-1 hover:gap-2 transition-all h-auto text-btn-small'
>
- {FEATURED_UPDATE.actionText} →
+
+ {FEATURED_UPDATE.actionText} →
+
diff --git a/frontend/src/pages/users/dashboard/dashboard-ui-refactor.md b/frontend/src/pages/users/dashboard/dashboard-ui-refactor.md
deleted file mode 100644
index e69de29..0000000
diff --git a/frontend/src/pages/users/lawyer/LawyerList.tsx b/frontend/src/pages/users/lawyer/LawyerList.tsx
index fc01d38..ca562e5 100644
--- a/frontend/src/pages/users/lawyer/LawyerList.tsx
+++ b/frontend/src/pages/users/lawyer/LawyerList.tsx
@@ -85,7 +85,7 @@ export default function LawyerList() {
{/* Header Section */}
-
Danh bạ Luật sư
+
Danh bạ Luật sư
Tìm kiếm chuyên gia pháp lý phù hợp với nhu cầu của bạn
@@ -176,63 +176,60 @@ export default function LawyerList() {
Vui lòng thay đổi từ khóa tìm kiếm hoặc bộ lọc.
) : (
-
-
- {filteredLawyers.map((lawyer) => {
- return (
-
- )
- })}
-
+
+ {filteredLawyers.map((lawyer) => {
+ return (
+
+ )
+ })}
+
+ )}
- {/* Pagination component */}
- {pagination.totalPages && pagination.totalPages > 1 && (
-
-
-
- {Array.from({ length: pagination.totalPages }, (_, index) => {
- const pageNum = index + 1
- const isActive = pageNum === currentPage
- return (
-
- )
- })}
-
+ {/* Pagination component */}
+ {pagination.totalPages && pagination.totalPages > 1 && (
+
+
+
+ {Array.from({ length: pagination.totalPages }, (_, index) => {
+ const pageNum = index + 1
+ const isActive = pageNum === currentPage
+ return (
-
- )}
+ )
+ })}
+
+
)}
diff --git a/frontend/src/pages/users/lawyer/LawyerProfile.tsx b/frontend/src/pages/users/lawyer/LawyerProfile.tsx
index 3fcdb58..39e9457 100644
--- a/frontend/src/pages/users/lawyer/LawyerProfile.tsx
+++ b/frontend/src/pages/users/lawyer/LawyerProfile.tsx
@@ -1,39 +1,23 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useState, useMemo } from 'react'
-import { ArrowLeft, Star, ShieldCheck, DollarSign, Briefcase, Award, MapPin, MessageSquare } from 'lucide-react'
+import { ArrowLeft, Star, ShieldCheck, DollarSign, MapPin, Mail, Phone, CheckCircle2 } from 'lucide-react'
import { useParams, useNavigate } from 'react-router-dom'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { getOptimizedImageUrl } from '@/core/helpers/image'
-import toastifyCommon from '@/core/lib/toastify-common'
-import { chatRequestApi } from '@/core/services/chat-request.service'
+import { cn } from '@/core/lib/utils'
import { useLawyerDetail } from '@/hooks/lawyers/use-lawyer'
+import { type Lawyer } from '@/models/lawyer/list-lawyer.type'
+
+import { LawyerContactDialog } from './components/LawyerContactDialog'
export default function LawyerProfile() {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
- const [isSending, setIsSending] = useState(false)
- const [isSent, setIsSent] = useState(false)
-
- const handleRequestConsultation = async () => {
- if (!id) return
- setIsSending(true)
- try {
- await chatRequestApi.createChatRequest({
- lawyerId: id,
- analysisId: ''
- })
- setIsSent(true)
- toastifyCommon.success('Gửi yêu cầu tư vấn thành công! Luật sư sẽ liên hệ với bạn sớm nhất.')
- } catch (err) {
- console.error(err)
- toastifyCommon.error('Gửi yêu cầu thất bại. Vui lòng thử lại sau.')
- } finally {
- setIsSending(false)
- }
- }
+ const [activeTab, setActiveTab] = useState<'bio' | 'specializations' | 'history' | 'reviews'>('bio')
+ const [isContactModalOpen, setIsContactModalOpen] = useState(false)
useEffect(() => {
window.scrollTo({ top: 0, behavior: 'auto' })
@@ -42,6 +26,52 @@ export default function LawyerProfile() {
// Call API with React Query hook
const { data: detailResponse, isLoading, error } = useLawyerDetail(id)
+ const detailSummary = detailResponse?.data?.summary
+
+ const contactLawyer = useMemo
(() => {
+ if (!detailSummary) return null
+ const rawUrl = detailSummary.avatarUrl || detailSummary.avatar || 'https://api.dicebear.com/7.x/avataaars/svg?seed=lawyer'
+ return {
+ id: id || '',
+ fullName: detailSummary.name || '',
+ avatar: rawUrl,
+ avatarUrl: getOptimizedImageUrl(rawUrl, 200),
+ careerHistory: detailSummary.careerHistory || '',
+ bio: detailSummary.bio || 'Chuyên gia tư vấn pháp lý chuyên nghiệp.',
+ averageRating: detailSummary.averageRating || 0,
+ successfulCases: detailSummary.successfulCases || 0,
+ specializations: detailSummary.specializations || [],
+ location: detailSummary.location || 'Việt Nam'
+ }
+ }, [id, detailSummary])
+
+ const mockReviews = useMemo(() => {
+ if (!detailSummary) return []
+ return [
+ {
+ id: 'r1',
+ userName: 'Nguyễn Văn Nam',
+ rating: 5,
+ date: '2026-06-15',
+ comment: `Luật sư ${detailSummary.name} tư vấn rất tận tâm, giải thích chi tiết các quy định pháp luật và đưa ra phương án giải quyết vụ việc rất rõ ràng. Cảm ơn luật sư rất nhiều!`
+ },
+ {
+ id: 'r2',
+ userName: 'Trần Thị Mai',
+ rating: Math.floor(detailSummary.averageRating),
+ date: '2026-05-20',
+ comment: 'Tôi rất hài lòng về phong cách làm việc chuyên nghiệp và nhanh chóng của luật sư. Sẽ tiếp tục nhờ luật sư hỗ trợ nếu có vấn đề phát sinh.'
+ },
+ {
+ id: 'r3',
+ userName: 'Phạm Minh Đức',
+ rating: 5,
+ date: '2026-04-12',
+ comment: 'Tư vấn nhiệt tình, có chuyên môn cao và am hiểu sâu sắc về lĩnh vực doanh nghiệp. Rất đáng tin cậy!'
+ }
+ ]
+ }, [detailSummary])
+
if (isLoading) {
return (
@@ -63,25 +93,84 @@ export default function LawyerProfile() {
const detail = detailResponse.data.summary
+ // Extract avatar and bio from API summary
const rawAvatarUrl = detail.avatarUrl || detail.avatar || 'https://api.dicebear.com/7.x/avataaars/svg?seed=lawyer'
const avatarUrl = getOptimizedImageUrl(rawAvatarUrl, 200)
const bioText = detail.bio || 'Chuyên gia tư vấn pháp lý chuyên nghiệp.'
const cityText = detail.location || 'Việt Nam'
+ const statusMap = {
+ AVAILABLE: { label: 'Sẵn sàng tư vấn', className: 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' },
+ BUSY: { label: 'Đang bận', className: 'bg-amber-500/10 text-amber-500 border-amber-500/20' },
+ OFFLINE: { label: 'Ngoại tuyến', className: 'bg-slate-500/10 text-slate-500 border-slate-500/20' }
+ }
+ const statusKey = detail.lawyerStatus || detail.status
+ const currentStatus = statusMap[statusKey as keyof typeof statusMap] || {
+ label: statusKey || 'Ngoại tuyến',
+ className: 'bg-slate-500/10 text-slate-500 border-slate-500/20'
+ }
+
+ const stats = [
+ {
+ label: 'Đánh giá',
+ labelClass: 'text-sm',
+ value: (
+
+
+ {detail.averageRating.toFixed(1)}
+
+ )
+ },
+ {
+ label: 'Kinh nghiệm',
+ labelClass: 'text-sm',
+ value: (
+
+ {detail.experienceYears} năm
+
+ )
+ },
+ {
+ label: 'Vụ thành công',
+ labelClass: 'text-sm',
+ value: (
+
+ {detail.successfulCases} vụ
+
+ )
+ },
+ {
+ label: 'Chứng chỉ',
+ labelClass: 'text-sm',
+ value: (
+
+ {detail.licenseInfo?.licenseNumber}
+
+ )
+ }
+ ]
+
+ const tabItems = [
+ { id: 'bio', label: 'Giới thiệu' },
+ { id: 'specializations', label: 'Lĩnh vực chuyên môn' },
+ { id: 'history', label: 'Lịch sử hoạt động' },
+ { id: 'reviews', label: 'Đánh giá' }
+ ] as const
+
return (
-
+
{/* Back Button */}
{/* Hero Card */}
-
-
+
+
-
+
{detail.name}
- {detail.licenseInfo.isVerified && (
-
-
- Đã xác thực
-
+ {detail.licenseInfo?.isVerified && (
+
+
+
+ )}
+
+ {currentStatus.label}
+
+
+
+
{detail.roleName === 'LAWYER' ? 'Luật sư tư vấn' : (detail.roleName || 'Luật sư')}
+
+ {/* Contact & Location details */}
+
+ {cityText && (
+
+ {cityText}
+
+ )}
+ {detail.email && (
+
+ {detail.email}
+
+ )}
+ {detail.phone && (
+
+ {detail.phone}
+
)}
-
{detail.role}
-
- {cityText}, Việt Nam
-
-
-
Đánh giá
-
-
- {detail.averageRating.toFixed(1)}
-
-
-
-
-
Kinh nghiệm
-
- {detail.experienceYears} năm
-
-
-
-
-
Chứng chỉ
-
- {detail.licenseInfo.licenseNumber}
+ {stats.map((stat, idx) => (
+
+
+ {stat.label}
+
+ {stat.value}
-
+ ))}
+
{/* Main Grid Content */}
-
+
{/* Left Column: Details */}
- {/* Bio */}
-
- Giới thiệu
- {bioText}
-
-
- {/* Specializations */}
-
- Lĩnh vực chuyên môn
-
- {detail.specializations.map((spec: string) => (
-
+ {tabItems.map((tab) => {
+ const isActive = activeTab === tab.id
+ return (
+
- ))}
-
-
+ {tab.label}
+
+ )
+ })}
+
- {/* Career Milestones / History */}
-
- Lịch sử hoạt động
-
-
-
-
+ {/* Tab Contents */}
+
+ {activeTab === 'bio' && (
+
+ Giới thiệu
+ {bioText}
+
+ )}
+
+ {activeTab === 'specializations' && (
+
+ Lĩnh vực chuyên môn
+
+ {detail.specializations && detail.specializations.length > 0 ? (
+ detail.specializations.map((spec: string) => (
+
+ {spec}
+
+ ))
+ ) : (
+ Chưa cập nhật lĩnh vực chuyên môn
+ )}
-
-
Kinh nghiệm làm việc
-
{detail.careerHistory}
+
+ )}
+
+ {activeTab === 'history' && (
+
+ Lịch sử hoạt động
+
+ {/* Working history */}
+ {detail.careerHistory && (
+
+
Kinh nghiệm làm việc
+
{detail.careerHistory}
+
+ )}
+
+ {/* License issuer */}
+ {detail.licenseInfo?.licenseIssuer && (
+
+
Đơn vị cấp thẻ hành nghề
+
{detail.licenseInfo.licenseIssuer}
+
+ )}
+
+ {/* License file URL if available */}
+ {detail.licenseInfo?.licenseFileUrl && (
+
+ )}
+
+ {/* Career Milestones */}
+ {detail.careerMilestones && detail.careerMilestones.length > 0 && (
+
+
+
+ Mốc sự nghiệp nổi bật
+
+
+ {detail.careerMilestones.map((milestone: string, idx: number) => (
+ - {milestone}
+ ))}
+
+
+ )}
-
-
-
-
+
+ )}
+
+ {activeTab === 'reviews' && (
+
+
+
Đánh giá từ khách hàng
+
+
+
{detail.averageRating.toFixed(1)}
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+
+ {mockReviews.length} đánh giá
+
+
+
+ Đánh giá trung bình phản ánh sự hài lòng của khách hàng sau khi nhận tư vấn trực tiếp và trực tuyến từ Luật sư {detail.name}.
+
+
-
-
Đơn vị cấp thẻ hành nghề
-
{detail.licenseInfo.licenseIssuer}
+
+
+ {mockReviews.map((review) => (
+
+
+
+
+ {review.userName.charAt(0)}
+
+
+
{review.userName}
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+
+
+
+ {new Date(review.date).toLocaleDateString('vi-VN')}
+
+
+
+ "{review.comment}"
+
+
+ ))}
-
-
-
+
+ )}
+
{/* Right Column: Pricing & Booking */}
-
+
-
Phí tư vấn cơ bản
+
Phí tư vấn cơ bản
{detail.consultingFee.toLocaleString('vi-VN')}
- / giờ
+ / giờ
@@ -212,30 +431,13 @@ export default function LawyerProfile() {
Hỗ trợ bảo mật
- 100%
+ 100%
-
+
)
}
diff --git a/frontend/src/pages/users/lawyer/components/LawyerCard.tsx b/frontend/src/pages/users/lawyer/components/LawyerCard.tsx
index dd52b4f..85bb770 100644
--- a/frontend/src/pages/users/lawyer/components/LawyerCard.tsx
+++ b/frontend/src/pages/users/lawyer/components/LawyerCard.tsx
@@ -38,7 +38,7 @@ export const LawyerCard = memo(function LawyerCard({
{/* Avatar */}
- {lawyer.city}
+ {lawyer.location}
diff --git a/frontend/src/pages/users/lawyer/components/LawyerContactDialog.tsx b/frontend/src/pages/users/lawyer/components/LawyerContactDialog.tsx
index 0ada1f2..d3f5418 100644
--- a/frontend/src/pages/users/lawyer/components/LawyerContactDialog.tsx
+++ b/frontend/src/pages/users/lawyer/components/LawyerContactDialog.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
-import { MapPin, Star, Check, Paperclip, X } from 'lucide-react'
+import { MapPin, Star, Check, Paperclip, X, Loader2 } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { Button } from '@/components/ui/button'
@@ -9,10 +9,19 @@ import { Input } from '@/components/ui/input'
import FileUpload from '@/components/upload-file/file-upload'
import toastifyCommon from '@/core/lib/toastify-common'
import { consultationApi } from '@/core/services/consultation.service'
+import { lawApi } from '@/core/services/law.service'
import { useAppointmentStore } from '@/core/store/features/appointments'
import { useUserInfo } from '@/hooks/tanstack-query/auth/use-query-auth'
import { type Lawyer } from '@/models/lawyer/list-lawyer.type'
+interface UploadedFile {
+ name: string
+ size: number
+ url?: string
+ isUploading?: boolean
+ error?: boolean
+}
+
interface LawyerContactDialogProps {
lawyer: Lawyer | null
isOpen: boolean
@@ -38,9 +47,9 @@ export function LawyerContactDialog({
}: LawyerContactDialogProps) {
const [step, setStep] = useState<1 | 2 | 3>(1)
const [contactForm, setContactForm] = useState({ name: '', phone: '', email: '', message: '' })
- const [selectedFiles, setSelectedFiles] = useState([])
+ const [selectedFiles, setSelectedFiles] = useState([])
const [prepopulatedFiles, setPrepopulatedFiles] = useState([])
- const [submitting, setSubmitting] = useState(false)
+ // const [submitting, setSubmitting] = useState(false)
const { data: user } = useUserInfo()
const navigate = useNavigate()
@@ -65,7 +74,7 @@ export function LawyerContactDialog({
const handleContactSubmit = async (e: React.FormEvent) => {
e.preventDefault()
- setSubmitting(true)
+ // setSubmitting(true)
try {
// Create a real consultation process in the database with the user's manually typed context/message
await consultationApi.createConsultation({
@@ -74,32 +83,36 @@ export function LawyerContactDialog({
message: contactForm.message
})
- // Add new request to the global appointments store
- const addRequest = useAppointmentStore.getState().addRequest
- addRequest({
- lawyerName: lawyer.fullName,
- lawyerAvatar: lawyer.avatar || '',
- topicVI: lawyer.specializations[0] || 'Tư vấn pháp lý',
- topicEN: lawyer.specializations[0] || 'Legal Consultation',
- message: contactForm.message,
- attachments: [
- ...prepopulatedFiles.map((file) => ({
- name: file.name,
- size: file.size || 'N/A'
- })),
- ...selectedFiles.map((file) => ({
+ // Add new request to the global appointments store
+ const addRequest = useAppointmentStore.getState().addRequest
+ addRequest({
+ lawyerName: lawyer.fullName,
+ lawyerAvatar: lawyer.avatar || '',
+ topicVI: lawyer.specializations[0] || 'Tư vấn pháp lý',
+ topicEN: lawyer.specializations[0] || 'Legal Consultation',
+ message: contactForm.message,
+ attachments: [
+ ...prepopulatedFiles.map((file) => ({
+ name: file.name,
+ size: file.size || 'N/A',
+ url: file.url
+ })),
+ ...selectedFiles
+ .filter((file) => !file.isUploading && !file.error)
+ .map((file) => ({
name: file.name,
- size: formatContactFileSize(file.size)
+ size: formatContactFileSize(file.size),
+ url: file.url
}))
- ]
- })
+ ]
+ })
setStep(3)
} catch (err) {
console.error('Failed to create consultation:', err)
toastifyCommon.error('Đăng ký tư vấn thất bại. Vui lòng thử lại!')
} finally {
- setSubmitting(false)
+ // setSubmitting(false)
}
}
@@ -125,7 +138,7 @@ export function LawyerContactDialog({
{lawyer.fullName}
- {lawyer.city}
+ {lawyer.location}
@@ -180,15 +193,15 @@ export function LawyerContactDialog({
-
Đăng ký tư vấn với Luật sư {lawyer.fullName}
-
{lawyer.city}
+
Luật sư {lawyer.fullName}
+
{lawyer.location}
-
+
setContactForm({ ...contactForm, name: e.target.value })}
@@ -197,7 +210,7 @@ export function LawyerContactDialog({
/>
-
+
setContactForm({ ...contactForm, phone: e.target.value })}
@@ -208,7 +221,7 @@ export function LawyerContactDialog({
-
+
-
+
diff --git a/frontend/src/pages/users/legal-analysis/LegalAnalysis.tsx b/frontend/src/pages/users/legal-analysis/LegalAnalysis.tsx
index c1f93e9..35965ce 100644
--- a/frontend/src/pages/users/legal-analysis/LegalAnalysis.tsx
+++ b/frontend/src/pages/users/legal-analysis/LegalAnalysis.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useMemo } from 'react'
-import { Award, Loader2 } from 'lucide-react'
+import { Loader2 } from 'lucide-react'
import { FadeUp } from '@/components/animated/animated-component'
import { Button } from '@/components/ui/button'
@@ -125,11 +125,7 @@ export default function LegalAnalysis() {
{/* Title section */}
-
-
- Thư viện Pháp lý
-
-
+
Văn bản pháp luật
diff --git a/frontend/src/pages/users/template/Template.tsx b/frontend/src/pages/users/template/Template.tsx
index ed21c92..4ba767e 100644
--- a/frontend/src/pages/users/template/Template.tsx
+++ b/frontend/src/pages/users/template/Template.tsx
@@ -59,7 +59,7 @@ export default function Template() {
{/* Header */}
-
+
Thư viện biểu mẫu
diff --git a/frontend/src/pages/users/template/components/TemplatePreviewModal.tsx b/frontend/src/pages/users/template/components/TemplatePreviewModal.tsx
index 2fc8df6..8c4c71e 100644
--- a/frontend/src/pages/users/template/components/TemplatePreviewModal.tsx
+++ b/frontend/src/pages/users/template/components/TemplatePreviewModal.tsx
@@ -172,7 +172,7 @@ export default function TemplatePreviewModal({ template, onClose, onUse }: Templ
{/* Body — iframe render hình dạng biểu mẫu */}
-