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:
|
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