Cài đặt tính năng hiển thị chi phí
This commit is contained in:
Vendored
+4
-1
@@ -159,12 +159,15 @@ let TourController = class TourController {
|
||||
this.prisma = prisma;
|
||||
}
|
||||
async createTour(body, req) {
|
||||
const { title, startDate, endDate } = body;
|
||||
const { title, startDate, endDate, adultCount, childCount, childDiscount } = body;
|
||||
return this.prisma.tour.create({
|
||||
data: {
|
||||
title,
|
||||
startDate: startDate ? new Date(startDate) : null,
|
||||
endDate: endDate ? new Date(endDate) : null,
|
||||
adultCount: adultCount || 1,
|
||||
childCount: childCount || 0,
|
||||
childDiscount: childDiscount || 0,
|
||||
createdById: req.user.id,
|
||||
participants: {
|
||||
create: {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Tour" ADD COLUMN "adultCount" INTEGER NOT NULL DEFAULT 1,
|
||||
ADD COLUMN "childCount" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "childDiscount" INTEGER NOT NULL DEFAULT 30;
|
||||
@@ -83,6 +83,10 @@ model Tour {
|
||||
createdAt DateTime @default(now())
|
||||
totalCost Decimal @default(0) @db.Decimal(15, 2)
|
||||
|
||||
adultCount Int @default(1)
|
||||
childCount Int @default(0)
|
||||
childDiscount Int @default(30)
|
||||
|
||||
createdById String
|
||||
creator User @relation("TourCreator", fields: [createdById], references: [id])
|
||||
|
||||
|
||||
+4
-1
@@ -108,12 +108,15 @@ class TourController {
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
async createTour(@Body() body: any, @Req() req: any) {
|
||||
const { title, startDate, endDate } = body;
|
||||
const { title, startDate, endDate, adultCount, childCount, childDiscount } = body;
|
||||
return this.prisma.tour.create({
|
||||
data: {
|
||||
title,
|
||||
startDate: startDate ? new Date(startDate) : null,
|
||||
endDate: endDate ? new Date(endDate) : null,
|
||||
adultCount: adultCount || 1,
|
||||
childCount: childCount || 0,
|
||||
childDiscount: childDiscount || 0,
|
||||
createdById: req.user.id,
|
||||
participants: {
|
||||
create: {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { Trash2, Users } from 'lucide-react';
|
||||
import { useTourStore } from '@/store/useTourStore';
|
||||
|
||||
export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolean, onClose: () => void, onSuccess: (tour: any) => void }) => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [adultCount, setAdultCount] = useState(2);
|
||||
const [childCount, setChildCount] = useState(1);
|
||||
const [childDiscount, setChildDiscount] = useState(30);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const createTour = useTourStore((state) => state.createTour);
|
||||
|
||||
@@ -52,7 +55,15 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
|
||||
setError('');
|
||||
try {
|
||||
const memberIds = members.map((m) => m.id);
|
||||
const tour = await createTour({ title, startDate, endDate, memberIds });
|
||||
const tour = await createTour({
|
||||
title,
|
||||
startDate,
|
||||
endDate,
|
||||
memberIds,
|
||||
adultCount,
|
||||
childCount,
|
||||
childDiscount
|
||||
});
|
||||
onSuccess(tour);
|
||||
onClose();
|
||||
} catch (e: any) {
|
||||
@@ -104,6 +115,31 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50/50 p-4 rounded-2xl border border-blue-100 space-y-3">
|
||||
<div className="flex items-center gap-2 text-blue-600 mb-1">
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="text-xs font-black uppercase tracking-wider">Cơ cấu đoàn & Định mức chi phí</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-500 uppercase mb-1">Người lớn</label>
|
||||
<input type="number" className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
|
||||
value={adultCount} onChange={e => setAdultCount(Number(e.target.value))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-500 uppercase mb-1">Trẻ em</label>
|
||||
<input type="number" className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
|
||||
value={childCount} onChange={e => setChildCount(Number(e.target.value))} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-500 uppercase mb-1">Giảm trẻ em %</label>
|
||||
<input type="number" className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
|
||||
value={childDiscount} onChange={e => setChildDiscount(Number(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] text-blue-400 italic font-medium">* Dùng để tính toán đơn giá bình quân trong báo cáo chi phí.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">Thành viên tham gia</label>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
|
||||
@@ -1,73 +1,297 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Wallet, Users, Info } from 'lucide-react';
|
||||
import { Wallet, Users, Info, PieChart, Tag, Calendar, User, FileText, TrendingUp, BarChart3 } from 'lucide-react';
|
||||
import { useTourStore } from '@/store/useTourStore';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
|
||||
export const ExpenseManager = () => {
|
||||
const { legs } = useTourStore();
|
||||
const [adults, setAdults] = useState(2);
|
||||
const [children, setChildren] = useState(1);
|
||||
const [discount, setDiscount] = useState(30);
|
||||
const { legs, currentTour } = useTourStore();
|
||||
|
||||
const totals = useMemo(() => {
|
||||
const totalAmount = legs.reduce((acc, leg) =>
|
||||
acc + leg.expenses.reduce((lAcc: number, exp: any) => lAcc + Number(exp.amount), 0), 0
|
||||
);
|
||||
const adults = currentTour?.adultCount ?? 1;
|
||||
const children = currentTour?.childCount ?? 0;
|
||||
const discount = currentTour?.childDiscount ?? 0;
|
||||
|
||||
const childRateFactor = 1 - (discount / 100);
|
||||
const stats = useMemo(() => {
|
||||
const categoryMap: Record<string, number> = {
|
||||
ACCOMMODATION: 0,
|
||||
FOOD: 0,
|
||||
TRANSPORT: 0,
|
||||
TICKET: 0,
|
||||
OTHER: 0
|
||||
};
|
||||
const legMap: Record<string, number> = {};
|
||||
const memberMap: Record<string, number> = {};
|
||||
let totalAmount = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
(legs || []).forEach(leg => {
|
||||
let legTotal = 0;
|
||||
(leg.expenses || []).forEach((exp: any) => {
|
||||
const amt = Number(exp.amount);
|
||||
totalAmount += amt;
|
||||
legTotal += amt;
|
||||
totalCount += 1;
|
||||
|
||||
const memberName = exp.paidBy?.name || 'Chưa rõ';
|
||||
memberMap[memberName] = (memberMap[memberName] || 0) + amt;
|
||||
|
||||
if (categoryMap[exp.category] !== undefined) {
|
||||
categoryMap[exp.category] += amt;
|
||||
} else {
|
||||
categoryMap.OTHER += amt;
|
||||
}
|
||||
});
|
||||
legMap[leg.sequence] = legTotal;
|
||||
});
|
||||
|
||||
// Tìm hạng mục tốn nhất
|
||||
const maxCat = Object.entries(categoryMap).reduce((a, b) => b[1] > a[1] ? b : a);
|
||||
|
||||
// Tìm chặng tốn nhất
|
||||
const maxLegEntry = Object.entries(legMap).length > 0
|
||||
? Object.entries(legMap).reduce((a, b) => b[1] > a[1] ? b : a)
|
||||
: [null, 0];
|
||||
|
||||
const avgPerLeg = legs.length > 0 ? totalAmount / legs.length : 0;
|
||||
|
||||
// Danh sách phẳng tất cả chi phí để hiển thị bảng
|
||||
const flatExpenses = (legs || []).flatMap(leg =>
|
||||
(leg.expenses || []).map((exp: any) => ({
|
||||
...exp,
|
||||
legSequence: leg.sequence,
|
||||
// Lấy ngày của Location nếu có, nếu không lấy ngày của Leg
|
||||
date: exp.location?.plannedStart || leg.startDate || null
|
||||
}))
|
||||
).sort((a, b) => {
|
||||
if (!a.date || !b.date) return 0;
|
||||
return new Date(a.date).getTime() - new Date(b.date).getTime();
|
||||
});
|
||||
|
||||
const childRateFactor = 1 - (Number(discount) / 100);
|
||||
const weightedCount = adults + (children * childRateFactor);
|
||||
const adultPrice = totalAmount / weightedCount;
|
||||
const childPrice = adultPrice * childRateFactor;
|
||||
|
||||
return {
|
||||
total: totalAmount,
|
||||
count: totalCount,
|
||||
adultPrice: Math.round(adultPrice),
|
||||
childPrice: Math.round(childPrice)
|
||||
childPrice: Math.round(childPrice),
|
||||
categories: categoryMap,
|
||||
list: flatExpenses,
|
||||
topCategory: maxCat[0],
|
||||
topLeg: maxLegEntry[0],
|
||||
avgPerLeg,
|
||||
memberStats: Object.entries(memberMap)
|
||||
};
|
||||
}, [legs, adults, children, discount]);
|
||||
|
||||
const categoryLabels: Record<string, { label: string, color: string }> = {
|
||||
ACCOMMODATION: { label: 'Chỗ ở', color: 'bg-blue-500' },
|
||||
FOOD: { label: 'Ăn uống', color: 'bg-orange-500' },
|
||||
TRANSPORT: { label: 'Di chuyển', color: 'bg-indigo-500' },
|
||||
TICKET: { label: 'Vé tham quan', color: 'bg-green-500' },
|
||||
OTHER: { label: 'Khác', color: 'bg-gray-400' }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4">
|
||||
<div className="bg-white p-6 rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-6 text-blue-600">
|
||||
<Users className="w-5 h-5" />
|
||||
<h3 className="font-bold">Cấu hình thành viên</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">Người lớn</label>
|
||||
<input type="number" value={adults} onChange={e => setAdults(Number(e.target.value))} className="w-full p-2 bg-gray-50 rounded-lg border-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">Trẻ em</label>
|
||||
<input type="number" value={children} onChange={e => setChildren(Number(e.target.value))} className="w-full p-2 bg-gray-50 rounded-lg border-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">Giảm trẻ em (%)</label>
|
||||
<input type="number" value={discount} onChange={e => setDiscount(Number(e.target.value))} className="w-full p-2 bg-gray-50 rounded-lg border-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-600 rounded-2xl p-6 text-white shadow-lg shadow-blue-200">
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<p className="text-blue-100 text-sm">Tổng chi phí chuyến đi</p>
|
||||
<h2 className="text-3xl font-bold mt-1">{totals.total.toLocaleString()} VND</h2>
|
||||
<h2 className="text-3xl font-bold mt-1">{stats.total.toLocaleString()} VND</h2>
|
||||
<p className="text-blue-200 text-[10px] mt-1 font-bold uppercase tracking-wider">{stats.count} hóa đơn đã ghi nhận</p>
|
||||
</div>
|
||||
<Wallet className="w-8 h-8 opacity-20" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 border-t border-blue-500 pt-6">
|
||||
<div>
|
||||
<p className="text-blue-100 text-xs uppercase tracking-wider font-semibold">Mỗi người lớn</p>
|
||||
<p className="text-xl font-bold">{totals.adultPrice.toLocaleString()}đ</p>
|
||||
<p className="text-xl font-bold">{stats.adultPrice.toLocaleString()}đ</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-blue-100 text-xs uppercase tracking-wider font-semibold">Mỗi trẻ em (-{discount}%)</p>
|
||||
<p className="text-xl font-bold">{totals.childPrice.toLocaleString()}đ</p>
|
||||
<p className="text-xl font-bold">{stats.childPrice.toLocaleString()}đ</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Các thẻ chỉ số nhanh */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-sm flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-orange-50 flex items-center justify-center text-orange-500">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Hạng mục cao nhất</p>
|
||||
<p className="text-sm font-bold text-gray-800">{categoryLabels[stats.topCategory]?.label || 'Chưa có'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-sm flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-purple-50 flex items-center justify-center text-purple-500">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Trung bình / Chặng</p>
|
||||
<p className="text-sm font-bold text-gray-800">{Math.round(stats.avgPerLeg).toLocaleString()}đ</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-sm flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-green-50 flex items-center justify-center text-green-500">
|
||||
<Tag className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Chặng tốn kém nhất</p>
|
||||
<p className="text-sm font-bold text-gray-800">{stats.topLeg ? `Chặng ${stats.topLeg}` : 'Chưa có'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thống kê theo thành viên */}
|
||||
<div className="bg-white p-6 rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-6 text-gray-800">
|
||||
<Users className="w-5 h-5 text-blue-500" />
|
||||
<h3 className="font-bold">Thống kê theo người chi trả</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{stats.memberStats.length > 0 ? stats.memberStats.map(([name, amount]) => {
|
||||
const percentage = stats.total > 0 ? (amount / stats.total) * 100 : 0;
|
||||
return (
|
||||
<div key={name} className="flex items-center justify-between p-4 bg-gray-50 rounded-2xl border border-gray-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold text-xs">
|
||||
{name.charAt(0)}
|
||||
</div>
|
||||
<span className="text-sm font-bold text-gray-700">{name}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-black text-gray-900">{amount.toLocaleString()}đ</div>
|
||||
<div className="text-[10px] text-gray-400 font-bold uppercase">{percentage.toFixed(1)}% tổng chi</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}) : (
|
||||
<div className="col-span-full py-8 text-center text-gray-400 text-sm italic">
|
||||
Chưa có thông tin chi trả của thành viên.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thống kê theo hạng mục */}
|
||||
<div className="bg-white p-6 rounded-2xl border border-gray-100 shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-6 text-gray-800">
|
||||
<PieChart className="w-5 h-5 text-indigo-500" />
|
||||
<h3 className="font-bold">Phân tích chi tiêu</h3>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{stats.total > 0 ? Object.entries(stats.categories).map(([key, value]) => {
|
||||
const percentage = stats.total > 0 ? (value / stats.total) * 100 : 0;
|
||||
if (value === 0) return null;
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className="flex justify-between text-xs mb-1.5 font-bold">
|
||||
<span className="text-gray-600 flex items-center gap-1.5">
|
||||
<div className={`w-2 h-2 rounded-full ${categoryLabels[key].color}`} />
|
||||
{categoryLabels[key].label}
|
||||
</span>
|
||||
<span className="text-gray-900">{value.toLocaleString()}đ ({percentage.toFixed(1)}%)</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-gray-50 rounded-full overflow-hidden border border-gray-100">
|
||||
<div className={`h-full ${categoryLabels[key].color} transition-all duration-500`} style={{ width: `${percentage}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}) : (
|
||||
<div className="py-8 text-center text-gray-400 text-sm italic">
|
||||
Chưa có dữ liệu phân tích theo hạng mục.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bảng thống kê chi tiết chi phí */}
|
||||
<div className="bg-white rounded-3xl border border-gray-100 shadow-sm overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-100 bg-gray-50/30 flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-blue-600" />
|
||||
<h3 className="font-black text-gray-900 uppercase tracking-tight">Bảng kê chi tiết các khoản chi</h3>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50/50 text-[10px] font-black text-gray-400 uppercase tracking-widest border-b border-gray-100">
|
||||
<th className="px-4 py-4 text-center">STT</th>
|
||||
<th className="px-4 py-4 min-w-[100px]">Ngày giờ</th>
|
||||
<th className="px-4 py-4 min-w-[80px]">Chặng</th>
|
||||
<th className="px-4 py-4">Dịch vụ sử dụng</th>
|
||||
<th className="px-4 py-4 text-right">Số tiền</th>
|
||||
<th className="px-4 py-4 min-w-[120px]">Người đã trả</th>
|
||||
<th className="px-4 py-4">Ghi chú</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{stats.list.length > 0 ? (
|
||||
stats.list.map((exp: any, index: number) => (
|
||||
<tr key={exp.id} className="hover:bg-blue-50/20 transition-colors text-sm">
|
||||
<td className="px-4 py-4 text-center font-bold text-gray-400">{index + 1}</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-1.5 text-gray-600 font-medium">
|
||||
<Calendar className="w-3.5 h-3.5 opacity-40" />
|
||||
{exp.date ? format(new Date(exp.date), 'dd/MM HH:mm') : '--/--'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<span className="px-2 py-1 bg-blue-50 text-blue-600 rounded-lg font-bold text-[10px]">
|
||||
Chặng {exp.legSequence}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 font-semibold text-gray-800">
|
||||
{exp.description || 'Không có mô tả'}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-right font-black text-blue-600">
|
||||
{Number(exp.amount).toLocaleString()}đ
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-1.5 text-gray-600">
|
||||
<User className="w-3.5 h-3.5 opacity-40" />
|
||||
{exp.paidBy?.name || 'Chưa rõ'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-xs text-gray-400 italic">
|
||||
{exp.note || '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
// Khung bảng mẫu khi chưa có dữ liệu (Placeholder)
|
||||
[1, 2, 3].map((i) => (
|
||||
<tr key={`sample-${i}`} className="opacity-30 grayscale pointer-events-none select-none text-sm">
|
||||
<td className="px-4 py-4 text-center font-bold text-gray-300">{i}</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-1.5 text-gray-300 font-medium">
|
||||
<Calendar className="w-3.5 h-3.5 opacity-20" /> --/-- --:--
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-400 rounded-lg font-bold text-[10px]">Chặng -</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-gray-300 italic">Ví dụ: Vé tham quan, Tiền ăn trưa...</td>
|
||||
<td className="px-4 py-4 text-right font-black text-gray-300">0đ</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex items-center gap-1.5 text-gray-300">
|
||||
<User className="w-3.5 h-3.5 opacity-20" /> Chưa có dữ liệu
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-xs text-gray-200 italic">-</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-gray-400 text-xs px-2">
|
||||
<Info className="w-4 h-4" />
|
||||
<p>Chi phí được tự động tính toán dựa trên hóa đơn của các chặng.</p>
|
||||
|
||||
@@ -539,12 +539,15 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
||||
|
||||
{/* Financial Quick-View Widget or Quote */}
|
||||
<div className="max-w-2xl mx-auto -mt-10 px-4 relative z-10">
|
||||
<div className={`rounded-3xl p-6 shadow-2xl transition-all duration-500 ${hasFinanceAccess ? 'bg-indigo-600 text-white shadow-indigo-200' : 'bg-white text-gray-600 border border-gray-100'}`}>
|
||||
<div
|
||||
onClick={() => hasFinanceAccess && setActiveTab('expense')}
|
||||
className={`rounded-3xl p-6 shadow-2xl transition-all duration-500 ${hasFinanceAccess ? 'bg-indigo-600 text-white shadow-indigo-200 cursor-pointer hover:scale-[1.02] active:scale-95' : 'bg-white text-gray-600 border border-gray-100'}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
{hasFinanceAccess ? (
|
||||
<>
|
||||
<div>
|
||||
<p className="text-indigo-100 text-xs font-black uppercase tracking-widest mb-1">Tổng chi tiêu hiện tại</p>
|
||||
<p className="text-indigo-100 text-[10px] font-black uppercase tracking-widest mb-1">Tổng chi tiêu hiện tại (Nhấn để xem chi tiết)</p>
|
||||
<h3 className="text-3xl font-black">{tourInfo.budget}</h3>
|
||||
</div>
|
||||
<div className="p-4 bg-white/10 rounded-2xl backdrop-blur-md"><Wallet className="w-8 h-8" /></div>
|
||||
|
||||
Reference in New Issue
Block a user