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
@@ -3,8 +3,126 @@
|
|||||||
Tài liệu này mô tả kiến trúc tổng thể, mô hình dữ liệu và các giải pháp kỹ thuật cho tính năng lập kế hoạch du lịch đa người dùng thuộc nền tảng **Travel**.
|
Tài liệu này mô tả kiến trúc tổng thể, mô hình dữ liệu và các giải pháp kỹ thuật cho tính năng lập kế hoạch du lịch đa người dùng thuộc nền tảng **Travel**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 1. Kiến Trúc Tổng Quan (System Overview)
|
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
|
|||||||
+57
-28
@@ -70,13 +70,16 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
note: '',
|
note: '',
|
||||||
expenseAmount: '',
|
expenseAmount: '',
|
||||||
expenseCategory: 'OTHER',
|
expenseCategory: 'OTHER',
|
||||||
|
expenseDescription: '',
|
||||||
|
expenseNote: '',
|
||||||
|
paidById: '',
|
||||||
plannedStart: '',
|
plannedStart: '',
|
||||||
plannedEnd: ''
|
plannedEnd: ''
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
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
|
// 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
|
// 1. Luôn khai báo Hook ở cấp cao nhất, không đặt code logic/return giữa các Hook
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -95,6 +98,9 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
note: editingLocation.note || '',
|
note: editingLocation.note || '',
|
||||||
expenseAmount: expense?.amount?.toString() || '',
|
expenseAmount: expense?.amount?.toString() || '',
|
||||||
expenseCategory: expense?.category || 'OTHER',
|
expenseCategory: expense?.category || 'OTHER',
|
||||||
|
expenseDescription: expense?.description || '',
|
||||||
|
expenseNote: expense?.note || '',
|
||||||
|
paidById: expense?.paidById || '',
|
||||||
plannedStart: editingLocation.plannedStart ? editingLocation.plannedStart.slice(0, 16) : '',
|
plannedStart: editingLocation.plannedStart ? editingLocation.plannedStart.slice(0, 16) : '',
|
||||||
plannedEnd: editingLocation.plannedEnd ? editingLocation.plannedEnd.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: '',
|
name: '', address: '',
|
||||||
legId: initialLegId || (legs.length > 0 ? legs[0].id : ''),
|
legId: initialLegId || (legs.length > 0 ? legs[0].id : ''),
|
||||||
note: '', expenseAmount: '', expenseCategory: 'OTHER',
|
note: '', expenseAmount: '', expenseCategory: 'OTHER',
|
||||||
|
expenseDescription: '', expenseNote: '', paidById: '',
|
||||||
plannedStart: '', plannedEnd: ''
|
plannedStart: '', plannedEnd: ''
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -146,20 +153,17 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
|
const payload: any = {
|
||||||
|
...formData,
|
||||||
|
legId: currentLegId,
|
||||||
|
latitude: parseFloat(formData.latitude as any),
|
||||||
|
longitude: parseFloat(formData.longitude as any),
|
||||||
|
};
|
||||||
|
|
||||||
if (editingLocation) {
|
if (editingLocation) {
|
||||||
await updateLocation(editingLocation.id, {
|
await updateLocation(editingLocation.id, payload);
|
||||||
...formData,
|
|
||||||
legId: currentLegId,
|
|
||||||
latitude: parseFloat(formData.latitude as any),
|
|
||||||
longitude: parseFloat(formData.longitude as any),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
await addLocation(tourId, {
|
await addLocation(tourId, payload);
|
||||||
...formData,
|
|
||||||
legId: currentLegId,
|
|
||||||
latitude: parseFloat(formData.latitude as any),
|
|
||||||
longitude: parseFloat(formData.longitude as any),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -206,27 +210,52 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
value={formData.address} onChange={e => setFormData({...formData, address: e.target.value})} />
|
value={formData.address} onChange={e => setFormData({...formData, address: e.target.value})} />
|
||||||
</div>
|
</div>
|
||||||
<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}
|
<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..."
|
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})} />
|
value={formData.note} onChange={e => setFormData({...formData, note: e.target.value})} />
|
||||||
</div>
|
</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">
|
||||||
<div>
|
<p className="text-xs font-black text-blue-500 uppercase tracking-widest">Chi phí nhanh tại điểm này</p>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">Số tiền (VNĐ)</label>
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<input type="number" className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
|
<div>
|
||||||
placeholder="0"
|
<label className="block text-xs font-bold text-gray-600 mb-1">Số tiền (VNĐ)</label>
|
||||||
value={formData.expenseAmount} onChange={e => setFormData({...formData, expenseAmount: e.target.value})} />
|
<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-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>
|
||||||
|
<option value="ACCOMMODATION">Chỗ ở</option>
|
||||||
|
<option value="TICKET">Vé tham quan</option>
|
||||||
|
<option value="OTHER">Khác</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">Loại dịch vụ</label>
|
<label className="block text-xs font-bold text-gray-600 mb-1">Dịch vụ / Mô tả</label>
|
||||||
<select className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-xl outline-none"
|
<input 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})}>
|
placeholder="Ví dụ: Ăn trưa, taxi, vé..."
|
||||||
<option value="FOOD">Ăn uống</option>
|
value={formData.expenseDescription} onChange={e => setFormData({...formData, expenseDescription: e.target.value})} />
|
||||||
<option value="TRANSPORT">Di chuyển</option>
|
</div>
|
||||||
<option value="ACCOMMODATION">Chỗ ở</option>
|
<div>
|
||||||
<option value="TICKET">Vé tham quan</option>
|
<label className="block text-xs font-bold text-gray-600 mb-1">Ghi chú chi phí</label>
|
||||||
<option value="OTHER">Khác</option>
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+17
-6
@@ -247,12 +247,23 @@ export const ItineraryTimeline = ({
|
|||||||
<span>Thời gian dừng: {formatTravelTime(dwellMinutes)}</span>
|
<span>Thời gian dừng: {formatTravelTime(dwellMinutes)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{locationExpense && (
|
{locationExpense && (
|
||||||
<div className="flex items-center text-xs text-indigo-600 font-bold mt-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">
|
||||||
<Zap className="w-3 h-3 mr-1" />
|
<div className="flex items-center gap-1 font-bold">
|
||||||
<span>Chi phí: {Number(locationExpense.amount).toLocaleString()}đ ({locationExpense.category})</span>
|
<Zap className="w-3 h-3" />
|
||||||
</div>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="text-right flex flex-col items-end">
|
<div className="text-right flex flex-col items-end">
|
||||||
|
|||||||
Vendored
+17
-14
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 locationExpense = leg.expenses?.find((e) => e.locationId === location.id);
|
||||||
const isStartPoint = legs[0]?.id === leg.id && idx === 0;
|
const isStartPoint = legs[0]?.id === leg.id && idx === 0;
|
||||||
const isEndPoint = legs[legs.length - 1]?.id === leg.id && idx === leg.locations.length - 1;
|
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));
|
}) })] }, 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"] })] }))] }) }));
|
})), ['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;
|
private prisma;
|
||||||
constructor(prisma: PrismaService);
|
constructor(prisma: PrismaService);
|
||||||
validate(payload: any): Promise<{
|
validate(payload: any): Promise<{
|
||||||
name: string | null;
|
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
passwordHash: string;
|
passwordHash: string;
|
||||||
|
name: string | null;
|
||||||
avatar: string | null;
|
avatar: string | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
|||||||
Vendored
+3
-2
@@ -157,7 +157,9 @@ let TourController = class TourController {
|
|||||||
category: body.expenseCategory || 'OTHER',
|
category: body.expenseCategory || 'OTHER',
|
||||||
locationId: loc.id,
|
locationId: loc.id,
|
||||||
legId: loc.legId,
|
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' },
|
orderBy: { sequence: 'asc' },
|
||||||
include: {
|
include: {
|
||||||
locations: { orderBy: { plannedStart: 'asc' } },
|
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... ---');
|
console.log('--- Đang tạo chi phí mẫu... ---');
|
||||||
await prisma.expense.create({
|
const expense1 = await prisma.expense.create({
|
||||||
data: {
|
data: {
|
||||||
legId: leg1.id,
|
legId: leg1.id,
|
||||||
category: 'FOOD',
|
category: 'FOOD',
|
||||||
amount: 500000,
|
amount: 500000,
|
||||||
currency: 'VND',
|
|
||||||
description: 'Ăn trưa đặc sản Quận 1',
|
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! ---');
|
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":"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"}
|
||||||
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,
|
plannedEnd: body.plannedEnd ? new Date(body.plannedEnd) : null,
|
||||||
}
|
}
|
||||||
}).then(async (loc) => {
|
}).then(async (loc) => {
|
||||||
// Thêm nhanh chi phí nếu có số tiền
|
|
||||||
if (body.expenseAmount && Number(body.expenseAmount) > 0) {
|
if (body.expenseAmount && Number(body.expenseAmount) > 0) {
|
||||||
await this.prisma.expense.create({
|
await this.prisma.expense.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -142,7 +141,9 @@ class TourController {
|
|||||||
category: body.expenseCategory || 'OTHER',
|
category: body.expenseCategory || 'OTHER',
|
||||||
locationId: loc.id,
|
locationId: loc.id,
|
||||||
legId: loc.legId,
|
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' },
|
orderBy: { sequence: 'asc' },
|
||||||
include: {
|
include: {
|
||||||
locations: { orderBy: { plannedStart: 'asc' } },
|
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
|
-- CreateEnum
|
||||||
CREATE TYPE "ParticipantRole" AS ENUM ('OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY');
|
CREATE TYPE "ParticipantRole" AS ENUM ('OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY');
|
||||||
|
|
||||||
@@ -83,12 +80,13 @@ CREATE TABLE "Location" (
|
|||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "Expense" (
|
CREATE TABLE "Expense" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"legId" TEXT NOT NULL,
|
"leg_id" TEXT NOT NULL,
|
||||||
"locationId" TEXT,
|
"location_id" TEXT,
|
||||||
"category" "ExpenseCategory" NOT NULL,
|
"category" "ExpenseCategory" NOT NULL,
|
||||||
"amount" DECIMAL(15,2) NOT NULL,
|
"amount" DECIMAL(15,2) NOT NULL,
|
||||||
"currency" TEXT NOT NULL DEFAULT 'VND',
|
|
||||||
"description" TEXT,
|
"description" TEXT,
|
||||||
|
"note" TEXT,
|
||||||
|
"paid_by_id" TEXT,
|
||||||
|
|
||||||
CONSTRAINT "Expense_pkey" PRIMARY KEY ("id")
|
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;
|
ALTER TABLE "Location" ADD CONSTRAINT "Location_legId_fkey" FOREIGN KEY ("legId") REFERENCES "Leg"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- 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
|
-- 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
|
-- AddForeignKey
|
||||||
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_tourId_fkey" FOREIGN KEY ("tourId") REFERENCES "Tour"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
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
|
-- AddForeignKey
|
||||||
ALTER TABLE "Photo" ADD CONSTRAINT "Photo_uploaderId_fkey" FOREIGN KEY ("uploaderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
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")
|
createdTours Tour[] @relation("TourCreator")
|
||||||
tourParticipations TourParticipant[]
|
tourParticipations TourParticipant[]
|
||||||
uploadedPhotos Photo[]
|
uploadedPhotos Photo[]
|
||||||
|
paidExpenses Expense[] @relation("ExpensePaidBy")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Tour {
|
model Tour {
|
||||||
@@ -128,9 +129,12 @@ model Expense {
|
|||||||
category ExpenseCategory
|
category ExpenseCategory
|
||||||
amount Decimal @db.Decimal(15, 2)
|
amount Decimal @db.Decimal(15, 2)
|
||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
|
note String? @db.Text
|
||||||
|
paidById String? @map("paid_by_id")
|
||||||
|
|
||||||
leg Leg @relation(fields: [legId], references: [id], onDelete: Cascade)
|
leg Leg @relation(fields: [legId], references: [id], onDelete: Cascade)
|
||||||
location Location? @relation(fields: [locationId], 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 {
|
model Photo {
|
||||||
|
|||||||
@@ -79,13 +79,14 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('--- Đang tạo chi phí mẫu... ---');
|
console.log('--- Đang tạo chi phí mẫu... ---');
|
||||||
await prisma.expense.create({
|
const expense1 = await prisma.expense.create({
|
||||||
data: {
|
data: {
|
||||||
legId: leg1.id,
|
legId: leg1.id,
|
||||||
category: 'FOOD',
|
category: 'FOOD',
|
||||||
amount: 500000,
|
amount: 500000,
|
||||||
currency: 'VND',
|
|
||||||
description: 'Ăn trưa đặc sản Quận 1',
|
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