Sửa đổi dữ liệu của hệ thống theo ARCHITECTURE.md

This commit is contained in:
2026-06-13 19:52:08 +07:00
parent 7fc80a49d0
commit b54707823c
33 changed files with 655 additions and 275 deletions
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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 = [
+2 -2
View File
@@ -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`;
+3 -3
View File
@@ -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">
+1 -1
View File
@@ -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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"}
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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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"}
+13 -3
View File
@@ -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 || '') },
+1 -1
View File
@@ -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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"}
+1 -1
View File
@@ -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;
+1
View File
@@ -1 +1,2 @@
import 'reflect-metadata';
import 'dotenv/config';
+14 -13
View File
@@ -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([
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -4,5 +4,6 @@ export default defineConfig({
datasource: {
url: process.env.DATABASE_URL,
},
schema: 'prisma/schema.prisma',
});
//# sourceMappingURL=prisma.config.js.map
+1 -1
View File
@@ -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"}
+7 -7
View File
@@ -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.");
}
+1 -1
View File
@@ -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"}
+13 -13
View File
@@ -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',
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../seed.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../seed.ts"],"names":[],"mappings":"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"}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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>>;
+5 -4
View File
@@ -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 });
},
+1 -1
View File
@@ -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"}
+11 -9
View File
@@ -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({
+211
View File
@@ -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;
+1
View File
@@ -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',
});
+142
View File
@@ -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;
+150
View File
@@ -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
View File
@@ -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
View File
@@ -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])
}
+14 -13
View File
@@ -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
View File
@@ -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 });
},