Bổ sung người thanh toán vào thêm địa điểm
This commit is contained in:
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"continue.enableConsole": true
|
||||
}
|
||||
+119
-1
@@ -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
@@ -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ụ / Mô 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
@@ -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>
|
||||
|
||||
|
||||
Vendored
+14
-11
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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"] })] }))] }) }));
|
||||
};
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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;
|
||||
|
||||
Vendored
+3
-2
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-2
@@ -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! ---');
|
||||
|
||||
Vendored
+1
-1
@@ -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"}
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "isBlocked" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -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;
|
||||
+9
-9
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user