Bổ sung người thanh toán vào thêm địa điểm

This commit is contained in:
2026-06-14 15:59:27 +07:00
parent 65a9b39966
commit 71007bd9a1
23 changed files with 245 additions and 449 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"continue.enableConsole": true
}
+119 -1
View File
@@ -4,7 +4,125 @@ Tài liệu này mô tả kiến trúc tổng thể, mô hình dữ liệu và c
---
## 1. Kiến Trúc Tổng Quan (System Overview)
## 8. Cấu Trúc Thư Mục (Directory Structure)
```text
/home/locpham/travelplanning/
├── admin.guard.ts # Middleware kiểm tra quyền admin
├── App.tsx # Component chính của ứng dụng frontend
├── ARCHITECTURE.md # Tài liệu kiến trúc hệ thống
├── CreateTourModal.tsx # Modal cho tạo tour
├── dist/ # Thư mục build output
├── .env # Biến môi trường
├── ExpenseManager.tsx # Component quản lý chi phí
├── ExploreMap.tsx # Component bản đồ khám phá
├── index.tsx # Điểm vào ứng dụng frontend
├── ItineraryTimeline.tsx # Component timeline lịch trình
├── jwt-auth.guard.ts # Middleware xác thực JWT
├── jwt.strategy.ts # Strategy xử lý JWT
├── LandingPage.tsx # Trang landing page
├── LoginModal.tsx # Modal đăng nhập
├── main.ts # Điểm vào ứng dụng backend
├── migrations/ # Migration cũ của ORM khác
│ ├── 20260613030135_init_travel_planning_schema/
│ │ └── migration.sql
│ ├── 20260613091754_add_is_admin_field/
│ │ └── migration.sql
│ ├── 20260613112158/
│ │ └── migration.sql
│ ├── 20260613114646/
│ │ └── migration.sql
│ └── migration_lock.toml
├── nest-cli.json # Cấu hình NestJS CLI
├── node_modules/ # Thư mục dependencies
├── package.json # Cấu hình project Node.js
├── package-lock.json # Lockfile dependencies
├── postcss.config.js # Cấu hình PostCSS
├── prisma/ # Thư mục Prisma ORM
│ ├── migrations/
│ │ └── 0_init/
│ │ └── migration.sql
│ ├── schema.prisma # Schema dữ liệu chính
│ └── prisma.service.ts # Service Prisma
├── rbac.middleware.ts # Middleware phân quyền RBAC
├── README.md # Tài liệu hướng dẫn dự án
├── schema.sql # File SQL schema
├── seed.ts # File seed dữ liệu mẫu
├── SignupPage.tsx # Trang đăng ký tài khoản
├── tailwind.config.ts # Cấu hình TailwindCSS
├── TourDetailPage.tsx # Trang chi tiết tour
├── tsconfig.build.json # Cấu hình TypeScript build
├── tsconfig.json # Cấu hình TypeScript
├── UITourDesign.md # Tài liệu thiết kế UI
├── useTourStore.ts # State management Zustand cho Tour
├── vite.config.ts # Cấu hình Vite bundler
├── .gitignore # Git ignore rules
├── .vscode/
│ └── settings.json # Cấu hình VS Code
└── index.css # Styles CSS toàn cục
```
### Giải thích Cấu trúc Thư mục
**Frontend Layer:**
- Các file `.tsx` (React components) xử lý giao diện người dùng: `LandingPage`, `TourDetailPage`, `CreateTourModal`, `ExpenseManager`, `ItineraryTimeline`, `ExploreMap`, `UserManagementModal`, `SignupPage`, `LoginModal`, `AddLocationModal`
- Cấu hình TailwindCSS (`tailwind.config.ts`, `index.css`) cho styling
- `useTourStore.ts` - State management với Zustand
**Backend Layer:**
- `main.ts` - Entry point ứng dụng NestJS/Express
- `expense-engine.service.ts` - Service tính toán và chia chi phí
- `prisma.service.ts` - Service kết nối cơ sở dữ liệu
- Guards & Middleware: `admin.guard.ts`, `jwt-auth.guard.ts`, `rbac.middleware.ts`
- `jwt.strategy.ts` - Xác thực người dùng qua JWT
**Database Layer:**
- `prisma/schema.prisma` - Định nghĩa schema dữ liệu
- `prisma/migrations/` - Lịch sử thay đổi schema của Prisma
- `migrations/` - Migration cũ (không dùng Prisma)
- `seed.ts` - Chèn dữ liệu mẫu vào cơ sở dữ liệu
**Configuration:**
- `tsconfig.json`, `tsconfig.build.json` - Cấu hình TypeScript
- `vite.config.ts` - Config bundler Vite
- `nest-cli.json` - Cấu hình NestJS CLI
- `postcss.config.js` - Cấu hình PostCSS cho TailwindCSS
- `.env` - Biến môi trường ứng dụng
**Documentation:**
- `ARCHITECTURE.md` - Tài liệu kiến trúc hệ thống
- `UITourDesign.md` - Tài liệu thiết kế giao diện người dùng
- `README.md` - Hướng dẫn cài đặt và chạy dự án
**Tools & IDE:**
- `.vscode/settings.json` - Cấu hình VS Code workspace
- `.gitignore` - Quy tắc bỏ qua file Git
### Công nghệ trong Dự án
```json
{
"frontend": {
"framework": "React + Vite",
"styling": "TailwindCSS + PostCSS",
"state_management": "Zustand",
"map_engine": "Yêu cầu tích hợp Leaflet/Google Maps API"
},
"backend": {
"framework": "NestJS/Express với TypeScript",
"auth": "JWT Strategy + Guards",
"orm": "Prisma",
"database": "PostgreSQL + PostGIS"
},
"build_tools": {
"bundler": "Vite",
"compiler": "TypeScript"
}
}
```
---
Hệ thống được thiết kế theo mô hình **Client-Server** kết hợp với kiến trúc **Modular Monolith** hoặc **Microservices** (tùy quy mô triển khai), chia làm 3 tầng chính:
+48 -19
View File
@@ -70,13 +70,16 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
note: '',
expenseAmount: '',
expenseCategory: 'OTHER',
expenseDescription: '',
expenseNote: '',
paidById: '',
plannedStart: '',
plannedEnd: ''
});
const [isLoading, setIsLoading] = useState(false);
// Gom các selector lại để giảm số lượng Hook gọi nội bộ và tăng hiệu năng
const { legs, addLocation, updateLocation, mapCenter } = useTourStore();
const { legs, addLocation, updateLocation, mapCenter, currentTour } = useTourStore();
// 1. Luôn khai báo Hook ở cấp cao nhất, không đặt code logic/return giữa các Hook
useEffect(() => {
@@ -95,6 +98,9 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
note: editingLocation.note || '',
expenseAmount: expense?.amount?.toString() || '',
expenseCategory: expense?.category || 'OTHER',
expenseDescription: expense?.description || '',
expenseNote: expense?.note || '',
paidById: expense?.paidById || '',
plannedStart: editingLocation.plannedStart ? editingLocation.plannedStart.slice(0, 16) : '',
plannedEnd: editingLocation.plannedEnd ? editingLocation.plannedEnd.slice(0, 16) : ''
});
@@ -104,6 +110,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
name: '', address: '',
legId: initialLegId || (legs.length > 0 ? legs[0].id : ''),
note: '', expenseAmount: '', expenseCategory: 'OTHER',
expenseDescription: '', expenseNote: '', paidById: '',
plannedStart: '', plannedEnd: ''
}));
}
@@ -146,20 +153,17 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
e.preventDefault();
setIsLoading(true);
try {
const payload: any = {
...formData,
legId: currentLegId,
latitude: parseFloat(formData.latitude as any),
longitude: parseFloat(formData.longitude as any),
};
if (editingLocation) {
await updateLocation(editingLocation.id, {
...formData,
legId: currentLegId,
latitude: parseFloat(formData.latitude as any),
longitude: parseFloat(formData.longitude as any),
});
await updateLocation(editingLocation.id, payload);
} else {
await addLocation(tourId, {
...formData,
legId: currentLegId,
latitude: parseFloat(formData.latitude as any),
longitude: parseFloat(formData.longitude as any),
});
await addLocation(tourId, payload);
}
onClose();
} catch (error) {
@@ -206,21 +210,23 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
value={formData.address} onChange={e => setFormData({...formData, address: e.target.value})} />
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Ghi chú / Dịch vụ sử dụng</label>
<label className="block text-sm font-bold text-gray-700 mb-1">Ghi chú đa điểm / Dịch vụ sử dụng</label>
<textarea className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none resize-none" rows={2}
placeholder="Ví dụ: Ăn trưa tại quán X, thuê hướng dẫn viên..."
value={formData.note} onChange={e => setFormData({...formData, note: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-blue-50/60 border border-blue-100 rounded-2xl p-4 space-y-3">
<p className="text-xs font-black text-blue-500 uppercase tracking-widest">Chi phí nhanh tại điểm này</p>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Số tiền (VNĐ)</label>
<input type="number" className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
<label className="block text-xs font-bold text-gray-600 mb-1">Số tiền (VNĐ)</label>
<input type="number" className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
placeholder="0"
value={formData.expenseAmount} onChange={e => setFormData({...formData, expenseAmount: e.target.value})} />
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Loại dịch vụ</label>
<select className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
<label className="block text-xs font-bold text-gray-600 mb-1">Loại dịch vụ</label>
<select className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
value={formData.expenseCategory} onChange={e => setFormData({...formData, expenseCategory: e.target.value})}>
<option value="FOOD">Ăn uống</option>
<option value="TRANSPORT">Di chuyển</option>
@@ -230,6 +236,29 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
</select>
</div>
</div>
<div>
<label className="block text-xs font-bold text-gray-600 mb-1">Dịch vụ / tả</label>
<input className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
placeholder="Ví dụ: Ăn trưa, taxi, vé..."
value={formData.expenseDescription} onChange={e => setFormData({...formData, expenseDescription: e.target.value})} />
</div>
<div>
<label className="block text-xs font-bold text-gray-600 mb-1">Ghi chú chi phí</label>
<textarea className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none resize-none text-sm" rows={2}
placeholder="Ghi chú thêm..."
value={formData.expenseNote} onChange={e => setFormData({...formData, expenseNote: e.target.value})} />
</div>
<div>
<label className="block text-xs font-bold text-gray-600 mb-1">Thành viên đã thanh toán</label>
<select className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-sm"
value={formData.paidById} onChange={e => setFormData({...formData, paidById: e.target.value})}>
<option value="">-- Chọn người thanh toán --</option>
{currentTour?.participants?.map((p: any) => (
<option key={p.userId} value={p.userId}>{p.user?.name || p.userId}</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Gán vào chặng</label>
<select required className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
+13 -2
View File
@@ -248,10 +248,21 @@ export const ItineraryTimeline = ({
</div>
)}
{locationExpense && (
<div className="flex items-center text-xs text-indigo-600 font-bold mt-1">
<Zap className="w-3 h-3 mr-1" />
<div className="mt-2 text-xs text-indigo-600 bg-indigo-50/80 p-2 rounded-lg border border-indigo-100 space-y-1">
<div className="flex items-center gap-1 font-bold">
<Zap className="w-3 h-3" />
<span>Chi phí: {Number(locationExpense.amount).toLocaleString()}đ ({locationExpense.category})</span>
</div>
{locationExpense.description && (
<div className="text-[10px] text-gray-600">Dịch vụ: {locationExpense.description}</div>
)}
{locationExpense.note && (
<div className="text-[10px] text-gray-500 italic">{locationExpense.note}</div>
)}
{locationExpense.paidBy && (
<div className="text-[10px] font-semibold text-indigo-700">Đã thanh toán: {locationExpense.paidBy.name}</div>
)}
</div>
)}
</div>
+14 -11
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -89,7 +89,7 @@ export const ItineraryTimeline = ({ onAddLocation, onEditLocation }) => {
const locationExpense = leg.expenses?.find((e) => e.locationId === location.id);
const isStartPoint = legs[0]?.id === leg.id && idx === 0;
const isEndPoint = legs[legs.length - 1]?.id === leg.id && idx === leg.locations.length - 1;
return (_jsxs("div", { children: [_jsxs("div", { className: "relative flex group mb-6", children: [_jsx("div", { className: "z-10 mt-1.5 mr-4", children: _jsx("button", { onClick: () => toggleComplete(location.id), className: `transition-colors duration-200 ${location.status === 'COMPLETED' ? 'text-green-500' : 'text-gray-300 hover:text-blue-500'}`, children: location.status === 'COMPLETED' ? (_jsx(CheckCircle2, { className: "w-8 h-8 bg-white rounded-full" })) : (_jsx(Circle, { className: "w-8 h-8 bg-white rounded-full fill-white" })) }) }), _jsxs("div", { className: `flex-1 bg-white p-4 rounded-xl border transition-all duration-200 ${location.status === 'COMPLETED' ? 'border-green-100 bg-green-50/30' : 'border-gray-100 shadow-sm hover:shadow-md'}`, children: [_jsxs("div", { className: "flex justify-between items-start", children: [_jsxs("div", { children: [isStartPoint && (_jsx("span", { className: "inline-block px-2 py-0.5 bg-green-100 text-green-700 text-[10px] font-bold rounded-md mb-1 uppercase tracking-wider", children: "\u0110i\u1EC3m b\u1EAFt \u0111\u1EA7u" })), isEndPoint && (_jsx("span", { className: "inline-block px-2 py-0.5 bg-red-100 text-red-700 text-[10px] font-bold rounded-md mb-1 uppercase tracking-wider", children: "\u0110i\u1EC3m k\u1EBFt th\u00FAc" })), _jsx("h3", { className: `font-semibold text-lg ${location.status === 'COMPLETED' ? 'text-gray-500 line-through' : 'text-gray-800'}`, children: location.name }), _jsxs("div", { className: "flex items-center text-sm text-gray-500 mt-1", children: [_jsx(MapPin, { className: "w-3 h-3 mr-1" }), _jsx("span", { className: "truncate max-w-[200px] sm:max-w-md", children: location.address })] }), location.note && (_jsx("div", { className: "mt-2 text-xs text-gray-600 bg-gray-50 p-2 rounded-lg border border-gray-100 italic", children: location.note })), dwellMinutes !== null && (_jsxs("div", { className: "flex items-center text-xs text-amber-600 font-medium mt-1", children: [_jsx(Clock, { className: "w-3 h-3 mr-1" }), _jsxs("span", { children: ["Th\u1EDDi gian d\u1EEBng: ", formatTravelTime(dwellMinutes)] })] })), locationExpense && (_jsxs("div", { className: "flex items-center text-xs text-indigo-600 font-bold mt-1", children: [_jsx(Zap, { className: "w-3 h-3 mr-1" }), _jsxs("span", { children: ["Chi ph\u00ED: ", Number(locationExpense.amount).toLocaleString(), "\u0111 (", locationExpense.category, ")"] })] }))] }), _jsxs("div", { className: "text-right flex flex-col items-end", children: [_jsxs("div", { className: "flex items-center text-sm font-medium text-blue-600", children: [_jsx(Clock, { className: "w-3 h-3 mr-1" }), location.plannedStart ? format(parseISO(location.plannedStart), 'HH:mm') : '--:--'] }), location.status === 'COMPLETED' && location.actualStart && (_jsxs("div", { className: "text-[10px] text-gray-400 mt-1 italic", children: ["Th\u1EF1c t\u1EBF: ", format(parseISO(location.actualStart), 'HH:mm')] })), ['OWNER', 'MANAGER'].includes(userRole || '') && !isStartPoint && !isEndPoint && (_jsxs("div", { className: "flex gap-1 mt-2", children: [_jsx("button", { onClick: () => onEditLocation?.(location), className: "p-1 text-gray-400 hover:text-blue-600 transition-colors", children: _jsx(Edit2, { className: "w-3.5 h-3.5" }) }), _jsx("button", { onClick: () => handleDeleteLocation(location.id), className: "p-1 text-gray-400 hover:text-red-600 transition-colors", children: _jsx(Trash2, { className: "w-3.5 h-3.5" }) })] }))] })] }), _jsx(TimeVariance, { planned: location.plannedStart || '', actual: location.actualStart || null })] })] }), distanceToNext !== null && travelTimeMinutes !== null && (_jsxs("div", { className: "ml-4 -mt-4 mb-2 flex items-center gap-3", children: [_jsx("div", { className: "w-8 flex justify-center", children: _jsx(Navigation, { className: "w-3 h-3 text-blue-400 rotate-180" }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-[10px] font-bold text-blue-500 bg-blue-50 px-2 py-0.5 rounded-full border border-blue-100", children: [distanceToNext.toFixed(2), " km"] }), _jsxs("span", { className: "text-[10px] font-bold text-gray-500 bg-gray-50 px-2 py-0.5 rounded-full border border-gray-100 flex items-center gap-1", children: [_jsx(Clock, { className: "w-2.5 h-2.5" }), "~ ", formatTravelTime(travelTimeMinutes)] })] })] }))] }, location.id));
return (_jsxs("div", { children: [_jsxs("div", { className: "relative flex group mb-6", children: [_jsx("div", { className: "z-10 mt-1.5 mr-4", children: _jsx("button", { onClick: () => toggleComplete(location.id), className: `transition-colors duration-200 ${location.status === 'COMPLETED' ? 'text-green-500' : 'text-gray-300 hover:text-blue-500'}`, children: location.status === 'COMPLETED' ? (_jsx(CheckCircle2, { className: "w-8 h-8 bg-white rounded-full" })) : (_jsx(Circle, { className: "w-8 h-8 bg-white rounded-full fill-white" })) }) }), _jsxs("div", { className: `flex-1 bg-white p-4 rounded-xl border transition-all duration-200 ${location.status === 'COMPLETED' ? 'border-green-100 bg-green-50/30' : 'border-gray-100 shadow-sm hover:shadow-md'}`, children: [_jsxs("div", { className: "flex justify-between items-start", children: [_jsxs("div", { children: [isStartPoint && (_jsx("span", { className: "inline-block px-2 py-0.5 bg-green-100 text-green-700 text-[10px] font-bold rounded-md mb-1 uppercase tracking-wider", children: "\u0110i\u1EC3m b\u1EAFt \u0111\u1EA7u" })), isEndPoint && (_jsx("span", { className: "inline-block px-2 py-0.5 bg-red-100 text-red-700 text-[10px] font-bold rounded-md mb-1 uppercase tracking-wider", children: "\u0110i\u1EC3m k\u1EBFt th\u00FAc" })), _jsx("h3", { className: `font-semibold text-lg ${location.status === 'COMPLETED' ? 'text-gray-500 line-through' : 'text-gray-800'}`, children: location.name }), _jsxs("div", { className: "flex items-center text-sm text-gray-500 mt-1", children: [_jsx(MapPin, { className: "w-3 h-3 mr-1" }), _jsx("span", { className: "truncate max-w-[200px] sm:max-w-md", children: location.address })] }), location.note && (_jsx("div", { className: "mt-2 text-xs text-gray-600 bg-gray-50 p-2 rounded-lg border border-gray-100 italic", children: location.note })), dwellMinutes !== null && (_jsxs("div", { className: "flex items-center text-xs text-amber-600 font-medium mt-1", children: [_jsx(Clock, { className: "w-3 h-3 mr-1" }), _jsxs("span", { children: ["Th\u1EDDi gian d\u1EEBng: ", formatTravelTime(dwellMinutes)] })] })), locationExpense && (_jsxs("div", { className: "mt-2 text-xs text-indigo-600 bg-indigo-50/80 p-2 rounded-lg border border-indigo-100 space-y-1", children: [_jsxs("div", { className: "flex items-center gap-1 font-bold", children: [_jsx(Zap, { className: "w-3 h-3" }), _jsxs("span", { children: ["Chi ph\u00ED: ", Number(locationExpense.amount).toLocaleString(), "\u0111 (", locationExpense.category, ")"] })] }), locationExpense.description && (_jsxs("div", { className: "text-[10px] text-gray-600", children: ["D\u1ECBch v\u1EE5: ", locationExpense.description] })), locationExpense.note && (_jsx("div", { className: "text-[10px] text-gray-500 italic", children: locationExpense.note })), locationExpense.paidBy && (_jsxs("div", { className: "text-[10px] font-semibold text-indigo-700", children: ["\u0110\u00E3 thanh to\u00E1n: ", locationExpense.paidBy.name] }))] }))] }), _jsxs("div", { className: "text-right flex flex-col items-end", children: [_jsxs("div", { className: "flex items-center text-sm font-medium text-blue-600", children: [_jsx(Clock, { className: "w-3 h-3 mr-1" }), location.plannedStart ? format(parseISO(location.plannedStart), 'HH:mm') : '--:--'] }), location.status === 'COMPLETED' && location.actualStart && (_jsxs("div", { className: "text-[10px] text-gray-400 mt-1 italic", children: ["Th\u1EF1c t\u1EBF: ", format(parseISO(location.actualStart), 'HH:mm')] })), ['OWNER', 'MANAGER'].includes(userRole || '') && !isStartPoint && !isEndPoint && (_jsxs("div", { className: "flex gap-1 mt-2", children: [_jsx("button", { onClick: () => onEditLocation?.(location), className: "p-1 text-gray-400 hover:text-blue-600 transition-colors", children: _jsx(Edit2, { className: "w-3.5 h-3.5" }) }), _jsx("button", { onClick: () => handleDeleteLocation(location.id), className: "p-1 text-gray-400 hover:text-red-600 transition-colors", children: _jsx(Trash2, { className: "w-3.5 h-3.5" }) })] }))] })] }), _jsx(TimeVariance, { planned: location.plannedStart || '', actual: location.actualStart || null })] })] }), distanceToNext !== null && travelTimeMinutes !== null && (_jsxs("div", { className: "ml-4 -mt-4 mb-2 flex items-center gap-3", children: [_jsx("div", { className: "w-8 flex justify-center", children: _jsx(Navigation, { className: "w-3 h-3 text-blue-400 rotate-180" }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-[10px] font-bold text-blue-500 bg-blue-50 px-2 py-0.5 rounded-full border border-blue-100", children: [distanceToNext.toFixed(2), " km"] }), _jsxs("span", { className: "text-[10px] font-bold text-gray-500 bg-gray-50 px-2 py-0.5 rounded-full border border-gray-100 flex items-center gap-1", children: [_jsx(Clock, { className: "w-2.5 h-2.5" }), "~ ", formatTravelTime(travelTimeMinutes)] })] })] }))] }, location.id));
}) })] }, leg.id));
})), ['OWNER', 'MANAGER'].includes(userRole || '') && (_jsxs("div", { className: "flex flex-col gap-3 pb-20 mt-8", children: [_jsxs("button", { onClick: handleDeclareLegs, className: "w-full py-4 rounded-2xl bg-white text-blue-600 border-2 border-dashed border-blue-200 hover:bg-blue-50 transition-all flex items-center justify-center gap-2 text-sm font-bold", children: [_jsx(List, { className: "w-5 h-5" }), legs.length > 0 ? "Khai báo lại số lượng chặng" : "Bắt đầu bằng việc khai báo số chặng"] }), _jsxs("button", { onClick: handleAddLeg, className: "w-full py-4 rounded-2xl bg-blue-600 text-white shadow-lg shadow-blue-100 hover:bg-blue-700 transition-all flex items-center justify-center gap-2 text-sm font-bold", children: [_jsx(Plus, { className: "w-5 h-5" }), " Th\u00EAm ch\u1EB7ng l\u1EBB v\u00E0o cu\u1ED1i"] })] }))] }) }));
};
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,10 +7,10 @@ export declare class JwtStrategy extends JwtStrategy_base {
private prisma;
constructor(prisma: PrismaService);
validate(payload: any): Promise<{
name: string | null;
id: string;
email: string;
passwordHash: string;
name: string | null;
avatar: string | null;
createdAt: Date;
isAdmin: boolean;
+3 -2
View File
@@ -157,7 +157,9 @@ let TourController = class TourController {
category: body.expenseCategory || 'OTHER',
locationId: loc.id,
legId: loc.legId,
description: `Chi phí tại ${loc.name}`
description: body.expenseDescription || `Chi phí tại ${loc.name}`,
note: body.expenseNote || null,
paidById: body.paidById || null,
}
});
}
@@ -309,7 +311,6 @@ let TourController = class TourController {
orderBy: { sequence: 'asc' },
include: {
locations: { orderBy: { plannedStart: 'asc' } },
expenses: true,
},
},
},
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -70,13 +70,14 @@ async function main() {
},
});
console.log('--- Đang tạo chi phí mẫu... ---');
await prisma.expense.create({
const expense1 = await prisma.expense.create({
data: {
legId: leg1.id,
category: 'FOOD',
amount: 500000,
currency: 'VND',
description: 'Ăn trưa đặc sản Quận 1',
note: 'Đặt trước cho 3 người',
paidById: owner.id,
},
});
console.log('--- Seed dữ liệu hoàn tất! ---');
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../seed.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../seed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC1B,OAAO,eAAe,CAAC;AACvB,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC;AACtE,MAAM,OAAO,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;AACnC,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AAE7C,KAAK,UAAU,IAAI;IACjB,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;IAE9C,MAAM,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;IAClC,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;IAChC,MAAM,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;IAC/B,MAAM,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;IAE/B,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QACrC,IAAI,EAAE;YACJ,KAAK,EAAE,kBAAkB;YACzB,IAAI,EAAE,qBAAqB;YAE3B,YAAY,EAAE,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;YAC7C,OAAO,EAAE,IAAI;SACd;KACF,CAAC,CAAC;IAEH,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QAC3C,IAAI,EAAE;YACJ,KAAK,EAAE,wBAAwB;YAC/B,IAAI,EAAE,8BAA8B;YACpC,YAAY,EAAE,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;SAC9C;KACF,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QACpC,IAAI,EAAE;YACJ,KAAK,EAAE,4BAA4B;YACnC,SAAS,EAAE,IAAI,IAAI,CAAC,YAAY,CAAC;YACjC,OAAO,EAAE,IAAI,IAAI,CAAC,YAAY,CAAC;YAC/B,WAAW,EAAE,KAAK,CAAC,EAAE;YACrB,YAAY,EAAE;gBACZ,MAAM,EAAE;oBACN,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;oBACnC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE;iBAChD;aACF;SACF;KACF,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;QACnC,IAAI,EAAE;YACJ,MAAM,EAAE,IAAI,CAAC,EAAE;YACf,QAAQ,EAAE,CAAC;YACX,IAAI,EAAE,4BAA4B;YAClC,SAAS,EAAE;gBACT,MAAM,EAAE;oBACN;wBACE,IAAI,EAAE,cAAc;wBACpB,OAAO,EAAE,+BAA+B;wBACxC,QAAQ,EAAE,MAAM;wBAChB,SAAS,EAAE,OAAO;wBAClB,YAAY,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC;qBAC/C;oBACD;wBACE,IAAI,EAAE,oBAAoB;wBAC1B,OAAO,EAAE,0BAA0B;wBACnC,QAAQ,EAAE,MAAM;wBAChB,SAAS,EAAE,OAAO;wBAClB,YAAY,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC;qBAC/C;iBACF;aACF;SACF;KACF,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;QAC3C,IAAI,EAAE;YACJ,KAAK,EAAE,IAAI,CAAC,EAAE;YACd,QAAQ,EAAE,MAAM;YAChB,MAAM,EAAE,MAAM;YACd,WAAW,EAAE,wBAAwB;YACrC,IAAI,EAAE,uBAAuB;YAC7B,QAAQ,EAAE,KAAK,CAAC,EAAE;SACnB;KACF,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;IAC9C,OAAO,CAAC,GAAG,CAAC,0BAA0B,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,+BAA+B,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC;IAChE,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,IAAI,EAAE;KACH,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACX,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;KACD,OAAO,CAAC,KAAK,IAAI,EAAE;IAClB,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;AACnB,CAAC,CAAC,CAAC"}
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -134,7 +134,6 @@ class TourController {
plannedEnd: body.plannedEnd ? new Date(body.plannedEnd) : null,
}
}).then(async (loc) => {
// Thêm nhanh chi phí nếu có số tiền
if (body.expenseAmount && Number(body.expenseAmount) > 0) {
await this.prisma.expense.create({
data: {
@@ -142,7 +141,9 @@ class TourController {
category: body.expenseCategory || 'OTHER',
locationId: loc.id,
legId: loc.legId,
description: `Chi phí tại ${loc.name}`
description: body.expenseDescription || `Chi phí tại ${loc.name}`,
note: body.expenseNote || null,
paidById: body.paidById || null,
}
});
}
@@ -344,7 +345,6 @@ class TourController {
orderBy: { sequence: 'asc' },
include: {
locations: { orderBy: { plannedStart: 'asc' } },
expenses: true,
},
},
},
@@ -1,148 +0,0 @@
-- CreateEnum
CREATE TYPE "MemberRole" AS ENUM ('OWNER', 'EDITOR', 'MEMBER_PLAN_ONLY', 'MEMBER_PHOTO_ONLY', 'VIEWER_EXTERNAL');
-- CreateEnum
CREATE TYPE "ExpenseCategory" AS ENUM ('LODGING', 'DINING', 'TRANSPORT', 'OTHER');
-- CreateEnum
CREATE TYPE "TriggerType" AS ENUM ('AUTO_BY_TIME', 'MANUAL_BY_USER');
-- CreateEnum
CREATE TYPE "PrivacyLevel" AS ENUM ('PUBLIC_IN_TOUR', 'PRIVATE_OWNER');
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"name" TEXT,
"avatar" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Tour" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"startDate" TIMESTAMP(3),
"endDate" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"creatorId" INTEGER NOT NULL,
CONSTRAINT "Tour_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TourMember" (
"tourId" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,
"role" "MemberRole" NOT NULL DEFAULT 'MEMBER_PLAN_ONLY',
CONSTRAINT "TourMember_pkey" PRIMARY KEY ("tourId","userId")
);
-- CreateTable
CREATE TABLE "Leg" (
"id" SERIAL NOT NULL,
"tourId" INTEGER NOT NULL,
"sequenceNumber" INTEGER NOT NULL,
"notes" TEXT,
CONSTRAINT "Leg_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Place" (
"id" SERIAL NOT NULL,
"legId" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"address" TEXT,
"latitude" DOUBLE PRECISION NOT NULL,
"longitude" DOUBLE PRECISION NOT NULL,
"sequenceInLeg" INTEGER NOT NULL,
"arrivalTime" TIMESTAMP(3),
"departureTime" TIMESTAMP(3),
CONSTRAINT "Place_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Expense" (
"id" SERIAL NOT NULL,
"legId" INTEGER NOT NULL,
"placeId" INTEGER,
"category" "ExpenseCategory" NOT NULL,
"amount" DECIMAL(15,2) NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'VND',
"description" TEXT,
CONSTRAINT "Expense_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Task" (
"id" SERIAL NOT NULL,
"tourId" INTEGER NOT NULL,
"legId" INTEGER,
"title" TEXT NOT NULL,
"plannedTimestamp" TIMESTAMP(3) NOT NULL,
"isCompleted" BOOLEAN NOT NULL DEFAULT false,
"completedAt" TIMESTAMP(3),
"triggerType" "TriggerType" NOT NULL DEFAULT 'MANUAL_BY_USER',
CONSTRAINT "Task_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Photo" (
"id" SERIAL NOT NULL,
"tourId" INTEGER NOT NULL,
"placeId" INTEGER,
"uploaderId" INTEGER NOT NULL,
"imageUrl" TEXT NOT NULL,
"capturedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"privacyLevel" "PrivacyLevel" NOT NULL DEFAULT 'PUBLIC_IN_TOUR',
CONSTRAINT "Photo_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Tour" ADD CONSTRAINT "Tour_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TourMember" ADD CONSTRAINT "TourMember_tourId_fkey" FOREIGN KEY ("tourId") REFERENCES "Tour"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TourMember" ADD CONSTRAINT "TourMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Leg" ADD CONSTRAINT "Leg_tourId_fkey" FOREIGN KEY ("tourId") REFERENCES "Tour"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Place" ADD CONSTRAINT "Place_legId_fkey" FOREIGN KEY ("legId") REFERENCES "Leg"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_legId_fkey" FOREIGN KEY ("legId") REFERENCES "Leg"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_placeId_fkey" FOREIGN KEY ("placeId") REFERENCES "Place"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Task" ADD CONSTRAINT "Task_tourId_fkey" FOREIGN KEY ("tourId") REFERENCES "Tour"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Task" ADD CONSTRAINT "Task_legId_fkey" FOREIGN KEY ("legId") REFERENCES "Leg"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_tourId_fkey" FOREIGN KEY ("tourId") REFERENCES "Tour"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_placeId_fkey" FOREIGN KEY ("placeId") REFERENCES "Place"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_uploaderId_fkey" FOREIGN KEY ("uploaderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -1,14 +0,0 @@
-- DropForeignKey
ALTER TABLE "Expense" DROP CONSTRAINT "Expense_legId_fkey";
-- DropForeignKey
ALTER TABLE "Expense" DROP CONSTRAINT "Expense_placeId_fkey";
-- AlterTable
ALTER TABLE "User" ADD COLUMN "isAdmin" BOOLEAN NOT NULL DEFAULT false;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_legId_fkey" FOREIGN KEY ("legId") REFERENCES "Leg"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_placeId_fkey" FOREIGN KEY ("placeId") REFERENCES "Place"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-2
View File
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "isBlocked" BOOLEAN NOT NULL DEFAULT false;
-211
View File
@@ -1,211 +0,0 @@
/*
Warnings:
- The values [LODGING,DINING] on the enum `ExpenseCategory` will be removed. If these variants are still used in the database, this will fail.
- The values [PUBLIC_IN_TOUR,PRIVATE_OWNER] on the enum `PrivacyLevel` will be removed. If these variants are still used in the database, this will fail.
- The primary key for the `Expense` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `placeId` on the `Expense` table. All the data in the column will be lost.
- The primary key for the `Leg` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `notes` on the `Leg` table. All the data in the column will be lost.
- You are about to drop the column `sequenceNumber` on the `Leg` table. All the data in the column will be lost.
- The primary key for the `Photo` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `placeId` on the `Photo` table. All the data in the column will be lost.
- You are about to drop the column `privacyLevel` on the `Photo` table. All the data in the column will be lost.
- The primary key for the `Tour` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `creatorId` on the `Tour` table. All the data in the column will be lost.
- The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the `Place` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Task` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `TourMember` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `sequence` to the `Leg` table without a default value. This is not possible if the table is not empty.
- Added the required column `createdById` to the `Tour` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "ParticipantRole" AS ENUM ('OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY');
-- CreateEnum
CREATE TYPE "LocationStatus" AS ENUM ('PENDING', 'COMPLETED');
-- CreateEnum
CREATE TYPE "LocationType" AS ENUM ('MOVE', 'VISIT', 'REST', 'EAT');
-- AlterEnum
BEGIN;
CREATE TYPE "ExpenseCategory_new" AS ENUM ('ACCOMMODATION', 'FOOD', 'TRANSPORT', 'TICKET', 'OTHER');
ALTER TABLE "Expense" ALTER COLUMN "category" TYPE "ExpenseCategory_new" USING ("category"::text::"ExpenseCategory_new");
ALTER TYPE "ExpenseCategory" RENAME TO "ExpenseCategory_old";
ALTER TYPE "ExpenseCategory_new" RENAME TO "ExpenseCategory";
DROP TYPE "public"."ExpenseCategory_old";
COMMIT;
-- AlterEnum
BEGIN;
CREATE TYPE "PrivacyLevel_new" AS ENUM ('PUBLIC', 'TOUR_ONLY', 'PRIVATE');
ALTER TABLE "public"."Photo" ALTER COLUMN "privacyLevel" DROP DEFAULT;
ALTER TABLE "Photo" ALTER COLUMN "privacy" TYPE "PrivacyLevel_new" USING ("privacy"::text::"PrivacyLevel_new");
ALTER TYPE "PrivacyLevel" RENAME TO "PrivacyLevel_old";
ALTER TYPE "PrivacyLevel_new" RENAME TO "PrivacyLevel";
DROP TYPE "public"."PrivacyLevel_old";
COMMIT;
-- DropForeignKey
ALTER TABLE "Expense" DROP CONSTRAINT "Expense_legId_fkey";
-- DropForeignKey
ALTER TABLE "Expense" DROP CONSTRAINT "Expense_placeId_fkey";
-- DropForeignKey
ALTER TABLE "Leg" DROP CONSTRAINT "Leg_tourId_fkey";
-- DropForeignKey
ALTER TABLE "Photo" DROP CONSTRAINT "Photo_placeId_fkey";
-- DropForeignKey
ALTER TABLE "Photo" DROP CONSTRAINT "Photo_tourId_fkey";
-- DropForeignKey
ALTER TABLE "Photo" DROP CONSTRAINT "Photo_uploaderId_fkey";
-- DropForeignKey
ALTER TABLE "Place" DROP CONSTRAINT "Place_legId_fkey";
-- DropForeignKey
ALTER TABLE "Task" DROP CONSTRAINT "Task_legId_fkey";
-- DropForeignKey
ALTER TABLE "Task" DROP CONSTRAINT "Task_tourId_fkey";
-- DropForeignKey
ALTER TABLE "Tour" DROP CONSTRAINT "Tour_creatorId_fkey";
-- DropForeignKey
ALTER TABLE "TourMember" DROP CONSTRAINT "TourMember_tourId_fkey";
-- DropForeignKey
ALTER TABLE "TourMember" DROP CONSTRAINT "TourMember_userId_fkey";
-- AlterTable
ALTER TABLE "Expense" DROP CONSTRAINT "Expense_pkey",
DROP COLUMN "placeId",
ADD COLUMN "locationId" TEXT,
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ALTER COLUMN "legId" SET DATA TYPE TEXT,
ADD CONSTRAINT "Expense_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "Expense_id_seq";
-- AlterTable
ALTER TABLE "Leg" DROP CONSTRAINT "Leg_pkey",
DROP COLUMN "notes",
DROP COLUMN "sequenceNumber",
ADD COLUMN "note" TEXT,
ADD COLUMN "sequence" INTEGER NOT NULL,
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ALTER COLUMN "tourId" SET DATA TYPE TEXT,
ADD CONSTRAINT "Leg_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "Leg_id_seq";
-- AlterTable
ALTER TABLE "Photo" DROP CONSTRAINT "Photo_pkey",
DROP COLUMN "placeId",
DROP COLUMN "privacyLevel",
ADD COLUMN "locationId" TEXT,
ADD COLUMN "metadata" JSONB,
ADD COLUMN "privacy" "PrivacyLevel" NOT NULL DEFAULT 'TOUR_ONLY',
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ALTER COLUMN "tourId" SET DATA TYPE TEXT,
ALTER COLUMN "uploaderId" SET DATA TYPE TEXT,
ADD CONSTRAINT "Photo_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "Photo_id_seq";
-- AlterTable
ALTER TABLE "Tour" DROP CONSTRAINT "Tour_pkey",
DROP COLUMN "creatorId",
ADD COLUMN "createdById" TEXT NOT NULL,
ADD COLUMN "totalCost" DECIMAL(15,2) NOT NULL DEFAULT 0,
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "Tour_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "Tour_id_seq";
-- AlterTable
ALTER TABLE "User" DROP CONSTRAINT "User_pkey",
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "User_id_seq";
-- DropTable
DROP TABLE "Place";
-- DropTable
DROP TABLE "Task";
-- DropTable
DROP TABLE "TourMember";
-- DropEnum
DROP TYPE "MemberRole";
-- DropEnum
DROP TYPE "TriggerType";
-- CreateTable
CREATE TABLE "TourParticipant" (
"tourId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"role" "ParticipantRole" NOT NULL DEFAULT 'MEMBER',
CONSTRAINT "TourParticipant_pkey" PRIMARY KEY ("tourId","userId")
);
-- CreateTable
CREATE TABLE "Location" (
"id" TEXT NOT NULL,
"legId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"address" TEXT,
"latitude" DOUBLE PRECISION NOT NULL,
"longitude" DOUBLE PRECISION NOT NULL,
"plannedStart" TIMESTAMP(3),
"plannedEnd" TIMESTAMP(3),
"actualStart" TIMESTAMP(3),
"actualEnd" TIMESTAMP(3),
"status" "LocationStatus" NOT NULL DEFAULT 'PENDING',
"type" "LocationType" NOT NULL DEFAULT 'VISIT',
CONSTRAINT "Location_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Tour" ADD CONSTRAINT "Tour_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TourParticipant" ADD CONSTRAINT "TourParticipant_tourId_fkey" FOREIGN KEY ("tourId") REFERENCES "Tour"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TourParticipant" ADD CONSTRAINT "TourParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Leg" ADD CONSTRAINT "Leg_tourId_fkey" FOREIGN KEY ("tourId") REFERENCES "Tour"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Location" ADD CONSTRAINT "Location_legId_fkey" FOREIGN KEY ("legId") REFERENCES "Leg"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_legId_fkey" FOREIGN KEY ("legId") REFERENCES "Leg"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_tourId_fkey" FOREIGN KEY ("tourId") REFERENCES "Tour"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_uploaderId_fkey" FOREIGN KEY ("uploaderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -1,6 +1,3 @@
-- CreateSchema
CREATE SCHEMA IF NOT EXISTS "public";
-- CreateEnum
CREATE TYPE "ParticipantRole" AS ENUM ('OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY');
@@ -83,12 +80,13 @@ CREATE TABLE "Location" (
-- CreateTable
CREATE TABLE "Expense" (
"id" TEXT NOT NULL,
"legId" TEXT NOT NULL,
"locationId" TEXT,
"leg_id" TEXT NOT NULL,
"location_id" TEXT,
"category" "ExpenseCategory" NOT NULL,
"amount" DECIMAL(15,2) NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'VND',
"description" TEXT,
"note" TEXT,
"paid_by_id" TEXT,
CONSTRAINT "Expense_pkey" PRIMARY KEY ("id")
);
@@ -126,10 +124,13 @@ ALTER TABLE "Leg" ADD CONSTRAINT "Leg_tourId_fkey" FOREIGN KEY ("tourId") REFERE
ALTER TABLE "Location" ADD CONSTRAINT "Location_legId_fkey" FOREIGN KEY ("legId") REFERENCES "Leg"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_legId_fkey" FOREIGN KEY ("legId") REFERENCES "Leg"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_leg_id_fkey" FOREIGN KEY ("leg_id") REFERENCES "Leg"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "Location"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Expense" ADD CONSTRAINT "Expense_paid_by_id_fkey" FOREIGN KEY ("paid_by_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_tourId_fkey" FOREIGN KEY ("tourId") REFERENCES "Tour"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -139,4 +140,3 @@ ALTER TABLE "Photo" ADD CONSTRAINT "Photo_locationId_fkey" FOREIGN KEY ("locatio
-- AddForeignKey
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_uploaderId_fkey" FOREIGN KEY ("uploaderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+4
View File
@@ -60,6 +60,7 @@ model User {
createdTours Tour[] @relation("TourCreator")
tourParticipations TourParticipant[]
uploadedPhotos Photo[]
paidExpenses Expense[] @relation("ExpensePaidBy")
}
model Tour {
@@ -128,9 +129,12 @@ model Expense {
category ExpenseCategory
amount Decimal @db.Decimal(15, 2)
description String? @db.Text
note String? @db.Text
paidById String? @map("paid_by_id")
leg Leg @relation(fields: [legId], references: [id], onDelete: Cascade)
location Location? @relation(fields: [locationId], references: [id], onDelete: Cascade)
paidBy User? @relation("ExpensePaidBy", fields: [paidById], references: [id], onDelete: SetNull)
}
model Photo {
+3 -2
View File
@@ -79,13 +79,14 @@ async function main() {
});
console.log('--- Đang tạo chi phí mẫu... ---');
await prisma.expense.create({
const expense1 = await prisma.expense.create({
data: {
legId: leg1.id,
category: 'FOOD',
amount: 500000,
currency: 'VND',
description: 'Ăn trưa đặc sản Quận 1',
note: 'Đặt trước cho 3 người',
paidById: owner.id,
},
});