Bổ sung tính năng cập nhật tiền của địa điểm bị lỗi trùng cổng

This commit is contained in:
2026-06-14 12:24:53 +07:00
parent 58087a4b37
commit 65a9b39966
11 changed files with 115 additions and 47 deletions
+16 -12
View File
@@ -180,6 +180,8 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
updateTourEndPoint, initializeLegs, addLocation updateTourEndPoint, initializeLegs, addLocation
} = useTourStore(); } = useTourStore();
const canEdit = ['OWNER', 'MANAGER'].includes(userRole || '');
const [mapZoom] = useState(initialViewState?.zoom || 13); const [mapZoom] = useState(initialViewState?.zoom || 13);
// Memoize danh sách tọa độ để MapTourBounds không bị trigger thừa // Memoize danh sách tọa độ để MapTourBounds không bị trigger thừa
@@ -517,7 +519,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
preferCanvas={true} preferCanvas={true}
> >
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<MapContextMenu onAction={handleMapAction} /> {canEdit && <MapContextMenu onAction={handleMapAction} />}
{/* Tự động đóng khung Start và End khi dữ liệu thay đổi */} {/* Tự động đóng khung Start và End khi dữ liệu thay đổi */}
<MapTourBounds locations={allLocations} /> <MapTourBounds locations={allLocations} />
@@ -590,17 +592,19 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
</div> </div>
{/* Floating Action Button (Mobile) */} {/* Floating Action Button (Mobile) */}
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40"> {canEdit && (
<button <div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40">
onClick={() => { <button
setTargetLegId(null); // Reset khi nhấn nút floating tổng quát onClick={() => {
setEditingLocation(null); setTargetLegId(null); // Reset khi nhấn nút floating tổng quát
if (activeTab === 'plan') setIsAddLocationOpen(true); setEditingLocation(null);
}} if (activeTab === 'plan') setIsAddLocationOpen(true);
className="bg-blue-600 text-white px-6 py-3 rounded-full shadow-lg hover:bg-blue-700 transition-all flex items-center font-bold"> }}
{activeTab === 'plan' ? 'Thêm địa điểm' : activeTab === 'expense' ? 'Thêm chi phí' : 'Đăng ảnh'} className="bg-blue-600 text-white px-6 py-3 rounded-full shadow-lg hover:bg-blue-700 transition-all flex items-center font-bold">
</button> {activeTab === 'plan' ? 'Thêm địa điểm' : activeTab === 'expense' ? 'Thêm chi phí' : 'Đăng ảnh'}
</div> </button>
</div>
)}
{/* Add Location Modal */} {/* Add Location Modal */}
{currentTour && ( {currentTour && (
+4 -3
View File
@@ -108,6 +108,7 @@ export const TourDetailPage = ({ onBack }) => {
return null; return null;
}); });
const { currentTour, legs, fetchTour, fetchPublicTours, publicTours, userRole, mapCenter, setMapCenter, updateTourStartPoint, updateTourEndPoint, initializeLegs, addLocation } = useTourStore(); const { currentTour, legs, fetchTour, fetchPublicTours, publicTours, userRole, mapCenter, setMapCenter, updateTourStartPoint, updateTourEndPoint, initializeLegs, addLocation } = useTourStore();
const canEdit = ['OWNER', 'MANAGER'].includes(userRole || '');
const [mapZoom] = useState(initialViewState?.zoom || 13); const [mapZoom] = useState(initialViewState?.zoom || 13);
const allLocations = useMemo(() => legs.flatMap(l => l.locations), [legs]); const allLocations = useMemo(() => legs.flatMap(l => l.locations), [legs]);
useEffect(() => { useEffect(() => {
@@ -246,16 +247,16 @@ export const TourDetailPage = ({ onBack }) => {
setTargetLegId(loc.legId); setTargetLegId(loc.legId);
setMapCenter([loc.latitude, loc.longitude]); setMapCenter([loc.latitude, loc.longitude]);
setIsAddLocationOpen(true); setIsAddLocationOpen(true);
} })) : (_jsxs("div", { className: "h-[60vh] w-full rounded-3xl overflow-hidden shadow-xl border-4 border-white relative", children: [_jsxs(MapContainer, { center: initialViewState?.center || mapCenter, zoom: mapZoom, className: "h-full w-full", preferCanvas: true, children: [_jsx(TileLayer, { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" }), _jsx(MapContextMenu, { onAction: handleMapAction }), _jsx(MapTourBounds, { locations: allLocations }), allLocations.length > 1 && (_jsx(Polyline, { positions: allLocations.map(l => [l.latitude, l.longitude]), color: "#3b82f6", weight: 3, dashArray: "5, 10", smoothFactor: 1.5 })), _jsx(MarkerClusterGroup, { chunkedLoading: true, children: legs.flatMap(l => l.locations).map((loc) => { } })) : (_jsxs("div", { className: "h-[60vh] w-full rounded-3xl overflow-hidden shadow-xl border-4 border-white relative", children: [_jsxs(MapContainer, { center: initialViewState?.center || mapCenter, zoom: mapZoom, className: "h-full w-full", preferCanvas: true, children: [_jsx(TileLayer, { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" }), canEdit && _jsx(MapContextMenu, { onAction: handleMapAction }), _jsx(MapTourBounds, { locations: allLocations }), allLocations.length > 1 && (_jsx(Polyline, { positions: allLocations.map(l => [l.latitude, l.longitude]), color: "#3b82f6", weight: 3, dashArray: "5, 10", smoothFactor: 1.5 })), _jsx(MarkerClusterGroup, { chunkedLoading: true, children: legs.flatMap(l => l.locations).map((loc) => {
const isStart = startPoint?.id === loc.id; const isStart = startPoint?.id === loc.id;
const isEnd = endPoint?.id === loc.id; const isEnd = endPoint?.id === loc.id;
const icon = isStart ? START_ICON : isEnd ? END_ICON : VISIT_ICON; const icon = isStart ? START_ICON : isEnd ? END_ICON : VISIT_ICON;
return (_jsx(Marker, { position: [loc.latitude, loc.longitude], icon: icon, children: _jsxs(Popup, { children: [_jsx("div", { className: "font-bold", children: loc.name }), _jsx("div", { className: "text-xs text-gray-500", children: loc.type })] }) }, loc.id)); return (_jsx(Marker, { position: [loc.latitude, loc.longitude], icon: icon, children: _jsxs(Popup, { children: [_jsx("div", { className: "font-bold", children: loc.name }), _jsx("div", { className: "text-xs text-gray-500", children: loc.type })] }) }, loc.id));
}) })] }), _jsx("div", { className: "absolute top-4 left-4 z-[1000] bg-white/90 backdrop-blur-md p-3 rounded-2xl text-[10px] font-bold text-gray-500 shadow-lg border border-white", children: "M\u1EB9o: Nh\u1EA5n gi\u1EEF (Mobile) ho\u1EB7c Chu\u1ED9t ph\u1EA3i \u0111\u1EC3 ghim \u0111\u1ECBa \u0111i\u1EC3m" })] }))] })), activeTab === 'expense' && (_jsx("div", { className: "animate-in fade-in slide-in-from-bottom-4", children: _jsx(ExpenseManager, {}) })), activeTab === 'photo' && (_jsx("div", { className: "grid grid-cols-3 gap-1.5 animate-in fade-in", children: [1, 2, 3, 4, 5, 6].map((i) => (_jsxs("div", { className: "aspect-square bg-gray-200 rounded-xl overflow-hidden relative group border border-white", children: [_jsx("div", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" }), _jsx("img", { src: `https://picsum.photos/seed/${i + 10}/400/400`, alt: "Tour photo", className: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" })] }, i))) })), activeTab === 'settings' && (_jsxs("div", { className: "p-8 text-center bg-white rounded-3xl border border-dashed border-gray-200 animate-in zoom-in-95", children: [_jsx(Settings, { className: "w-12 h-12 text-gray-300 mx-auto mb-4" }), _jsx("p", { className: "text-gray-500 font-medium", children: "T\u00EDnh n\u0103ng qu\u1EA3n l\u00FD th\u00E0nh vi\u00EAn \u0111ang \u0111\u01B0\u1EE3c c\u1EADp nh\u1EADt..." })] }))] })] }), _jsx("div", { className: "fixed bottom-6 left-1/2 -translate-x-1/2 z-40", children: _jsx("button", { onClick: () => { }) })] }), _jsx("div", { className: "absolute top-4 left-4 z-[1000] bg-white/90 backdrop-blur-md p-3 rounded-2xl text-[10px] font-bold text-gray-500 shadow-lg border border-white", children: "M\u1EB9o: Nh\u1EA5n gi\u1EEF (Mobile) ho\u1EB7c Chu\u1ED9t ph\u1EA3i \u0111\u1EC3 ghim \u0111\u1ECBa \u0111i\u1EC3m" })] }))] })), activeTab === 'expense' && (_jsx("div", { className: "animate-in fade-in slide-in-from-bottom-4", children: _jsx(ExpenseManager, {}) })), activeTab === 'photo' && (_jsx("div", { className: "grid grid-cols-3 gap-1.5 animate-in fade-in", children: [1, 2, 3, 4, 5, 6].map((i) => (_jsxs("div", { className: "aspect-square bg-gray-200 rounded-xl overflow-hidden relative group border border-white", children: [_jsx("div", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" }), _jsx("img", { src: `https://picsum.photos/seed/${i + 10}/400/400`, alt: "Tour photo", className: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" })] }, i))) })), activeTab === 'settings' && (_jsxs("div", { className: "p-8 text-center bg-white rounded-3xl border border-dashed border-gray-200 animate-in zoom-in-95", children: [_jsx(Settings, { className: "w-12 h-12 text-gray-300 mx-auto mb-4" }), _jsx("p", { className: "text-gray-500 font-medium", children: "T\u00EDnh n\u0103ng qu\u1EA3n l\u00FD th\u00E0nh vi\u00EAn \u0111ang \u0111\u01B0\u1EE3c c\u1EADp nh\u1EADt..." })] }))] })] }), canEdit && (_jsx("div", { className: "fixed bottom-6 left-1/2 -translate-x-1/2 z-40", children: _jsx("button", { onClick: () => {
setTargetLegId(null); setTargetLegId(null);
setEditingLocation(null); setEditingLocation(null);
if (activeTab === 'plan') if (activeTab === 'plan')
setIsAddLocationOpen(true); setIsAddLocationOpen(true);
}, className: "bg-blue-600 text-white px-6 py-3 rounded-full shadow-lg hover:bg-blue-700 transition-all flex items-center font-bold", children: activeTab === 'plan' ? 'Thêm địa điểm' : activeTab === 'expense' ? 'Thêm chi phí' : 'Đăng ảnh' }) }), currentTour && (_jsx(AddLocationModal, { isOpen: isAddLocationOpen, onClose: () => setIsAddLocationOpen(false), initialLegId: targetLegId || undefined, editingLocation: editingLocation, tourId: currentTour.id }))] })); }, className: "bg-blue-600 text-white px-6 py-3 rounded-full shadow-lg hover:bg-blue-700 transition-all flex items-center font-bold", children: activeTab === 'plan' ? 'Thêm địa điểm' : activeTab === 'expense' ? 'Thêm chi phí' : 'Đăng ảnh' }) })), currentTour && (_jsx(AddLocationModal, { isOpen: isAddLocationOpen, onClose: () => setIsAddLocationOpen(false), initialLegId: targetLegId || undefined, editingLocation: editingLocation, tourId: currentTour.id }))] }));
}; };
//# sourceMappingURL=TourDetailPage.js.map //# sourceMappingURL=TourDetailPage.js.map
+1 -1
View File
File diff suppressed because one or more lines are too long
+19 -10
View File
@@ -20,6 +20,7 @@ import { AdminGuard } from './admin.guard.js';
import { JwtModule, JwtService } from '@nestjs/jwt'; import { JwtModule, JwtService } from '@nestjs/jwt';
import { JwtAuthGuard } from './jwt-auth.guard.js'; import { JwtAuthGuard } from './jwt-auth.guard.js';
import { JwtStrategy } from './jwt.strategy.js'; import { JwtStrategy } from './jwt.strategy.js';
import { TourRoleGuard } from './rbac.middleware.js';
let AppController = class AppController { let AppController = class AppController {
getHello() { getHello() {
return 'Travel Planning API is running!'; return 'Travel Planning API is running!';
@@ -279,8 +280,13 @@ let TourController = class TourController {
}); });
return { success: true }; return { success: true };
} }
async getPublicTours() { async getPublicTours(req) {
return this.prisma.tour.findMany({ return this.prisma.tour.findMany({
where: {
participants: {
some: { userId: req.user.id }
}
},
take: 20, take: 20,
include: { include: {
photos: { take: 1 }, photos: { take: 1 },
@@ -323,7 +329,7 @@ __decorate([
__metadata("design:returntype", Promise) __metadata("design:returntype", Promise)
], TourController.prototype, "createTour", null); ], TourController.prototype, "createTour", null);
__decorate([ __decorate([
UseGuards(JwtAuthGuard), UseGuards(JwtAuthGuard, TourRoleGuard),
Post(':tourId/locations'), Post(':tourId/locations'),
__param(0, Param('tourId', ParseUUIDPipe)), __param(0, Param('tourId', ParseUUIDPipe)),
__param(1, Body()), __param(1, Body()),
@@ -333,7 +339,7 @@ __decorate([
__metadata("design:returntype", Promise) __metadata("design:returntype", Promise)
], TourController.prototype, "addLocation", null); ], TourController.prototype, "addLocation", null);
__decorate([ __decorate([
UseGuards(JwtAuthGuard), UseGuards(JwtAuthGuard, TourRoleGuard),
Post(':tourId/start-point'), Post(':tourId/start-point'),
__param(0, Param('tourId', ParseUUIDPipe)), __param(0, Param('tourId', ParseUUIDPipe)),
__param(1, Body()), __param(1, Body()),
@@ -343,7 +349,7 @@ __decorate([
__metadata("design:returntype", Promise) __metadata("design:returntype", Promise)
], TourController.prototype, "updateTourStartPoint", null); ], TourController.prototype, "updateTourStartPoint", null);
__decorate([ __decorate([
UseGuards(JwtAuthGuard), UseGuards(JwtAuthGuard, TourRoleGuard),
Post(':tourId/end-point'), Post(':tourId/end-point'),
__param(0, Param('tourId', ParseUUIDPipe)), __param(0, Param('tourId', ParseUUIDPipe)),
__param(1, Body()), __param(1, Body()),
@@ -353,7 +359,7 @@ __decorate([
__metadata("design:returntype", Promise) __metadata("design:returntype", Promise)
], TourController.prototype, "updateTourEndPoint", null); ], TourController.prototype, "updateTourEndPoint", null);
__decorate([ __decorate([
UseGuards(JwtAuthGuard), UseGuards(JwtAuthGuard, TourRoleGuard),
Post(':tourId/legs/batch'), Post(':tourId/legs/batch'),
__param(0, Param('tourId', ParseUUIDPipe)), __param(0, Param('tourId', ParseUUIDPipe)),
__param(1, Body()), __param(1, Body()),
@@ -362,7 +368,7 @@ __decorate([
__metadata("design:returntype", Promise) __metadata("design:returntype", Promise)
], TourController.prototype, "initializeLegs", null); ], TourController.prototype, "initializeLegs", null);
__decorate([ __decorate([
UseGuards(JwtAuthGuard), UseGuards(JwtAuthGuard, TourRoleGuard),
Post(':tourId/legs'), Post(':tourId/legs'),
__param(0, Param('tourId', ParseUUIDPipe)), __param(0, Param('tourId', ParseUUIDPipe)),
__param(1, Body()), __param(1, Body()),
@@ -371,7 +377,7 @@ __decorate([
__metadata("design:returntype", Promise) __metadata("design:returntype", Promise)
], TourController.prototype, "addLeg", null); ], TourController.prototype, "addLeg", null);
__decorate([ __decorate([
UseGuards(JwtAuthGuard), UseGuards(JwtAuthGuard, TourRoleGuard),
Patch(':id'), Patch(':id'),
__param(0, Param('id', ParseUUIDPipe)), __param(0, Param('id', ParseUUIDPipe)),
__param(1, Body()), __param(1, Body()),
@@ -380,7 +386,7 @@ __decorate([
__metadata("design:returntype", Promise) __metadata("design:returntype", Promise)
], TourController.prototype, "updateTour", null); ], TourController.prototype, "updateTour", null);
__decorate([ __decorate([
UseGuards(JwtAuthGuard), UseGuards(JwtAuthGuard, TourRoleGuard),
Delete(':id'), Delete(':id'),
__param(0, Param('id', ParseUUIDPipe)), __param(0, Param('id', ParseUUIDPipe)),
__metadata("design:type", Function), __metadata("design:type", Function),
@@ -388,12 +394,15 @@ __decorate([
__metadata("design:returntype", Promise) __metadata("design:returntype", Promise)
], TourController.prototype, "deleteTour", null); ], TourController.prototype, "deleteTour", null);
__decorate([ __decorate([
UseGuards(JwtAuthGuard),
Get('explore'), Get('explore'),
__param(0, Req()),
__metadata("design:type", Function), __metadata("design:type", Function),
__metadata("design:paramtypes", []), __metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise) __metadata("design:returntype", Promise)
], TourController.prototype, "getPublicTours", null); ], TourController.prototype, "getPublicTours", null);
__decorate([ __decorate([
UseGuards(JwtAuthGuard, TourRoleGuard),
Get(':id'), Get(':id'),
__param(0, Param('id', ParseUUIDPipe)), __param(0, Param('id', ParseUUIDPipe)),
__metadata("design:type", Function), __metadata("design:type", Function),
@@ -703,7 +712,7 @@ AppModule = __decorate([
}), }),
], ],
controllers: [AppController, AuthController, TourController, UserController, RoutingController, LegController, LocationController], controllers: [AppController, AuthController, TourController, UserController, RoutingController, LegController, LocationController],
providers: [PrismaService, JwtStrategy], providers: [PrismaService, JwtStrategy, TourRoleGuard],
exports: [PrismaService] exports: [PrismaService]
}) })
], AppModule); ], AppModule);
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+27 -3
View File
@@ -12,9 +12,24 @@ export const useTourStore = create((set, get) => ({
setMapCenter: (pos) => set({ mapCenter: pos }), setMapCenter: (pos) => set({ mapCenter: pos }),
fetchTour: async (id) => { fetchTour: async (id) => {
const API_BASE = `http://${window.location.hostname}:3001`; const API_BASE = `http://${window.location.hostname}:3001`;
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`); const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json(); const data = await response.json();
const role = data.participants?.[0]?.role || 'VIEWER_ONLY'; let role = 'VIEWER_ONLY';
if (token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const currentUserId = payload.sub;
const participant = data.participants?.find((p) => p.userId === currentUserId);
if (participant)
role = participant.role;
}
catch (e) {
console.error("Lỗi khi xác định vai trò người dùng:", e);
}
}
const legs = data.legs || []; const legs = data.legs || [];
set({ set({
currentTour: data, currentTour: data,
@@ -25,7 +40,16 @@ export const useTourStore = create((set, get) => ({
}, },
fetchPublicTours: async () => { fetchPublicTours: async () => {
const API_BASE = `http://${window.location.hostname}:3001`; const API_BASE = `http://${window.location.hostname}:3001`;
const response = await fetch(`${API_BASE}/api/v1/tours/explore`); const token = localStorage.getItem('token');
if (!token)
return;
const response = await fetch(`${API_BASE}/api/v1/tours/explore`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok)
return;
const data = await response.json(); const data = await response.json();
set({ publicTours: data }); set({ publicTours: data });
}, },
+1 -1
View File
File diff suppressed because one or more lines are too long
+18 -10
View File
@@ -8,6 +8,7 @@ import { AdminGuard } from './admin.guard.js';
import { JwtModule, JwtService } from '@nestjs/jwt'; import { JwtModule, JwtService } from '@nestjs/jwt';
import { JwtAuthGuard } from './jwt-auth.guard.js'; import { JwtAuthGuard } from './jwt-auth.guard.js';
import { JwtStrategy } from './jwt.strategy.js'; import { JwtStrategy } from './jwt.strategy.js';
import { TourRoleGuard } from './rbac.middleware.js';
@Controller() @Controller()
class AppController { class AppController {
@@ -108,7 +109,7 @@ class TourController {
}); });
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard, TourRoleGuard)
@Post(':tourId/locations') @Post(':tourId/locations')
async addLocation(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) { async addLocation(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
const legId = body.legId; const legId = body.legId;
@@ -149,7 +150,7 @@ class TourController {
}); });
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard, TourRoleGuard)
@Post(':tourId/start-point') @Post(':tourId/start-point')
async updateTourStartPoint(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) { async updateTourStartPoint(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
const { latitude, longitude, name } = body; const { latitude, longitude, name } = body;
@@ -186,7 +187,7 @@ class TourController {
}); });
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard, TourRoleGuard)
@Post(':tourId/end-point') @Post(':tourId/end-point')
async updateTourEndPoint(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) { async updateTourEndPoint(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any, @Req() req: any) {
const { latitude, longitude, name } = body; const { latitude, longitude, name } = body;
@@ -221,7 +222,7 @@ class TourController {
}); });
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard, TourRoleGuard)
@Post(':tourId/legs/batch') @Post(':tourId/legs/batch')
async initializeLegs(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: { count: number }) { async initializeLegs(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: { count: number }) {
const { count } = body; const { count } = body;
@@ -266,7 +267,7 @@ class TourController {
return allLegs; return allLegs;
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard, TourRoleGuard)
@Post(':tourId/legs') @Post(':tourId/legs')
async addLeg(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any) { async addLeg(@Param('tourId', ParseUUIDPipe) tourId: string, @Body() body: any) {
const tour = await this.prisma.tour.findUnique({ const tour = await this.prisma.tour.findUnique({
@@ -284,7 +285,7 @@ class TourController {
}); });
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard, TourRoleGuard)
@Patch(':id') @Patch(':id')
async updateTour(@Param('id', ParseUUIDPipe) id: string, @Body() body: any) { async updateTour(@Param('id', ParseUUIDPipe) id: string, @Body() body: any) {
// Lưu ý: Trong thực tế nên kiểm tra xem user có phải là OWNER không // Lưu ý: Trong thực tế nên kiểm tra xem user có phải là OWNER không
@@ -298,7 +299,7 @@ class TourController {
}); });
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard, TourRoleGuard)
@Delete(':id') @Delete(':id')
async deleteTour(@Param('id', ParseUUIDPipe) id: string) { async deleteTour(@Param('id', ParseUUIDPipe) id: string) {
// Xóa tour sẽ xóa cascade các Leg, Location, Expense nhờ config onDelete: Cascade trong schema // Xóa tour sẽ xóa cascade các Leg, Location, Expense nhờ config onDelete: Cascade trong schema
@@ -308,10 +309,16 @@ class TourController {
return { success: true }; return { success: true };
} }
@UseGuards(JwtAuthGuard)
@Get('explore') @Get('explore')
async getPublicTours() { async getPublicTours(@Req() req: any) {
// Lấy các tour có ít nhất 1 ảnh và thông tin vị trí từ chặng đầu tiên // Lc Tour: Chỉ lấy những tour mà người dùng hiện tại là thành viên (Participant)
return this.prisma.tour.findMany({ return this.prisma.tour.findMany({
where: {
participants: {
some: { userId: req.user.id }
}
},
take: 20, take: 20,
include: { include: {
photos: { take: 1 }, photos: { take: 1 },
@@ -325,6 +332,7 @@ class TourController {
}); });
} }
@UseGuards(JwtAuthGuard, TourRoleGuard)
@Get(':id') @Get(':id')
async getTourDetails(@Param('id', ParseUUIDPipe) id: string) { async getTourDetails(@Param('id', ParseUUIDPipe) id: string) {
const tour = await this.prisma.tour.findUnique({ const tour = await this.prisma.tour.findUnique({
@@ -618,7 +626,7 @@ class UserController {
}), }),
], ],
controllers: [AppController, AuthController, TourController, UserController, RoutingController, LegController, LocationController], controllers: [AppController, AuthController, TourController, UserController, RoutingController, LegController, LocationController],
providers: [PrismaService, JwtStrategy], providers: [PrismaService, JwtStrategy, TourRoleGuard],
exports: [PrismaService] exports: [PrismaService]
}) })
class AppModule {} class AppModule {}
+26 -4
View File
@@ -41,11 +41,24 @@ export const useTourStore = create<TourState>((set, get) => ({
setMapCenter: (pos) => set({ mapCenter: pos }), setMapCenter: (pos) => set({ mapCenter: pos }),
fetchTour: async (id: string) => { fetchTour: async (id: string) => {
const API_BASE = `http://${window.location.hostname}:3001`; const API_BASE = `http://${window.location.hostname}:3001`;
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`); const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json(); const data = await response.json();
// Lấy role từ danh sách participants (dữ liệu mẫu từ seed) // Xác định role của người dùng hiện tại dựa trên thông tin trong JWT
const role = data.participants?.[0]?.role || 'VIEWER_ONLY'; let role = 'VIEWER_ONLY';
if (token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const currentUserId = payload.sub;
const participant = data.participants?.find((p: any) => p.userId === currentUserId);
if (participant) role = participant.role;
} catch (e) {
console.error("Lỗi khi xác định vai trò người dùng:", e);
}
}
const legs = data.legs || []; const legs = data.legs || [];
set({ set({
@@ -57,7 +70,16 @@ export const useTourStore = create<TourState>((set, get) => ({
}, },
fetchPublicTours: async () => { fetchPublicTours: async () => {
const API_BASE = `http://${window.location.hostname}:3001`; const API_BASE = `http://${window.location.hostname}:3001`;
const response = await fetch(`${API_BASE}/api/v1/tours/explore`); const token = localStorage.getItem('token');
if (!token) return;
const response = await fetch(`${API_BASE}/api/v1/tours/explore`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) return;
const data = await response.json(); const data = await response.json();
set({ publicTours: data }); set({ publicTours: data });
}, },
+1 -1
View File
@@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react';
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
port: 3002, port: 3002, // Thay đổi cổng thành 5173 (mặc định của Vite) hoặc cổng khác bạn muốn
strictPort: true, strictPort: true,
host: true, host: true,
}, },