Sửa đổi dữ liệu của hệ thống theo ARCHITECTURE.md
This commit is contained in:
+3
-3
@@ -87,15 +87,15 @@ export const ExploreMap = ({ onBack, onLogout, user }: { onBack: () => void, onL
|
||||
<RecenterMap position={userPos} />
|
||||
|
||||
{publicTours.map((tour) => {
|
||||
const place = tour.legs?.[0]?.places?.[0];
|
||||
if (!place) return null;
|
||||
const location = tour.legs?.[0]?.locations?.[0];
|
||||
if (!location) return null;
|
||||
|
||||
const tourImage = tour.photos?.[0]?.imageUrl || `https://picsum.photos/seed/${tour.id}/200/200`;
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={tour.id}
|
||||
position={[place.latitude, place.longitude]}
|
||||
position={[location.latitude, location.longitude]}
|
||||
icon={L.divIcon({
|
||||
className: 'custom-bubble',
|
||||
html: `
|
||||
|
||||
+17
-17
@@ -22,9 +22,9 @@ const TimeVariance = ({ planned, actual }: { planned: string, actual: string | n
|
||||
export const ItineraryTimeline = () => {
|
||||
const { legs } = useTourStore();
|
||||
|
||||
const toggleComplete = async (placeId: number) => {
|
||||
// Gọi API PATCH /api/v1/places/:id để cập nhật trạng thái
|
||||
console.log("Toggle status for place:", placeId);
|
||||
const toggleComplete = async (locationId: string) => {
|
||||
// Gọi API PATCH /api/v1/locations/:id để cập nhật trạng thái
|
||||
console.log("Toggle status for location:", locationId);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -37,7 +37,7 @@ export const ItineraryTimeline = () => {
|
||||
{/* Leg Header */}
|
||||
<div className="flex items-center mb-4 px-2">
|
||||
<div className="bg-blue-600 text-white text-sm font-bold px-3 py-1 rounded-full shadow-sm">
|
||||
Chặng {leg.sequenceNumber}
|
||||
Chặng {leg.sequence}
|
||||
</div>
|
||||
<div className="ml-4 h-[1px] flex-1 bg-gray-200" />
|
||||
</div>
|
||||
@@ -46,15 +46,15 @@ export const ItineraryTimeline = () => {
|
||||
<div className="absolute left-6 top-10 bottom-0 w-0.5 bg-gray-200 -z-0" />
|
||||
|
||||
<div className="space-y-6 ml-2">
|
||||
{leg.places.map((place) => (
|
||||
<div key={place.id} className="relative flex group">
|
||||
{leg.locations.map((location) => (
|
||||
<div key={location.id} className="relative flex group">
|
||||
{/* Timeline Node */}
|
||||
<div className="z-10 mt-1.5 mr-4">
|
||||
<button
|
||||
onClick={() => toggleComplete(place.id)}
|
||||
className={`transition-colors duration-200 ${place.isCompleted ? 'text-green-500' : 'text-gray-300 hover:text-blue-500'}`}
|
||||
onClick={() => toggleComplete(location.id)}
|
||||
className={`transition-colors duration-200 ${location.status === 'COMPLETED' ? 'text-green-500' : 'text-gray-300 hover:text-blue-500'}`}
|
||||
>
|
||||
{place.isCompleted ? (
|
||||
{location.status === 'COMPLETED' ? (
|
||||
<CheckCircle2 className="w-8 h-8 bg-white rounded-full" />
|
||||
) : (
|
||||
<Circle className="w-8 h-8 bg-white rounded-full fill-white" />
|
||||
@@ -64,34 +64,34 @@ export const ItineraryTimeline = () => {
|
||||
|
||||
{/* Card Content */}
|
||||
<div className={`flex-1 bg-white p-4 rounded-xl border transition-all duration-200 ${
|
||||
place.isCompleted ? 'border-green-100 bg-green-50/30' : 'border-gray-100 shadow-sm hover:shadow-md'
|
||||
location.status === 'COMPLETED' ? 'border-green-100 bg-green-50/30' : 'border-gray-100 shadow-sm hover:shadow-md'
|
||||
}`}>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className={`font-semibold text-lg ${place.isCompleted ? 'text-gray-500 line-through' : 'text-gray-800'}`}>
|
||||
{place.name}
|
||||
<h3 className={`font-semibold text-lg ${location.status === 'COMPLETED' ? 'text-gray-500 line-through' : 'text-gray-800'}`}>
|
||||
{location.name}
|
||||
</h3>
|
||||
<div className="flex items-center text-sm text-gray-500 mt-1">
|
||||
<MapPin className="w-3 h-3 mr-1" />
|
||||
<span className="truncate max-w-[200px] sm:max-w-md">{place.address}</span>
|
||||
<span className="truncate max-w-[200px] sm:max-w-md">{location.address}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right flex flex-col items-end">
|
||||
<div className="flex items-center text-sm font-medium text-blue-600">
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
{format(parseISO(place.arrivalTime), 'HH:mm')}
|
||||
{location.plannedStart ? format(parseISO(location.plannedStart), 'HH:mm') : '--:--'}
|
||||
</div>
|
||||
{place.isCompleted && place.completedAt && (
|
||||
{location.status === 'COMPLETED' && location.actualStart && (
|
||||
<div className="text-[10px] text-gray-400 mt-1 italic">
|
||||
Thực tế: {format(parseISO(place.completedAt), 'HH:mm')}
|
||||
Thực tế: {format(parseISO(location.actualStart), 'HH:mm')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logic tính toán độ lệch thời gian */}
|
||||
<TimeVariance planned={place.arrivalTime} actual={place.completedAt} />
|
||||
<TimeVariance planned={location.plannedStart || ''} actual={location.actualStart || null} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
+16
-4
@@ -13,12 +13,24 @@ import {
|
||||
|
||||
export const TourDetailPage = () => {
|
||||
const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo'>('plan');
|
||||
const { currentTour, fetchTour, userRole } = useTourStore();
|
||||
const { currentTour, fetchTour, fetchPublicTours, publicTours, userRole } = useTourStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Gọi ID 1 (Dữ liệu từ file seed) để kiểm tra
|
||||
fetchTour(1);
|
||||
}, [fetchTour]);
|
||||
const loadData = async () => {
|
||||
// Nếu chưa có tour nào trong store, thử tải danh sách public trước
|
||||
if (publicTours.length === 0) {
|
||||
await fetchPublicTours();
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Lấy tour đầu tiên từ danh sách khám phá để hiển thị demo
|
||||
if (publicTours.length > 0 && !currentTour) {
|
||||
fetchTour(publicTours[0].id);
|
||||
}
|
||||
}, [publicTours, currentTour, fetchTour]);
|
||||
|
||||
// Định nghĩa các tab dựa trên quyền hạn (RBAC) từ store
|
||||
const tabs = [
|
||||
|
||||
@@ -32,7 +32,7 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
|
||||
if (isOpen) fetchUsers();
|
||||
}, [isOpen]);
|
||||
|
||||
const handleToggleBlock = async (id: number) => {
|
||||
const handleToggleBlock = async (id: string) => {
|
||||
try {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
await fetch(`${API_BASE}/api/v1/users/block/${id}`, {
|
||||
@@ -45,7 +45,7 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Bạn có chắc chắn muốn xóa người dùng này?')) return;
|
||||
try {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
|
||||
Vendored
+3
-3
@@ -29,11 +29,11 @@ export const ExploreMap = ({ onBack, onLogout, user }) => {
|
||||
navigator.geolocation.getCurrentPosition((pos) => setUserPos([pos.coords.latitude, pos.coords.longitude]), () => console.log("Không thể lấy vị trí người dùng"));
|
||||
}, []);
|
||||
return (_jsxs("div", { className: "h-screen w-full relative", children: [_jsx("button", { onClick: onBack, className: "absolute top-6 left-6 z-[1000] bg-white p-3 rounded-full shadow-xl hover:bg-gray-50 transition-all", children: _jsx(X, { className: "w-6 h-6 text-gray-800" }) }), onLogout && (_jsxs("button", { onClick: onLogout, className: "absolute top-6 right-6 z-[1000] bg-white px-4 py-3 rounded-2xl shadow-xl hover:bg-red-50 hover:text-red-600 transition-all flex items-center gap-2 font-bold text-gray-700", children: [_jsx(LogOut, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "\u0110\u0103ng xu\u1EA5t" })] })), user?.isAdmin && (_jsxs("button", { onClick: () => setIsAdminModalOpen(true), className: "absolute top-6 right-40 z-[1000] bg-blue-600 px-4 py-3 rounded-2xl shadow-xl hover:bg-blue-700 text-white transition-all flex items-center gap-2 font-bold", children: [_jsx(Settings, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "Qu\u1EA3n l\u00FD h\u1EC7 th\u1ED1ng" })] })), _jsx("div", { className: "absolute top-6 left-20 z-[1000] bg-white/90 backdrop-blur-md px-6 py-3 rounded-2xl shadow-xl border border-white/20 hidden sm:block", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Navigation, { className: "w-4 h-4 text-blue-600" }), _jsx("span", { className: "font-bold text-gray-800", children: "\u0110ang kh\u00E1m ph\u00E1 khu v\u1EF1c c\u1EE7a b\u1EA1n" })] }) }), _jsxs(MapContainer, { center: userPos, zoom: 13, className: "h-full w-full", children: [_jsx(TileLayer, { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", attribution: '\u00A9 OpenStreetMap contributors' }), _jsx(RecenterMap, { position: userPos }), publicTours.map((tour) => {
|
||||
const place = tour.legs?.[0]?.places?.[0];
|
||||
if (!place)
|
||||
const location = tour.legs?.[0]?.locations?.[0];
|
||||
if (!location)
|
||||
return null;
|
||||
const tourImage = tour.photos?.[0]?.imageUrl || `https://picsum.photos/seed/${tour.id}/200/200`;
|
||||
return (_jsx(Marker, { position: [place.latitude, place.longitude], icon: L.divIcon({
|
||||
return (_jsx(Marker, { position: [location.latitude, location.longitude], icon: L.divIcon({
|
||||
className: 'custom-bubble',
|
||||
html: `
|
||||
<div class="relative group">
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"ExploreMap.js","sourceRoot":"","sources":["../ExploreMap.tsx"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"ExploreMap.js","sourceRoot":"","sources":["../ExploreMap.tsx"],"names":[],"mappings":"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"}
|
||||
Vendored
+3
-3
@@ -11,9 +11,9 @@ const TimeVariance = ({ planned, actual }) => {
|
||||
};
|
||||
export const ItineraryTimeline = () => {
|
||||
const { legs } = useTourStore();
|
||||
const toggleComplete = async (placeId) => {
|
||||
console.log("Toggle status for place:", placeId);
|
||||
const toggleComplete = async (locationId) => {
|
||||
console.log("Toggle status for location:", locationId);
|
||||
};
|
||||
return (_jsxs("div", { className: "max-w-2xl mx-auto p-4 sm:p-6 bg-gray-50 min-h-screen", children: [_jsx("h2", { className: "text-2xl font-bold text-gray-800 mb-8 px-2", children: "L\u1ED9 tr\u00ECnh chuy\u1EBFn \u0111i" }), _jsx("div", { className: "space-y-8", children: legs.map((leg, legIdx) => (_jsxs("div", { className: "relative", children: [_jsxs("div", { className: "flex items-center mb-4 px-2", children: [_jsxs("div", { className: "bg-blue-600 text-white text-sm font-bold px-3 py-1 rounded-full shadow-sm", children: ["Ch\u1EB7ng ", leg.sequenceNumber] }), _jsx("div", { className: "ml-4 h-[1px] flex-1 bg-gray-200" })] }), _jsx("div", { className: "absolute left-6 top-10 bottom-0 w-0.5 bg-gray-200 -z-0" }), _jsx("div", { className: "space-y-6 ml-2", children: leg.places.map((place) => (_jsxs("div", { className: "relative flex group", children: [_jsx("div", { className: "z-10 mt-1.5 mr-4", children: _jsx("button", { onClick: () => toggleComplete(place.id), className: `transition-colors duration-200 ${place.isCompleted ? 'text-green-500' : 'text-gray-300 hover:text-blue-500'}`, children: place.isCompleted ? (_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 ${place.isCompleted ? '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: [_jsx("h3", { className: `font-semibold text-lg ${place.isCompleted ? 'text-gray-500 line-through' : 'text-gray-800'}`, children: place.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: place.address })] })] }), _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" }), format(parseISO(place.arrivalTime), 'HH:mm')] }), place.isCompleted && place.completedAt && (_jsxs("div", { className: "text-[10px] text-gray-400 mt-1 italic", children: ["Th\u1EF1c t\u1EBF: ", format(parseISO(place.completedAt), 'HH:mm')] }))] })] }), _jsx(TimeVariance, { planned: place.arrivalTime, actual: place.completedAt })] })] }, place.id))) }), leg.notes && (_jsxs("p", { className: "ml-14 mt-4 text-sm text-gray-400 italic", children: ["* ", leg.notes] }))] }, leg.id))) })] }));
|
||||
return (_jsxs("div", { className: "max-w-2xl mx-auto p-4 sm:p-6 bg-gray-50 min-h-screen", children: [_jsx("h2", { className: "text-2xl font-bold text-gray-800 mb-8 px-2", children: "L\u1ED9 tr\u00ECnh chuy\u1EBFn \u0111i" }), _jsx("div", { className: "space-y-8", children: legs.map((leg, legIdx) => (_jsxs("div", { className: "relative", children: [_jsxs("div", { className: "flex items-center mb-4 px-2", children: [_jsxs("div", { className: "bg-blue-600 text-white text-sm font-bold px-3 py-1 rounded-full shadow-sm", children: ["Ch\u1EB7ng ", leg.sequence] }), _jsx("div", { className: "ml-4 h-[1px] flex-1 bg-gray-200" })] }), _jsx("div", { className: "absolute left-6 top-10 bottom-0 w-0.5 bg-gray-200 -z-0" }), _jsx("div", { className: "space-y-6 ml-2", children: leg.locations.map((location) => (_jsxs("div", { className: "relative flex group", 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: [_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 })] })] }), _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')] }))] })] }), _jsx(TimeVariance, { planned: location.plannedStart || '', actual: location.actualStart || null })] })] }, location.id))) }), leg.notes && (_jsxs("p", { className: "ml-14 mt-4 text-sm text-gray-400 italic", children: ["* ", leg.notes] }))] }, leg.id))) })] }));
|
||||
};
|
||||
//# sourceMappingURL=ItineraryTimeline.js.map
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"ItineraryTimeline.js","sourceRoot":"","sources":["../ItineraryTimeline.tsx"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"ItineraryTimeline.js","sourceRoot":"","sources":["../ItineraryTimeline.tsx"],"names":[],"mappings":"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"}
|
||||
Vendored
+13
-3
@@ -6,10 +6,20 @@ import { useTourStore } from './useTourStore.js';
|
||||
import { Map as MapIcon, Wallet, Image as ImageIcon, Calendar, Users, ChevronLeft } from 'lucide-react';
|
||||
export const TourDetailPage = () => {
|
||||
const [activeTab, setActiveTab] = useState('plan');
|
||||
const { currentTour, fetchTour, userRole } = useTourStore();
|
||||
const { currentTour, fetchTour, fetchPublicTours, publicTours, userRole } = useTourStore();
|
||||
useEffect(() => {
|
||||
fetchTour(1);
|
||||
}, [fetchTour]);
|
||||
const loadData = async () => {
|
||||
if (publicTours.length === 0) {
|
||||
await fetchPublicTours();
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (publicTours.length > 0 && !currentTour) {
|
||||
fetchTour(publicTours[0].id);
|
||||
}
|
||||
}, [publicTours, currentTour, fetchTour]);
|
||||
const tabs = [
|
||||
{ id: 'plan', label: 'Lộ trình', icon: MapIcon, visible: ['OWNER', 'EDITOR', 'MEMBER_PLAN_ONLY'].includes(userRole || '') },
|
||||
{ id: 'expense', label: 'Chi phí', icon: Wallet, visible: ['OWNER', 'EDITOR'].includes(userRole || '') },
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"TourDetailPage.js","sourceRoot":"","sources":["../TourDetailPage.tsx"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"TourDetailPage.js","sourceRoot":"","sources":["../TourDetailPage.tsx"],"names":[],"mappings":"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"}
|
||||
Vendored
+1
-1
@@ -8,7 +8,7 @@ export declare class JwtStrategy extends JwtStrategy_base {
|
||||
constructor(prisma: PrismaService);
|
||||
validate(payload: any): Promise<{
|
||||
email: string;
|
||||
id: number;
|
||||
id: string;
|
||||
name: string | null;
|
||||
passwordHash: string;
|
||||
avatar: string | null;
|
||||
|
||||
Vendored
+1
@@ -1 +1,2 @@
|
||||
import 'reflect-metadata';
|
||||
import 'dotenv/config';
|
||||
|
||||
Vendored
+14
-13
@@ -10,8 +10,9 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
||||
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
||||
return function (target, key) { decorator(target, key, paramIndex); }
|
||||
};
|
||||
import 'reflect-metadata';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseIntPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards } from '@nestjs/common';
|
||||
import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service.js';
|
||||
import 'dotenv/config';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
@@ -113,7 +114,7 @@ let TourController = class TourController {
|
||||
legs: {
|
||||
take: 1,
|
||||
include: {
|
||||
places: { take: 1 }
|
||||
locations: { take: 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,12 +124,12 @@ let TourController = class TourController {
|
||||
const tour = await this.prisma.tour.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
members: true,
|
||||
participants: true,
|
||||
photos: true,
|
||||
legs: {
|
||||
orderBy: { sequenceNumber: 'asc' },
|
||||
orderBy: { sequence: 'asc' },
|
||||
include: {
|
||||
places: { orderBy: { sequenceInLeg: 'asc' } },
|
||||
locations: { orderBy: { plannedStart: 'asc' } },
|
||||
expenses: true,
|
||||
},
|
||||
},
|
||||
@@ -147,9 +148,9 @@ __decorate([
|
||||
], TourController.prototype, "getPublicTours", null);
|
||||
__decorate([
|
||||
Get(':id'),
|
||||
__param(0, Param('id', ParseIntPipe)),
|
||||
__param(0, Param('id', ParseUUIDPipe)),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [Number]),
|
||||
__metadata("design:paramtypes", [String]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], TourController.prototype, "getTourDetails", null);
|
||||
TourController = __decorate([
|
||||
@@ -204,24 +205,24 @@ __decorate([
|
||||
], UserController.prototype, "getAllUsers", null);
|
||||
__decorate([
|
||||
Patch(':id'),
|
||||
__param(0, Param('id', ParseIntPipe)),
|
||||
__param(0, Param('id', ParseUUIDPipe)),
|
||||
__param(1, Body()),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [Number, Object]),
|
||||
__metadata("design:paramtypes", [String, Object]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], UserController.prototype, "updateUser", null);
|
||||
__decorate([
|
||||
Delete(':id'),
|
||||
__param(0, Param('id', ParseIntPipe)),
|
||||
__param(0, Param('id', ParseUUIDPipe)),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [Number]),
|
||||
__metadata("design:paramtypes", [String]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], UserController.prototype, "deleteUser", null);
|
||||
__decorate([
|
||||
Post('block/:id'),
|
||||
__param(0, Param('id', ParseIntPipe)),
|
||||
__param(0, Param('id', ParseUUIDPipe)),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [Number]),
|
||||
__metadata("design:paramtypes", [String]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], UserController.prototype, "toggleBlock", null);
|
||||
UserController = __decorate([
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -4,5 +4,6 @@ export default defineConfig({
|
||||
datasource: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
schema: 'prisma/schema.prisma',
|
||||
});
|
||||
//# sourceMappingURL=prisma.config.js.map
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"prisma.config.js","sourceRoot":"","sources":["../prisma.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,eAAe,CAAC;AAEvB,eAAe,YAAY,CAAC;IAC1B,UAAU,EAAE;QAEV,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY;KAC9B;CACF,CAAC,CAAC"}
|
||||
{"version":3,"file":"prisma.config.js","sourceRoot":"","sources":["../prisma.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,eAAe,CAAC;AAEvB,eAAe,YAAY,CAAC;IAC1B,UAAU,EAAE;QAEV,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY;KAC9B;IACD,MAAM,EAAE,sBAAsB;CAC/B,CAAC,CAAC"}
|
||||
Vendored
+7
-7
@@ -16,12 +16,12 @@ let TourRoleGuard = class TourRoleGuard {
|
||||
async canActivate(context) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
const tourId = parseInt(request.params.id || request.params.tourId);
|
||||
const tourId = request.params.id || request.params.tourId;
|
||||
const path = request.url;
|
||||
if (!user || isNaN(tourId)) {
|
||||
if (!user || !tourId) {
|
||||
throw new ForbiddenException("Thông tin xác thực hoặc mã Tour không hợp lệ.");
|
||||
}
|
||||
const membership = await this.prisma.tourMember.findUnique({
|
||||
const participation = await this.prisma.tourParticipant.findUnique({
|
||||
where: {
|
||||
tourId_userId: {
|
||||
tourId: tourId,
|
||||
@@ -29,14 +29,14 @@ let TourRoleGuard = class TourRoleGuard {
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!membership) {
|
||||
if (!participation) {
|
||||
throw new ForbiddenException("Bạn không phải là thành viên của tour này.");
|
||||
}
|
||||
request.tourMembership = membership;
|
||||
const role = membership.role;
|
||||
request.tourParticipation = participation;
|
||||
const role = participation.role;
|
||||
const isPlanPath = path.includes('/plans');
|
||||
const isExpensePath = path.includes('/expenses');
|
||||
if ((role === 'MEMBER_PHOTO_ONLY' || role === 'VIEWER_EXTERNAL') &&
|
||||
if ((role === 'MEMBER_NO_FINANCE' || role === 'VIEWER_ONLY') &&
|
||||
(isPlanPath || isExpensePath)) {
|
||||
throw new ForbiddenException("Bạn không có quyền xem kế hoạch và chi phí của tour này.");
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"rbac.middleware.js","sourceRoot":"","sources":["../rbac.middleware.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,UAAU,EAAiC,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAG7C,IAAM,aAAa,GAAnB,MAAM,aAAa;IACxB,YAAoB,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE7C,KAAK,CAAC,WAAW,CAAC,OAAyB;QACzC,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAG1B,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACpE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC;QAEzB,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,kBAAkB,CAAC,+CAA+C,CAAC,CAAC;QAChF,CAAC;QAOD,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC;YACzD,KAAK,EAAE;gBACL,aAAa,EAAE;oBACb,MAAM,EAAE,MAAM;oBACd,MAAM,EAAE,IAAI,CAAC,EAAE;iBAChB;aACF;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,kBAAkB,CAAC,4CAA4C,CAAC,CAAC;QAC7E,CAAC;QAGD,OAAO,CAAC,cAAc,GAAG,UAAU,CAAC;QAEpC,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC;QAC7B,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC3C,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAGjD,IACE,CAAC,IAAI,KAAK,mBAAmB,IAAI,IAAI,KAAK,iBAAiB,CAAC;YAC5D,CAAC,UAAU,IAAI,aAAa,CAAC,EAC7B,CAAC;YACD,MAAM,IAAI,kBAAkB,CAAC,0DAA0D,CAAC,CAAC;QAC3F,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AAlDY,aAAa;IADzB,UAAU,EAAE;qCAEiB,aAAa;GAD9B,aAAa,CAkDzB"}
|
||||
{"version":3,"file":"rbac.middleware.js","sourceRoot":"","sources":["../rbac.middleware.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,UAAU,EAAiC,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAG7C,IAAM,aAAa,GAAnB,MAAM,aAAa;IACxB,YAAoB,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE7C,KAAK,CAAC,WAAW,CAAC,OAAyB;QACzC,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAG1B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;QAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC;QAEzB,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACrB,MAAM,IAAI,kBAAkB,CAAC,+CAA+C,CAAC,CAAC;QAChF,CAAC;QAOD,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,UAAU,CAAC;YACjE,KAAK,EAAE;gBACL,aAAa,EAAE;oBACb,MAAM,EAAE,MAAM;oBACd,MAAM,EAAE,IAAI,CAAC,EAAE;iBAChB;aACF;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,MAAM,IAAI,kBAAkB,CAAC,4CAA4C,CAAC,CAAC;QAC7E,CAAC;QAGD,OAAO,CAAC,iBAAiB,GAAG,aAAa,CAAC;QAE1C,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC;QAChC,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC3C,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAGjD,IACE,CAAC,IAAI,KAAK,mBAAmB,IAAI,IAAI,KAAK,aAAa,CAAC;YACxD,CAAC,UAAU,IAAI,aAAa,CAAC,EAC7B,CAAC;YACD,MAAM,IAAI,kBAAkB,CAAC,0DAA0D,CAAC,CAAC;QAC3F,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AAlDY,aAAa;IADzB,UAAU,EAAE;qCAEiB,aAAa;GAD9B,aAAa,CAkDzB"}
|
||||
Vendored
+13
-13
@@ -2,6 +2,7 @@ import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import { Pool } from 'pg';
|
||||
import 'dotenv/config';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
const adapter = new PrismaPg(pool);
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
@@ -16,14 +17,15 @@ async function main() {
|
||||
data: {
|
||||
email: 'owner@travel.com',
|
||||
name: 'Lộc Phạm (Chủ Tour)',
|
||||
passwordHash: 'hashed_password_123',
|
||||
passwordHash: await bcrypt.hash('123456', 10),
|
||||
isAdmin: true,
|
||||
},
|
||||
});
|
||||
const photoMember = await prisma.user.create({
|
||||
data: {
|
||||
email: 'photomember@travel.com',
|
||||
name: 'Nguyễn Văn Ảnh (Chỉ xem ảnh)',
|
||||
passwordHash: 'hashed_password_456',
|
||||
passwordHash: await bcrypt.hash('123456', 10),
|
||||
},
|
||||
});
|
||||
console.log('--- Đang tạo Tour và phân quyền... ---');
|
||||
@@ -32,11 +34,11 @@ async function main() {
|
||||
title: 'Hành trình khám phá TP.HCM',
|
||||
startDate: new Date('2023-11-20'),
|
||||
endDate: new Date('2023-11-21'),
|
||||
creatorId: owner.id,
|
||||
members: {
|
||||
createdById: owner.id,
|
||||
participants: {
|
||||
create: [
|
||||
{ userId: owner.id, role: 'OWNER' },
|
||||
{ userId: photoMember.id, role: 'MEMBER_PHOTO_ONLY' },
|
||||
{ userId: photoMember.id, role: 'VIEWER_ONLY' },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -45,25 +47,23 @@ async function main() {
|
||||
const leg1 = await prisma.leg.create({
|
||||
data: {
|
||||
tourId: tour.id,
|
||||
sequenceNumber: 1,
|
||||
notes: 'Khám phá lịch sử trung tâm',
|
||||
places: {
|
||||
sequence: 1,
|
||||
note: 'Khám phá lịch sử trung tâm',
|
||||
locations: {
|
||||
create: [
|
||||
{
|
||||
name: 'Dinh Độc Lập',
|
||||
address: '135 Nam Kỳ Khởi Nghĩa, Quận 1',
|
||||
latitude: 10.777,
|
||||
longitude: 106.695,
|
||||
sequenceInLeg: 1,
|
||||
arrivalTime: new Date('2023-11-20T08:00:00Z'),
|
||||
plannedStart: new Date('2023-11-20T08:00:00Z'),
|
||||
},
|
||||
{
|
||||
name: 'Bưu điện Thành phố',
|
||||
address: '02 Công xã Paris, Quận 1',
|
||||
latitude: 10.779,
|
||||
longitude: 106.699,
|
||||
sequenceInLeg: 2,
|
||||
arrivalTime: new Date('2023-11-20T10:00:00Z'),
|
||||
plannedStart: new Date('2023-11-20T10:00:00Z'),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -73,7 +73,7 @@ async function main() {
|
||||
await prisma.expense.create({
|
||||
data: {
|
||||
legId: leg1.id,
|
||||
category: 'DINING',
|
||||
category: 'FOOD',
|
||||
amount: 500000,
|
||||
currency: 'VND',
|
||||
description: 'Ăn trưa đặc sản Quận 1',
|
||||
|
||||
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
Vendored
+1
-1
@@ -6,7 +6,7 @@ interface TourState {
|
||||
setTour: (tour: any) => void;
|
||||
updateLegs: (legs: any[]) => void;
|
||||
optimizeRouting: () => Promise<void>;
|
||||
fetchTour: (id: number) => Promise<void>;
|
||||
fetchTour: (id: string) => Promise<void>;
|
||||
fetchPublicTours: () => Promise<void>;
|
||||
}
|
||||
export declare const useTourStore: import("zustand").UseBoundStore<import("zustand").StoreApi<TourState>>;
|
||||
|
||||
Vendored
+5
-4
@@ -7,14 +7,15 @@ export const useTourStore = create((set, get) => ({
|
||||
setTour: (tour) => set({ currentTour: tour }),
|
||||
updateLegs: (legs) => set({ legs }),
|
||||
fetchTour: async (id) => {
|
||||
const currentUserId = 1;
|
||||
const response = await fetch(`http://localhost:3001/api/v1/tours/${id}`);
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`);
|
||||
const data = await response.json();
|
||||
const role = data.members?.find((m) => m.userId === currentUserId)?.role || 'VIEWER_EXTERNAL';
|
||||
const role = data.participants?.[0]?.role || 'VIEWER_ONLY';
|
||||
set({ currentTour: data, legs: data.legs || [], userRole: role });
|
||||
},
|
||||
fetchPublicTours: async () => {
|
||||
const response = await fetch(`http://localhost:3001/api/v1/tours/explore`);
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/explore`);
|
||||
const data = await response.json();
|
||||
set({ publicTours: data });
|
||||
},
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"useTourStore.js","sourceRoot":"","sources":["../useTourStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAcjC,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAY,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAC3D,WAAW,EAAE,IAAI;IACjB,IAAI,EAAE,EAAE;IACR,WAAW,EAAE,EAAE;IACf,QAAQ,EAAE,IAAI;IACd,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAC7C,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;IACnC,SAAS,EAAE,KAAK,EAAE,EAAU,EAAE,EAAE;QAE9B,MAAM,aAAa,GAAG,CAAC,CAAC;QACxB,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,sCAAsC,EAAE,EAAE,CAAC,CAAC;QACzE,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAEnC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,aAAa,CAAC,EAAE,IAAI,IAAI,iBAAiB,CAAC;QACnG,GAAG,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpE,CAAC;IACD,gBAAgB,EAAE,KAAK,IAAI,EAAE;QAC3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,4CAA4C,CAAC,CAAC;QAC3E,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,GAAG,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7B,CAAC;IACD,eAAe,EAAE,KAAK,IAAI,EAAE;IAE5B,CAAC;CACF,CAAC,CAAC,CAAC"}
|
||||
{"version":3,"file":"useTourStore.js","sourceRoot":"","sources":["../useTourStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAcjC,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAY,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAC3D,WAAW,EAAE,IAAI;IACjB,IAAI,EAAE,EAAE;IACR,WAAW,EAAE,EAAE;IACf,QAAQ,EAAE,IAAI;IACd,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAC7C,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;IACnC,SAAS,EAAE,KAAK,EAAE,EAAU,EAAE,EAAE;QAC9B,MAAM,QAAQ,GAAG,UAAU,MAAM,CAAC,QAAQ,CAAC,QAAQ,OAAO,CAAC;QAC3D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,iBAAiB,EAAE,EAAE,CAAC,CAAC;QAC/D,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAGnC,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,aAAa,CAAC;QAC3D,GAAG,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpE,CAAC;IACD,gBAAgB,EAAE,KAAK,IAAI,EAAE;QAC3B,MAAM,QAAQ,GAAG,UAAU,MAAM,CAAC,QAAQ,CAAC,QAAQ,OAAO,CAAC;QAC3D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,uBAAuB,CAAC,CAAC;QACjE,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,GAAG,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7B,CAAC;IACD,eAAe,EAAE,KAAK,IAAI,EAAE;IAE5B,CAAC;CACF,CAAC,CAAC,CAAC"}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'reflect-metadata';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseIntPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards } from '@nestjs/common';
|
||||
import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service.js';
|
||||
import 'dotenv/config';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
@@ -70,6 +71,7 @@ class AuthController {
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
|
||||
return this.prisma.user.create({
|
||||
// Bỏ createdTours: {} vì đây là quan hệ 1-n, Prisma sẽ tự hiểu là mảng rỗng
|
||||
data: { email, passwordHash, name, isAdmin: shouldBeAdmin },
|
||||
select: { id: true, email: true, name: true, isAdmin: true }
|
||||
});
|
||||
@@ -90,7 +92,7 @@ class TourController {
|
||||
legs: {
|
||||
take: 1,
|
||||
include: {
|
||||
places: { take: 1 }
|
||||
locations: { take: 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,16 +100,16 @@ class TourController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getTourDetails(@Param('id', ParseIntPipe) id: number) {
|
||||
async getTourDetails(@Param('id', ParseUUIDPipe) id: string) {
|
||||
const tour = await this.prisma.tour.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
members: true,
|
||||
participants: true,
|
||||
photos: true,
|
||||
legs: {
|
||||
orderBy: { sequenceNumber: 'asc' },
|
||||
orderBy: { sequence: 'asc' },
|
||||
include: {
|
||||
places: { orderBy: { sequenceInLeg: 'asc' } },
|
||||
locations: { orderBy: { plannedStart: 'asc' } },
|
||||
expenses: true,
|
||||
},
|
||||
},
|
||||
@@ -132,7 +134,7 @@ class UserController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
async updateUser(@Param('id', ParseIntPipe) id: number, @Body() data: any) {
|
||||
async updateUser(@Param('id', ParseUUIDPipe) id: string, @Body() data: any) {
|
||||
// Nếu đổi mật khẩu thì cần hash
|
||||
if (data.password) {
|
||||
data.passwordHash = await bcrypt.hash(data.password, 10);
|
||||
@@ -146,7 +148,7 @@ class UserController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async deleteUser(@Param('id', ParseIntPipe) id: number) {
|
||||
async deleteUser(@Param('id', ParseUUIDPipe) id: string) {
|
||||
// Không cho phép tự xóa chính mình hoặc xóa admin cuối cùng (logic đơn giản)
|
||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||
if (user?.isAdmin) {
|
||||
@@ -158,7 +160,7 @@ class UserController {
|
||||
}
|
||||
|
||||
@Post('block/:id')
|
||||
async toggleBlock(@Param('id', ParseIntPipe) id: number) {
|
||||
async toggleBlock(@Param('id', ParseUUIDPipe) id: string) {
|
||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||
if (!user) throw new NotFoundException('Người dùng không tồn tại');
|
||||
return this.prisma.user.update({
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
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;
|
||||
@@ -6,4 +6,5 @@ export default defineConfig({
|
||||
// Prisma CLI sẽ sử dụng url này để thực hiện migrate
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
schema: 'prisma/schema.prisma',
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
-- CreateSchema
|
||||
CREATE SCHEMA IF NOT EXISTS "public";
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ParticipantRole" AS ENUM ('OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ExpenseCategory" AS ENUM ('ACCOMMODATION', 'FOOD', 'TRANSPORT', 'TICKET', 'OTHER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LocationStatus" AS ENUM ('PENDING', 'COMPLETED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LocationType" AS ENUM ('MOVE', 'VISIT', 'REST', 'EAT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PrivacyLevel" AS ENUM ('PUBLIC', 'TOUR_ONLY', 'PRIVATE');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"avatar" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"isAdmin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isBlocked" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Tour" (
|
||||
"id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"startDate" TIMESTAMP(3),
|
||||
"endDate" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"totalCost" DECIMAL(15,2) NOT NULL DEFAULT 0,
|
||||
"createdById" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Tour_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- 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 "Leg" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tourId" TEXT NOT NULL,
|
||||
"sequence" INTEGER NOT NULL,
|
||||
"note" TEXT,
|
||||
|
||||
CONSTRAINT "Leg_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- 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")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Expense" (
|
||||
"id" TEXT NOT NULL,
|
||||
"legId" TEXT NOT NULL,
|
||||
"locationId" TEXT,
|
||||
"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 "Photo" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tourId" TEXT NOT NULL,
|
||||
"locationId" TEXT,
|
||||
"uploaderId" TEXT NOT NULL,
|
||||
"imageUrl" TEXT NOT NULL,
|
||||
"capturedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"metadata" JSONB,
|
||||
"privacy" "PrivacyLevel" NOT NULL DEFAULT 'TOUR_ONLY',
|
||||
|
||||
CONSTRAINT "Photo_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- 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;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
enum ParticipantRole {
|
||||
OWNER
|
||||
MANAGER
|
||||
MEMBER
|
||||
MEMBER_NO_FINANCE
|
||||
VIEWER_ONLY
|
||||
}
|
||||
|
||||
enum ExpenseCategory {
|
||||
ACCOMMODATION
|
||||
FOOD
|
||||
TRANSPORT
|
||||
TICKET
|
||||
OTHER
|
||||
}
|
||||
|
||||
enum LocationStatus {
|
||||
PENDING
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
enum LocationType {
|
||||
MOVE
|
||||
VISIT
|
||||
REST
|
||||
EAT
|
||||
}
|
||||
|
||||
enum PrivacyLevel {
|
||||
PUBLIC
|
||||
TOUR_ONLY
|
||||
PRIVATE
|
||||
}
|
||||
|
||||
// --- Models ---
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
passwordHash String
|
||||
name String?
|
||||
avatar String?
|
||||
createdAt DateTime @default(now())
|
||||
isAdmin Boolean @default(false)
|
||||
isBlocked Boolean @default(false)
|
||||
|
||||
createdTours Tour[] @relation("TourCreator")
|
||||
tourParticipations TourParticipant[]
|
||||
uploadedPhotos Photo[]
|
||||
}
|
||||
|
||||
model Tour {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
startDate DateTime?
|
||||
endDate DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
totalCost Decimal @default(0) @db.Decimal(15, 2)
|
||||
|
||||
createdById String
|
||||
creator User @relation("TourCreator", fields: [createdById], references: [id])
|
||||
|
||||
participants TourParticipant[]
|
||||
legs Leg[]
|
||||
photos Photo[]
|
||||
}
|
||||
|
||||
model TourParticipant {
|
||||
tourId String
|
||||
userId String
|
||||
role ParticipantRole @default(MEMBER)
|
||||
|
||||
tour Tour @relation(fields: [tourId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([tourId, userId])
|
||||
}
|
||||
|
||||
model Leg {
|
||||
id String @id @default(uuid())
|
||||
tourId String
|
||||
sequence Int
|
||||
note String? @db.Text
|
||||
|
||||
tour Tour @relation(fields: [tourId], references: [id], onDelete: Cascade)
|
||||
locations Location[]
|
||||
expenses Expense[]
|
||||
}
|
||||
|
||||
model Location {
|
||||
id String @id @default(uuid())
|
||||
legId String
|
||||
name String
|
||||
address String?
|
||||
latitude Float
|
||||
longitude Float
|
||||
|
||||
plannedStart DateTime?
|
||||
plannedEnd DateTime?
|
||||
actualStart DateTime?
|
||||
actualEnd DateTime?
|
||||
|
||||
status LocationStatus @default(PENDING)
|
||||
type LocationType @default(VISIT)
|
||||
|
||||
leg Leg @relation(fields: [legId], references: [id], onDelete: Cascade)
|
||||
expenses Expense[]
|
||||
photos Photo[]
|
||||
}
|
||||
|
||||
model Expense {
|
||||
id String @id @default(uuid())
|
||||
legId String
|
||||
locationId String?
|
||||
category ExpenseCategory
|
||||
amount Decimal @db.Decimal(15, 2)
|
||||
currency String @default("VND")
|
||||
description String? @db.Text
|
||||
|
||||
leg Leg @relation(fields: [legId], references: [id], onDelete: Cascade)
|
||||
location Location? @relation(fields: [locationId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Photo {
|
||||
id String @id @default(uuid())
|
||||
tourId String
|
||||
locationId String?
|
||||
uploaderId String
|
||||
imageUrl String
|
||||
capturedAt DateTime @default(now())
|
||||
metadata Json?
|
||||
privacy PrivacyLevel @default(TOUR_ONLY)
|
||||
|
||||
tour Tour @relation(fields: [tourId], references: [id], onDelete: Cascade)
|
||||
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
|
||||
uploader User @relation(fields: [uploaderId], references: [id])
|
||||
}
|
||||
+9
-9
@@ -9,11 +9,11 @@ export class TourRoleGuard implements CanActivate {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user; // Giả sử đã qua AuthGuard (Passport/JWT)
|
||||
|
||||
// Lấy tourId từ URL params (:id hoặc :tourId)
|
||||
const tourId = parseInt(request.params.id || request.params.tourId);
|
||||
// UUID không cần parseInt
|
||||
const tourId = request.params.id || request.params.tourId;
|
||||
const path = request.url;
|
||||
|
||||
if (!user || isNaN(tourId)) {
|
||||
if (!user || !tourId) {
|
||||
throw new ForbiddenException("Thông tin xác thực hoặc mã Tour không hợp lệ.");
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export class TourRoleGuard implements CanActivate {
|
||||
* Chúng ta lưu kết quả vào request object để các interceptor hoặc controller
|
||||
* sau này có thể dùng lại mà không cần query lại.
|
||||
*/
|
||||
const membership = await this.prisma.tourMember.findUnique({
|
||||
const participation = await this.prisma.tourParticipant.findUnique({
|
||||
where: {
|
||||
tourId_userId: {
|
||||
tourId: tourId,
|
||||
@@ -31,20 +31,20 @@ export class TourRoleGuard implements CanActivate {
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
if (!participation) {
|
||||
throw new ForbiddenException("Bạn không phải là thành viên của tour này.");
|
||||
}
|
||||
|
||||
// Gắn thông tin vào request để sử dụng ở tầng Controller
|
||||
request.tourMembership = membership;
|
||||
request.tourParticipation = participation;
|
||||
|
||||
const role = membership.role;
|
||||
const role = participation.role;
|
||||
const isPlanPath = path.includes('/plans');
|
||||
const isExpensePath = path.includes('/expenses');
|
||||
|
||||
// 1. Chặn MEMBER_PHOTO_ONLY và VIEWER_EXTERNAL truy cập Plans & Expenses
|
||||
// Theo định nghĩa mới: MEMBER_NO_FINANCE và VIEWER_ONLY bị hạn chế
|
||||
if (
|
||||
(role === 'MEMBER_PHOTO_ONLY' || role === 'VIEWER_EXTERNAL') &&
|
||||
(role === 'MEMBER_NO_FINANCE' || role === 'VIEWER_ONLY') &&
|
||||
(isPlanPath || isExpensePath)
|
||||
) {
|
||||
throw new ForbiddenException("Bạn không có quyền xem kế hoạch và chi phí của tour này.");
|
||||
|
||||
-154
@@ -1,154 +0,0 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
enum MemberRole {
|
||||
OWNER
|
||||
EDITOR
|
||||
MEMBER_PLAN_ONLY
|
||||
MEMBER_PHOTO_ONLY
|
||||
VIEWER_EXTERNAL
|
||||
}
|
||||
|
||||
enum ExpenseCategory {
|
||||
LODGING
|
||||
DINING
|
||||
TRANSPORT
|
||||
OTHER
|
||||
}
|
||||
|
||||
enum TriggerType {
|
||||
AUTO_BY_TIME
|
||||
MANUAL_BY_USER
|
||||
}
|
||||
|
||||
enum PrivacyLevel {
|
||||
PUBLIC_IN_TOUR
|
||||
PRIVATE_OWNER
|
||||
}
|
||||
|
||||
// --- Models ---
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
passwordHash String
|
||||
name String?
|
||||
avatar String?
|
||||
createdAt DateTime @default(now())
|
||||
isAdmin Boolean @default(false)
|
||||
isBlocked Boolean @default(false)
|
||||
|
||||
createdTours Tour[] @relation("TourCreator")
|
||||
tourMemberships TourMember[]
|
||||
uploadedPhotos Photo[]
|
||||
}
|
||||
|
||||
model Tour {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
startDate DateTime?
|
||||
endDate DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
creatorId Int
|
||||
creator User @relation("TourCreator", fields: [creatorId], references: [id])
|
||||
|
||||
members TourMember[]
|
||||
legs Leg[]
|
||||
tasks Task[]
|
||||
photos Photo[]
|
||||
}
|
||||
|
||||
model TourMember {
|
||||
tourId Int
|
||||
userId Int
|
||||
role MemberRole @default(MEMBER_PLAN_ONLY)
|
||||
|
||||
tour Tour @relation(fields: [tourId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([tourId, userId])
|
||||
}
|
||||
|
||||
model Leg {
|
||||
id Int @id @default(autoincrement())
|
||||
tourId Int
|
||||
sequenceNumber Int
|
||||
notes String? @db.Text
|
||||
|
||||
tour Tour @relation(fields: [tourId], references: [id], onDelete: Cascade)
|
||||
places Place[]
|
||||
expenses Expense[]
|
||||
tasks Task[]
|
||||
}
|
||||
|
||||
model Place {
|
||||
id Int @id @default(autoincrement())
|
||||
legId Int
|
||||
name String
|
||||
address String?
|
||||
|
||||
// Sử dụng Float cho Lat/Lng để dễ thao tác ở Frontend.
|
||||
// Nếu bạn muốn dùng PostGIS chuyên sâu, có thể dùng Unsupported("geography(Point, 4326)")
|
||||
latitude Float
|
||||
longitude Float
|
||||
|
||||
sequenceInLeg Int
|
||||
arrivalTime DateTime?
|
||||
departureTime DateTime?
|
||||
|
||||
leg Leg @relation(fields: [legId], references: [id], onDelete: Cascade)
|
||||
expenses Expense[]
|
||||
photos Photo[]
|
||||
}
|
||||
|
||||
model Expense {
|
||||
id Int @id @default(autoincrement())
|
||||
legId Int
|
||||
placeId Int?
|
||||
category ExpenseCategory
|
||||
amount Decimal @db.Decimal(15, 2)
|
||||
currency String @default("VND")
|
||||
description String? @db.Text
|
||||
|
||||
leg Leg @relation(fields: [legId], references: [id], onDelete: Cascade)
|
||||
place Place? @relation(fields: [placeId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Task {
|
||||
id Int @id @default(autoincrement())
|
||||
tourId Int
|
||||
legId Int?
|
||||
title String
|
||||
plannedTimestamp DateTime
|
||||
isCompleted Boolean @default(false)
|
||||
completedAt DateTime?
|
||||
triggerType TriggerType @default(MANUAL_BY_USER)
|
||||
|
||||
tour Tour @relation(fields: [tourId], references: [id], onDelete: Cascade)
|
||||
leg Leg? @relation(fields: [legId], references: [id])
|
||||
}
|
||||
|
||||
model Photo {
|
||||
id Int @id @default(autoincrement())
|
||||
tourId Int
|
||||
placeId Int?
|
||||
uploaderId Int
|
||||
imageUrl String
|
||||
capturedAt DateTime @default(now())
|
||||
privacyLevel PrivacyLevel @default(PUBLIC_IN_TOUR)
|
||||
|
||||
tour Tour @relation(fields: [tourId], references: [id], onDelete: Cascade)
|
||||
place Place? @relation(fields: [placeId], references: [id], onDelete: SetNull)
|
||||
uploader User @relation(fields: [uploaderId], references: [id])
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import { Pool } from 'pg';
|
||||
import 'dotenv/config';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
const adapter = new PrismaPg(pool);
|
||||
@@ -20,7 +21,9 @@ async function main() {
|
||||
data: {
|
||||
email: 'owner@travel.com',
|
||||
name: 'Lộc Phạm (Chủ Tour)',
|
||||
passwordHash: 'hashed_password_123',
|
||||
// Hash mật khẩu '123456' để có thể đăng nhập thực tế
|
||||
passwordHash: await bcrypt.hash('123456', 10),
|
||||
isAdmin: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -28,7 +31,7 @@ async function main() {
|
||||
data: {
|
||||
email: 'photomember@travel.com',
|
||||
name: 'Nguyễn Văn Ảnh (Chỉ xem ảnh)',
|
||||
passwordHash: 'hashed_password_456',
|
||||
passwordHash: await bcrypt.hash('123456', 10),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -38,11 +41,11 @@ async function main() {
|
||||
title: 'Hành trình khám phá TP.HCM',
|
||||
startDate: new Date('2023-11-20'),
|
||||
endDate: new Date('2023-11-21'),
|
||||
creatorId: owner.id,
|
||||
members: {
|
||||
createdById: owner.id,
|
||||
participants: {
|
||||
create: [
|
||||
{ userId: owner.id, role: 'OWNER' },
|
||||
{ userId: photoMember.id, role: 'MEMBER_PHOTO_ONLY' },
|
||||
{ userId: photoMember.id, role: 'VIEWER_ONLY' },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -52,25 +55,23 @@ async function main() {
|
||||
const leg1 = await prisma.leg.create({
|
||||
data: {
|
||||
tourId: tour.id,
|
||||
sequenceNumber: 1,
|
||||
notes: 'Khám phá lịch sử trung tâm',
|
||||
places: {
|
||||
sequence: 1,
|
||||
note: 'Khám phá lịch sử trung tâm',
|
||||
locations: {
|
||||
create: [
|
||||
{
|
||||
name: 'Dinh Độc Lập',
|
||||
address: '135 Nam Kỳ Khởi Nghĩa, Quận 1',
|
||||
latitude: 10.777,
|
||||
longitude: 106.695,
|
||||
sequenceInLeg: 1,
|
||||
arrivalTime: new Date('2023-11-20T08:00:00Z'),
|
||||
plannedStart: new Date('2023-11-20T08:00:00Z'),
|
||||
},
|
||||
{
|
||||
name: 'Bưu điện Thành phố',
|
||||
address: '02 Công xã Paris, Quận 1',
|
||||
latitude: 10.779,
|
||||
longitude: 106.699,
|
||||
sequenceInLeg: 2,
|
||||
arrivalTime: new Date('2023-11-20T10:00:00Z'),
|
||||
plannedStart: new Date('2023-11-20T10:00:00Z'),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -81,7 +82,7 @@ async function main() {
|
||||
await prisma.expense.create({
|
||||
data: {
|
||||
legId: leg1.id,
|
||||
category: 'DINING',
|
||||
category: 'FOOD',
|
||||
amount: 500000,
|
||||
currency: 'VND',
|
||||
description: 'Ăn trưa đặc sản Quận 1',
|
||||
|
||||
+8
-7
@@ -8,7 +8,7 @@ interface TourState {
|
||||
setTour: (tour: any) => void;
|
||||
updateLegs: (legs: any[]) => void;
|
||||
optimizeRouting: () => Promise<void>;
|
||||
fetchTour: (id: number) => Promise<void>;
|
||||
fetchTour: (id: string) => Promise<void>;
|
||||
fetchPublicTours: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -19,17 +19,18 @@ export const useTourStore = create<TourState>((set, get) => ({
|
||||
userRole: null,
|
||||
setTour: (tour) => set({ currentTour: tour }),
|
||||
updateLegs: (legs) => set({ legs }),
|
||||
fetchTour: async (id: number) => {
|
||||
// Giả sử user hiện tại có ID là 1 (Lộc Phạm - OWNER) để test
|
||||
const currentUserId = 1;
|
||||
const response = await fetch(`http://localhost:3001/api/v1/tours/${id}`);
|
||||
fetchTour: async (id: string) => {
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`);
|
||||
const data = await response.json();
|
||||
|
||||
const role = data.members?.find((m: any) => m.userId === currentUserId)?.role || 'VIEWER_EXTERNAL';
|
||||
// Lấy role từ danh sách participants (dữ liệu mẫu từ seed)
|
||||
const role = data.participants?.[0]?.role || 'VIEWER_ONLY';
|
||||
set({ currentTour: data, legs: data.legs || [], userRole: role });
|
||||
},
|
||||
fetchPublicTours: async () => {
|
||||
const response = await fetch(`http://localhost:3001/api/v1/tours/explore`);
|
||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
||||
const response = await fetch(`${API_BASE}/api/v1/tours/explore`);
|
||||
const data = await response.json();
|
||||
set({ publicTours: data });
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user