Sửa lỗi người nhận được liên kết chia sẻ không xem được nội dung Tour
This commit is contained in:
Vendored
+45
-36
@@ -157,6 +157,49 @@ AuthController = __decorate([
|
|||||||
(0, common_1.Controller)('auth'),
|
(0, common_1.Controller)('auth'),
|
||||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService, jwt_1.JwtService])
|
__metadata("design:paramtypes", [prisma_service_1.PrismaService, jwt_1.JwtService])
|
||||||
], AuthController);
|
], AuthController);
|
||||||
|
let PublicTourController = class PublicTourController {
|
||||||
|
constructor(prisma) {
|
||||||
|
this.prisma = prisma;
|
||||||
|
}
|
||||||
|
async getPublicTourDetails(id) {
|
||||||
|
const tour = await this.prisma.tour.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
participants: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: { id: true, name: true, email: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
photos: true,
|
||||||
|
legs: {
|
||||||
|
orderBy: { sequence: 'asc' },
|
||||||
|
include: {
|
||||||
|
locations: {
|
||||||
|
orderBy: { plannedStart: 'asc' },
|
||||||
|
include: { _count: { select: { comments: true } } }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!tour)
|
||||||
|
throw new common_1.NotFoundException(`Không tìm thấy Tour`);
|
||||||
|
return tour;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
__decorate([
|
||||||
|
(0, common_1.Get)(':id/public'),
|
||||||
|
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||||
|
__metadata("design:type", Function),
|
||||||
|
__metadata("design:paramtypes", [String]),
|
||||||
|
__metadata("design:returntype", Promise)
|
||||||
|
], PublicTourController.prototype, "getPublicTourDetails", null);
|
||||||
|
PublicTourController = __decorate([
|
||||||
|
(0, common_1.Controller)('tours'),
|
||||||
|
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||||
|
], PublicTourController);
|
||||||
let TourController = class TourController {
|
let TourController = class TourController {
|
||||||
constructor(prisma) {
|
constructor(prisma) {
|
||||||
this.prisma = prisma;
|
this.prisma = prisma;
|
||||||
@@ -374,33 +417,6 @@ let TourController = class TourController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async getPublicTourDetails(id) {
|
|
||||||
const tour = await this.prisma.tour.findUnique({
|
|
||||||
where: { id },
|
|
||||||
include: {
|
|
||||||
participants: {
|
|
||||||
include: {
|
|
||||||
user: {
|
|
||||||
select: { id: true, name: true, email: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
photos: true,
|
|
||||||
legs: {
|
|
||||||
orderBy: { sequence: 'asc' },
|
|
||||||
include: {
|
|
||||||
locations: {
|
|
||||||
orderBy: { plannedStart: 'asc' },
|
|
||||||
include: { _count: { select: { comments: true } } }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!tour)
|
|
||||||
throw new common_1.NotFoundException(`Không tìm thấy Tour`);
|
|
||||||
return tour;
|
|
||||||
}
|
|
||||||
async getTourDetails(id) {
|
async getTourDetails(id) {
|
||||||
const tour = await this.prisma.tour.findUnique({
|
const tour = await this.prisma.tour.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
@@ -677,13 +693,6 @@ __decorate([
|
|||||||
__metadata("design:paramtypes", [Object]),
|
__metadata("design:paramtypes", [Object]),
|
||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], TourController.prototype, "getPublicTours", null);
|
], TourController.prototype, "getPublicTours", null);
|
||||||
__decorate([
|
|
||||||
(0, common_1.Get)(':id/public'),
|
|
||||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
|
||||||
__metadata("design:type", Function),
|
|
||||||
__metadata("design:paramtypes", [String]),
|
|
||||||
__metadata("design:returntype", Promise)
|
|
||||||
], TourController.prototype, "getPublicTourDetails", null);
|
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, rbac_middleware_1.TourRoleGuard),
|
||||||
(0, common_1.Get)(':id'),
|
(0, common_1.Get)(':id'),
|
||||||
@@ -1131,6 +1140,7 @@ __decorate([
|
|||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], CommentController.prototype, "getComments", null);
|
], CommentController.prototype, "getComments", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
|
||||||
(0, common_1.Post)(':locationId/comments'),
|
(0, common_1.Post)(':locationId/comments'),
|
||||||
__param(0, (0, common_1.Param)('locationId', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('locationId', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Body)()),
|
__param(1, (0, common_1.Body)()),
|
||||||
@@ -1141,7 +1151,6 @@ __decorate([
|
|||||||
], CommentController.prototype, "addComment", null);
|
], CommentController.prototype, "addComment", null);
|
||||||
CommentController = __decorate([
|
CommentController = __decorate([
|
||||||
(0, common_1.Controller)('locations'),
|
(0, common_1.Controller)('locations'),
|
||||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
|
|
||||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
|
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
|
||||||
CommentGateway])
|
CommentGateway])
|
||||||
], CommentController);
|
], CommentController);
|
||||||
@@ -1155,7 +1164,7 @@ AppModule = __decorate([
|
|||||||
signOptions: { expiresIn: '1d' },
|
signOptions: { expiresIn: '1d' },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
controllers: [AppController, AuthController, TourController, UserController, RoutingController, LegController, LocationController, CommentController],
|
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController],
|
||||||
providers: [prisma_service_1.PrismaService, jwt_strategy_1.JwtStrategy, rbac_middleware_1.TourRoleGuard, jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard, CommentGateway],
|
providers: [prisma_service_1.PrismaService, jwt_strategy_1.JwtStrategy, rbac_middleware_1.TourRoleGuard, jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard, CommentGateway],
|
||||||
exports: [prisma_service_1.PrismaService]
|
exports: [prisma_service_1.PrismaService]
|
||||||
})
|
})
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+37
-31
@@ -103,6 +103,40 @@ class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Controller('tours') // Controller mới để xử lý các tour công khai
|
||||||
|
class PublicTourController {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
@Get(':id/public')
|
||||||
|
async getPublicTourDetails(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
const tour = await this.prisma.tour.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
participants: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: { id: true, name: true, email: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
photos: true,
|
||||||
|
legs: {
|
||||||
|
orderBy: { sequence: 'asc' },
|
||||||
|
include: {
|
||||||
|
locations: {
|
||||||
|
orderBy: { plannedStart: 'asc' },
|
||||||
|
include: { _count: { select: { comments: true } } }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tour) throw new NotFoundException(`Không tìm thấy Tour`);
|
||||||
|
return tour;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Controller('tours')
|
@Controller('tours')
|
||||||
class TourController {
|
class TourController {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
@@ -376,35 +410,6 @@ class TourController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/public')
|
|
||||||
async getPublicTourDetails(@Param('id', ParseUUIDPipe) id: string) {
|
|
||||||
const tour = await this.prisma.tour.findUnique({
|
|
||||||
where: { id },
|
|
||||||
include: {
|
|
||||||
participants: {
|
|
||||||
include: {
|
|
||||||
user: {
|
|
||||||
select: { id: true, name: true, email: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
photos: true,
|
|
||||||
legs: {
|
|
||||||
orderBy: { sequence: 'asc' },
|
|
||||||
include: {
|
|
||||||
locations: {
|
|
||||||
orderBy: { plannedStart: 'asc' },
|
|
||||||
include: { _count: { select: { comments: true } } }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tour) throw new NotFoundException(`Không tìm thấy Tour`);
|
|
||||||
return tour;
|
|
||||||
}
|
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async getTourDetails(@Param('id', ParseUUIDPipe) id: string) {
|
async getTourDetails(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
@@ -933,7 +938,6 @@ export class CommentGateway implements OnGatewayConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Controller('locations')
|
@Controller('locations')
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
class CommentController {
|
class CommentController {
|
||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
@@ -941,6 +945,7 @@ class CommentController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get(':locationId/comments')
|
@Get(':locationId/comments')
|
||||||
|
// Cho phép khách xem bình luận mà không cần đăng nhập
|
||||||
async getComments(@Param('locationId', ParseUUIDPipe) locationId: string) {
|
async getComments(@Param('locationId', ParseUUIDPipe) locationId: string) {
|
||||||
return this.prisma.comment.findMany({
|
return this.prisma.comment.findMany({
|
||||||
where: { locationId },
|
where: { locationId },
|
||||||
@@ -951,6 +956,7 @@ class CommentController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
@Post(':locationId/comments')
|
@Post(':locationId/comments')
|
||||||
async addComment(
|
async addComment(
|
||||||
@Param('locationId', ParseUUIDPipe) locationId: string,
|
@Param('locationId', ParseUUIDPipe) locationId: string,
|
||||||
@@ -990,7 +996,7 @@ class CommentController {
|
|||||||
signOptions: { expiresIn: '1d' },
|
signOptions: { expiresIn: '1d' },
|
||||||
}) as any,
|
}) as any,
|
||||||
],
|
],
|
||||||
controllers: [AppController, AuthController, TourController, UserController, RoutingController, LegController, LocationController, CommentController],
|
controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController],
|
||||||
providers: [PrismaService, JwtStrategy, TourRoleGuard, JwtAuthGuard, AdminGuard, CommentGateway],
|
providers: [PrismaService, JwtStrategy, TourRoleGuard, JwtAuthGuard, AdminGuard, CommentGateway],
|
||||||
exports: [PrismaService]
|
exports: [PrismaService]
|
||||||
})
|
})
|
||||||
|
|||||||
+84
-67
@@ -1,88 +1,105 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { LandingPage } from '@/pages/LandingPage';
|
import { LandingPage } from './pages/LandingPage';
|
||||||
import { TourDetailPage } from '@/pages/TourDetailPage';
|
import { ExploreMap } from './pages/ExploreMap';
|
||||||
import { ExploreMap } from '@/pages/ExploreMap';
|
import { TourDetailPage } from './pages/TourDetailPage';
|
||||||
import { SignupPage } from '@/pages/SignupPage';
|
import { SignupPage } from './pages/SignupPage';
|
||||||
import { useTourStore } from '@/store/useTourStore';
|
import { useTourStore } from './store/useTourStore';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const viewTourId = params.get('viewTour');
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
type View = 'landing' | 'explore' | 'detail' | 'signup';
|
|
||||||
const [view, setView] = useState<View>('landing');
|
|
||||||
const [isInitialSetup, setIsInitialSetup] = useState(false);
|
|
||||||
const [user, setUser] = useState<any>(null);
|
const [user, setUser] = useState<any>(null);
|
||||||
const [isUserLoaded, setIsUserLoaded] = useState(false); // Trạng thái để biết user đã được load từ localStorage chưa
|
const [currentPage, setCurrentPage] = useState<'landing' | 'explore' | 'tourDetail' | 'signup'>(viewTourId ? 'tourDetail' : 'landing');
|
||||||
|
const [currentTourId, setCurrentTourId] = useState<string | null>(viewTourId);
|
||||||
|
const [isPublicTourView, setIsPublicTourView] = useState(!!viewTourId);
|
||||||
|
|
||||||
|
// Lấy action từ store
|
||||||
const fetchTour = useTourStore(state => state.fetchTour);
|
const fetchTour = useTourStore(state => state.fetchTour);
|
||||||
|
const fetchPublicTourDetails = useTourStore(state => state.fetchPublicTourDetails);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Khôi phục phiên đăng nhập từ localStorage
|
const params = new URLSearchParams(window.location.search);
|
||||||
const savedUser = localStorage.getItem('user');
|
const viewTourId = params.get('viewTour');
|
||||||
if (savedUser) {
|
|
||||||
const parsedUser = JSON.parse(savedUser);
|
if (viewTourId) {
|
||||||
setUser(parsedUser);
|
// Không gọi replaceState ngay để tránh mất ID khi component re-render hoặc refresh
|
||||||
|
} else {
|
||||||
|
// Kiểm tra đăng nhập bình thường nếu không có tham số viewTour
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const storedUser = localStorage.getItem('user');
|
||||||
|
if (token && storedUser) {
|
||||||
|
try {
|
||||||
|
setUser(JSON.parse(storedUser));
|
||||||
|
setCurrentPage('explore'); // Chuyển đến bản đồ khám phá nếu đã đăng nhập
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Lỗi khi phân tích dữ liệu người dùng từ localStorage", e);
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
setCurrentPage('landing'); // Quay về trang Landing nếu dữ liệu lỗi
|
||||||
}
|
}
|
||||||
setIsUserLoaded(true); // Đánh dấu user đã được load
|
} else {
|
||||||
|
setCurrentPage('landing'); // Quay về trang Landing nếu chưa đăng nhập
|
||||||
// Kiểm tra xem hệ thống đã được cài đặt chưa
|
|
||||||
fetch(`/api/v1/auth/status`)
|
|
||||||
.then(res => res.ok ? res.json() : Promise.reject())
|
|
||||||
.then(data => setIsInitialSetup(!!data.isInitialSetup))
|
|
||||||
.catch(() => setIsInitialSetup(false));
|
|
||||||
}, []); // Chạy một lần khi component mount
|
|
||||||
|
|
||||||
// Effect để xử lý chuyển hướng nếu user đã đăng nhập và đang ở trang landing
|
|
||||||
useEffect(() => {
|
|
||||||
if (isUserLoaded && user && view === 'landing') {
|
|
||||||
setView('explore');
|
|
||||||
}
|
}
|
||||||
}, [isUserLoaded, user, view]);
|
}
|
||||||
|
}, []); // Chỉ chạy một lần khi component mount
|
||||||
|
|
||||||
const handleLoginSuccess = (userData: any) => {
|
const handleLoginSuccess = (loggedInUser: any) => {
|
||||||
setUser(userData);
|
setUser(loggedInUser);
|
||||||
|
setCurrentPage('explore');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('user');
|
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setView('landing');
|
setCurrentPage('landing');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewTour = (tourId: string) => {
|
||||||
|
setCurrentTourId(tourId);
|
||||||
|
setIsPublicTourView(false); // Không phải chế độ công khai nếu đến từ bản đồ khám phá
|
||||||
|
setCurrentPage('tourDetail');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackFromTourDetail = () => {
|
||||||
|
setCurrentTourId(null);
|
||||||
|
setIsPublicTourView(false);
|
||||||
|
// Quay về trang khám phá nếu đã đăng nhập, ngược lại quay về Landing
|
||||||
|
if (user) {
|
||||||
|
setCurrentPage('explore');
|
||||||
|
} else {
|
||||||
|
setCurrentPage('landing');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackFromSignup = () => {
|
||||||
|
setCurrentPage('landing');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignupSuccess = () => {
|
||||||
|
setCurrentPage('landing'); // Quay về trang Landing sau khi đăng ký thành công
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentPage === 'tourDetail') {
|
||||||
return (
|
return (
|
||||||
<div className="app-container">
|
<TourDetailPage
|
||||||
{view === 'landing' && (
|
tourId={currentTourId!} // tourId được đảm bảo không null ở đây
|
||||||
<LandingPage
|
onBack={handleBackFromTourDetail}
|
||||||
isInitialSetup={isInitialSetup}
|
isPublicView={isPublicTourView}
|
||||||
onContinue={() => setView('explore')}
|
|
||||||
onGoToSignup={() => setView('signup')}
|
|
||||||
onGoToMap={() => setView('explore')}
|
|
||||||
onLoginSuccess={handleLoginSuccess}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{view === 'signup' && (
|
|
||||||
<SignupPage
|
|
||||||
onBack={() => setView('landing')}
|
|
||||||
onSuccess={() => setView('landing')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{view === 'explore' && (
|
|
||||||
<ExploreMap
|
|
||||||
onBack={() => setView('landing')}
|
|
||||||
onLogout={user ? handleLogout : undefined}
|
|
||||||
user={user}
|
|
||||||
onViewTour={(id) => {
|
|
||||||
fetchTour(id);
|
|
||||||
setView('detail');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{view === 'detail' && (
|
|
||||||
<TourDetailPage onBack={() => setView('explore')} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
if (currentPage === 'explore') {
|
||||||
|
return <ExploreMap onBack={handleBackFromTourDetail} onLogout={handleLogout} user={user} onViewTour={handleViewTour} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage === 'signup') {
|
||||||
|
return <SignupPage onBack={handleBackFromSignup} onSuccess={handleSignupSuccess} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <LandingPage onContinue={() => setCurrentPage('explore')} onGoToSignup={() => setCurrentPage('signup')} onGoToMap={() => setCurrentPage('explore')} onLoginSuccess={handleLoginSuccess} isInitialSetup={false} />;
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@@ -26,8 +26,12 @@ export const CommentModal: React.FC<CommentModalProps> = ({ isOpen, onClose, loc
|
|||||||
const fetchComments = async () => {
|
const fetchComments = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
|
||||||
const res = await fetch(`/api/v1/locations/${locationId}/comments`, {
|
const res = await fetch(`/api/v1/locations/${locationId}/comments`, {
|
||||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
headers
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export const TourDetailPage = ({ onBack, tourId, isPublicView = false }: { onBac
|
|||||||
const canEdit = isPublicView ? false : ['OWNER', 'MANAGER'].includes(userRole || '');
|
const canEdit = isPublicView ? false : ['OWNER', 'MANAGER'].includes(userRole || '');
|
||||||
const canInvite = isPublicView ? false : ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
|
const canInvite = isPublicView ? false : ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
|
||||||
const isOwner = isPublicView ? false : userRole === 'OWNER';
|
const isOwner = isPublicView ? false : userRole === 'OWNER';
|
||||||
const canShare = ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
|
const canShare = isPublicView || ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPublicView) {
|
if (isPublicView) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface TourState {
|
|||||||
setActiveLegId: (id: string | null) => void;
|
setActiveLegId: (id: string | null) => void;
|
||||||
setMapCenter: (pos: [number, number]) => void;
|
setMapCenter: (pos: [number, number]) => void;
|
||||||
fetchTour: (id: string) => Promise<void>;
|
fetchTour: (id: string) => Promise<void>;
|
||||||
|
fetchPublicTourDetails: (tourId: string) => Promise<void>;
|
||||||
fetchPublicTours: () => Promise<void>;
|
fetchPublicTours: () => Promise<void>;
|
||||||
createJoinRequest: (tourId: string, userId?: string) => Promise<any>;
|
createJoinRequest: (tourId: string, userId?: string) => Promise<any>;
|
||||||
fetchJoinRequests: (tourId: string) => Promise<any[]>;
|
fetchJoinRequests: (tourId: string) => Promise<any[]>;
|
||||||
@@ -47,10 +48,9 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
setActiveLegId: (id) => set({ activeLegId: id }),
|
setActiveLegId: (id) => set({ activeLegId: id }),
|
||||||
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 token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`, {
|
const response = await fetch(`/api/v1/tours/${id}`, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
@@ -75,11 +75,10 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchPublicTours: async () => {
|
fetchPublicTours: async () => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/explore`, {
|
const response = await fetch(`/api/v1/tours/explore`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
}
|
||||||
@@ -89,9 +88,27 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
set({ publicTours: data });
|
set({ publicTours: data });
|
||||||
},
|
},
|
||||||
|
fetchPublicTourDetails: async (tourId: string) => {
|
||||||
|
try {
|
||||||
|
// Reset state cũ trước khi tải dữ liệu mới
|
||||||
|
set({ currentTour: null, legs: [], userRole: 'VIEWER_ONLY' });
|
||||||
|
|
||||||
|
const response = await fetch(`/api/v1/tours/${tourId}/public`);
|
||||||
|
if (!response.ok) throw new Error('Không thể tải tour công khai');
|
||||||
|
const data = await response.json();
|
||||||
|
const legs = data.legs || [];
|
||||||
|
set({
|
||||||
|
currentTour: data,
|
||||||
|
legs,
|
||||||
|
userRole: 'VIEWER_ONLY',
|
||||||
|
activeLegId: legs.length > 0 ? legs[0].id : null
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Lỗi khi tải tour công khai:', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
createTour: async (tourData: any) => {
|
createTour: async (tourData: any) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -108,9 +125,8 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
return tour;
|
return tour;
|
||||||
},
|
},
|
||||||
updateTourDetails: async (tourId: string, data: any) => {
|
updateTourDetails: async (tourId: string, data: any) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}`, {
|
const response = await fetch(`/api/v1/tours/${tourId}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -125,8 +141,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateTour: async (id: string, data: any) => {
|
updateTour: async (id: string, data: any) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${id}`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`, {
|
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -140,8 +155,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
get().fetchPublicTours();
|
get().fetchPublicTours();
|
||||||
},
|
},
|
||||||
deleteTour: async (id: string) => {
|
deleteTour: async (id: string) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${id}`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`, {
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
@@ -157,8 +171,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
get().fetchPublicTours();
|
get().fetchPublicTours();
|
||||||
},
|
},
|
||||||
addLeg: async (tourId: string, data: any) => {
|
addLeg: async (tourId: string, data: any) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${tourId}/legs`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/legs`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -170,8 +183,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
get().fetchTour(tourId);
|
get().fetchTour(tourId);
|
||||||
},
|
},
|
||||||
initializeLegs: async (tourId: string, count: number) => {
|
initializeLegs: async (tourId: string, count: number) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${tourId}/legs/batch`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/legs/batch`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -183,8 +195,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
await get().fetchTour(tourId);
|
await get().fetchTour(tourId);
|
||||||
},
|
},
|
||||||
updateLeg: async (legId: string, data: any) => {
|
updateLeg: async (legId: string, data: any) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/legs/${legId}`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/legs/${legId}`, {
|
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -198,8 +209,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
if (currentTour) get().fetchTour(currentTour.id);
|
if (currentTour) get().fetchTour(currentTour.id);
|
||||||
},
|
},
|
||||||
deleteLeg: async (legId: string) => {
|
deleteLeg: async (legId: string) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/legs/${legId}`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/legs/${legId}`, {
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
@@ -214,8 +224,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
if (currentTour) get().fetchTour(currentTour.id);
|
if (currentTour) get().fetchTour(currentTour.id);
|
||||||
},
|
},
|
||||||
addLocation: async (tourId: string, locationData: any) => {
|
addLocation: async (tourId: string, locationData: any) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${tourId}/locations`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/locations`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -228,8 +237,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
get().fetchTour(tourId);
|
get().fetchTour(tourId);
|
||||||
},
|
},
|
||||||
updateLocation: async (locationId: string, data: any) => {
|
updateLocation: async (locationId: string, data: any) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/locations/${locationId}`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/locations/${locationId}`, {
|
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -243,8 +251,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
if (currentTour) get().fetchTour(currentTour.id);
|
if (currentTour) get().fetchTour(currentTour.id);
|
||||||
},
|
},
|
||||||
deleteLocation: async (locationId: string) => {
|
deleteLocation: async (locationId: string) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/locations/${locationId}`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/locations/${locationId}`, {
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
@@ -256,8 +263,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
if (currentTour) get().fetchTour(currentTour.id);
|
if (currentTour) get().fetchTour(currentTour.id);
|
||||||
},
|
},
|
||||||
updateTourStartPoint: async (tourId: string, data: any) => {
|
updateTourStartPoint: async (tourId: string, data: any) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${tourId}/start-point`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/start-point`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -271,8 +277,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
await get().fetchTour(tourId);
|
await get().fetchTour(tourId);
|
||||||
},
|
},
|
||||||
updateTourEndPoint: async (tourId: string, data: any) => {
|
updateTourEndPoint: async (tourId: string, data: any) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${tourId}/end-point`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/end-point`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -284,8 +289,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
await get().fetchTour(tourId);
|
await get().fetchTour(tourId);
|
||||||
},
|
},
|
||||||
optimizeRouting: async (legId: string) => {
|
optimizeRouting: async (legId: string) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/routing/optimize/${legId}`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/routing/optimize/${legId}`, {
|
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
const { locations, totalDistance } = await response.json();
|
const { locations, totalDistance } = await response.json();
|
||||||
@@ -299,8 +303,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeMember: async (tourId: string, userId: string) => {
|
removeMember: async (tourId: string, userId: string) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${tourId}/members/${userId}`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/members/${userId}`, {
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||||
@@ -311,8 +314,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
if (currentTour) get().fetchTour(currentTour.id);
|
if (currentTour) get().fetchTour(currentTour.id);
|
||||||
},
|
},
|
||||||
addMember: async (tourId: string, member: { userId: string; role?: string }) => {
|
addMember: async (tourId: string, member: { userId: string; role?: string }) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${tourId}/members`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/members`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -328,8 +330,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
if (currentTour) get().fetchTour(currentTour.id);
|
if (currentTour) get().fetchTour(currentTour.id);
|
||||||
},
|
},
|
||||||
createJoinRequest: async (tourId: string, userId?: string) => {
|
createJoinRequest: async (tourId: string, userId?: string) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${tourId}/join-requests`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/join-requests`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -344,8 +345,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
fetchJoinRequests: async (tourId: string) => {
|
fetchJoinRequests: async (tourId: string) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${tourId}/join-requests`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/join-requests`, {
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||||
}
|
}
|
||||||
@@ -357,8 +357,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
acceptJoinRequest: async (tourId: string, requestId: string) => {
|
acceptJoinRequest: async (tourId: string, requestId: string) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${tourId}/join-requests/${requestId}/accept`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/join-requests/${requestId}/accept`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||||
@@ -372,8 +371,7 @@ export const useTourStore = create<TourState>((set, get) => ({
|
|||||||
if (currentTour) get().fetchTour(currentTour.id);
|
if (currentTour) get().fetchTour(currentTour.id);
|
||||||
},
|
},
|
||||||
rejectJoinRequest: async (tourId: string, requestId: string) => {
|
rejectJoinRequest: async (tourId: string, requestId: string) => {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/tours/${tourId}/join-requests/${requestId}/reject`, {
|
||||||
const response = await fetch(`${API_BASE}/api/v1/tours/${tourId}/join-requests/${requestId}/reject`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||||
|
|||||||
Reference in New Issue
Block a user