Thêm tính năng owner có thể thêm thành viên, member thêm thành viên sẽ pending
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
name: Local Config
|
||||||
|
version: 1.0.0
|
||||||
|
schema: v1
|
||||||
|
models:
|
||||||
|
- name: Autodetect
|
||||||
|
provider: lmstudio
|
||||||
|
model: AUTODETECT
|
||||||
|
apiBase: http://192.168.1.12:1234/v1/
|
||||||
+15
-5
@@ -5,6 +5,7 @@ import { useTourStore } from './useTourStore.js';
|
|||||||
import { AddLocationModal } from './AddLocationModal.js';
|
import { AddLocationModal } from './AddLocationModal.js';
|
||||||
import { AddMemberModal } from './AddMemberModal.js';
|
import { AddMemberModal } from './AddMemberModal.js';
|
||||||
import { ConfirmModal } from './ConfirmModal.js';
|
import { ConfirmModal } from './ConfirmModal.js';
|
||||||
|
import { NotificationModal, useNotificationModal } from './components/NotificationModal.js';
|
||||||
import { MapContainer, TileLayer, Marker, Popup, useMapEvents, Polyline } from 'react-leaflet';
|
import { MapContainer, TileLayer, Marker, Popup, useMapEvents, Polyline } from 'react-leaflet';
|
||||||
import _MarkerClusterGroup from 'react-leaflet-cluster';
|
import _MarkerClusterGroup from 'react-leaflet-cluster';
|
||||||
const MarkerClusterGroup = (_MarkerClusterGroup as any).default || _MarkerClusterGroup;
|
const MarkerClusterGroup = (_MarkerClusterGroup as any).default || _MarkerClusterGroup;
|
||||||
@@ -181,6 +182,8 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
|||||||
fetchJoinRequests, acceptJoinRequest, rejectJoinRequest
|
fetchJoinRequests, acceptJoinRequest, rejectJoinRequest
|
||||||
} = useTourStore();
|
} = useTourStore();
|
||||||
|
|
||||||
|
const notificationModal = useNotificationModal();
|
||||||
|
|
||||||
// Khôi phục vị trí và mức zoom từ localStorage
|
// Khôi phục vị trí và mức zoom từ localStorage
|
||||||
const [initialViewState] = useState(() => {
|
const [initialViewState] = useState(() => {
|
||||||
const saved = localStorage.getItem('map_view_state');
|
const saved = localStorage.getItem('map_view_state');
|
||||||
@@ -443,7 +446,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
|||||||
await acceptJoinRequest(currentTour.id, req.id);
|
await acceptJoinRequest(currentTour.id, req.id);
|
||||||
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
alert(e.message || 'Không thể chấp nhận yêu cầu');
|
notificationModal.openModal('Thông báo', e.message || 'Không thể chấp nhận yêu cầu', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setJoinRequestActionId(null);
|
setJoinRequestActionId(null);
|
||||||
setConfirmState({ open: false });
|
setConfirmState({ open: false });
|
||||||
@@ -472,7 +475,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
|||||||
await rejectJoinRequest(currentTour.id, req.id);
|
await rejectJoinRequest(currentTour.id, req.id);
|
||||||
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
alert(e.message || 'Không thể từ chối yêu cầu');
|
notificationModal.openModal('Thông báo', e.message || 'Không thể từ chối yêu cầu', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setJoinRequestActionId(null);
|
setJoinRequestActionId(null);
|
||||||
setConfirmState({ open: false });
|
setConfirmState({ open: false });
|
||||||
@@ -721,7 +724,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
|||||||
await acceptJoinRequest(currentTour.id, req.id);
|
await acceptJoinRequest(currentTour.id, req.id);
|
||||||
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
alert(e.message || 'Không thể chấp nhận yêu cầu');
|
notificationModal.openModal('Thông báo', e.message || 'Không thể chấp nhận yêu cầu', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setJoinRequestActionId(null);
|
setJoinRequestActionId(null);
|
||||||
setConfirmState({ open: false });
|
setConfirmState({ open: false });
|
||||||
@@ -748,7 +751,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
|||||||
await rejectJoinRequest(currentTour.id, req.id);
|
await rejectJoinRequest(currentTour.id, req.id);
|
||||||
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
alert(e.message || 'Không thể từ chối yêu cầu');
|
notificationModal.openModal('Thông báo', e.message || 'Không thể từ chối yêu cầu', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setJoinRequestActionId(null);
|
setJoinRequestActionId(null);
|
||||||
setConfirmState({ open: false });
|
setConfirmState({ open: false });
|
||||||
@@ -849,7 +852,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
|||||||
await removeMember(currentTour.id, selectedMember.userId);
|
await removeMember(currentTour.id, selectedMember.userId);
|
||||||
setIsMemberDetailOpen(false);
|
setIsMemberDetailOpen(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Không thể xóa thành viên');
|
notificationModal.openModal('Thông báo', 'Không thể xóa thành viên', 'error');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded-xl text-sm font-bold"
|
className="px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded-xl text-sm font-bold"
|
||||||
@@ -879,6 +882,13 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
|||||||
onConfirm={() => confirmState.onConfirm?.()}
|
onConfirm={() => confirmState.onConfirm?.()}
|
||||||
onCancel={() => setConfirmState({ open: false })}
|
onCancel={() => setConfirmState({ open: false })}
|
||||||
/>
|
/>
|
||||||
|
<NotificationModal
|
||||||
|
isOpen={notificationModal.modalState?.isOpen ?? false}
|
||||||
|
title={notificationModal.modalState?.title}
|
||||||
|
message={notificationModal.modalState?.message}
|
||||||
|
type={notificationModal.modalState?.type}
|
||||||
|
onConfirm={() => notificationModal.closeModal()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
// Types for notification modal
|
||||||
|
export interface NotificationModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
onConfirm?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
type?: 'info' | 'success' | 'warning' | 'error';
|
||||||
|
confirmButtonText?: string;
|
||||||
|
cancelButtonText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon components for different types
|
||||||
|
const Icons = {
|
||||||
|
info: (props: React.SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg {...props} viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<path d="M12 16v-4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||||
|
<path d="M12 8h.01" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
success: (props: React.SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg {...props} viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<path d="M8 12l2.5 2.5L15.5 9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
warning: (props: React.SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg {...props} viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||||
|
<circle cx="12" cy="17" r="1" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
error: (props: React.SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg {...props} viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<line x1="15" y1="9" x2="9" y2="15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||||
|
<line x1="9" y1="9" x2="15" y2="15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default props
|
||||||
|
const defaultProps: Partial<NotificationModalProps> = {
|
||||||
|
title: 'Thông báo',
|
||||||
|
type: 'info',
|
||||||
|
confirmButtonText: 'OK',
|
||||||
|
cancelButtonText: 'Hủy',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NotificationModal: React.FC<NotificationModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
title = defaultProps.title,
|
||||||
|
message,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
type = defaultProps.type,
|
||||||
|
confirmButtonText = defaultProps.confirmButtonText,
|
||||||
|
cancelButtonText = defaultProps.cancelButtonText,
|
||||||
|
}) => {
|
||||||
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
|
||||||
|
// Get colors based on type
|
||||||
|
const getTypeStyles = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'success':
|
||||||
|
return { bg: '#d4edda', border: '#c3e6cb', text: '#155724' };
|
||||||
|
case 'warning':
|
||||||
|
return { bg: '#fff3cd', border: '#ffeeba', text: '#856404' };
|
||||||
|
case 'error':
|
||||||
|
return { bg: '#f8d7da', border: '#f5c6cb', text: '#721c24' };
|
||||||
|
default:
|
||||||
|
return { bg: '#e2e3e5', border: '#d6d8db', text: '#383d41' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = getTypeStyles();
|
||||||
|
|
||||||
|
// Animation classes based on state
|
||||||
|
const getAnimationClass = () => {
|
||||||
|
if (!isOpen) return 'opacity-0 translate-y-4';
|
||||||
|
if (isAnimating && onCancel) return 'animate-fade-out';
|
||||||
|
return 'animate-fade-in';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle confirm click
|
||||||
|
const handleConfirm = () => {
|
||||||
|
setIsAnimating(true);
|
||||||
|
onConfirm?.();
|
||||||
|
setTimeout(() => setIsAnimating(false), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle cancel click
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsAnimating(true);
|
||||||
|
onCancel?.();
|
||||||
|
setTimeout(() => setIsAnimating(false), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4">
|
||||||
|
<div
|
||||||
|
className={`bg-white rounded-lg shadow-xl max-w-md w-full transform transition-all duration-300 ${getAnimationClass()}`}
|
||||||
|
role="alertdialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
aria-describedby="modal-message"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className={`p-6 border-b ${styles.border}`}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{type === 'success' && <Icons.success className="w-5 h-5 text-green-600" />}
|
||||||
|
{type === 'warning' && <Icons.warning className="w-5 h-5 text-yellow-600" />}
|
||||||
|
{type === 'error' && <Icons.error className="w-5 h-5 text-red-600" />}
|
||||||
|
{type === 'info' && <Icons.info className="w-5 h-5 text-blue-600" />}
|
||||||
|
<h2 id="modal-title" className={`text-xl font-semibold ${styles.text}`}>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="p-6">
|
||||||
|
<p id="modal-message" className="text-gray-700 leading-relaxed">{message}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className={`px-6 py-4 flex justify-end gap-3 border-t ${styles.border}`}>
|
||||||
|
{onCancel && (
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
{cancelButtonText}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onConfirm && (
|
||||||
|
<button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
className={`px-4 py-2 text-white rounded-md font-medium transition-colors ${
|
||||||
|
type === 'error'
|
||||||
|
? 'bg-red-600 hover:bg-red-700'
|
||||||
|
: 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{confirmButtonText}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook for easy usage without props management
|
||||||
|
export const useNotificationModal = () => {
|
||||||
|
const [modalState, setModalState] = useState<{
|
||||||
|
isOpen: boolean;
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
type?: 'info' | 'success' | 'warning' | 'error';
|
||||||
|
onConfirm?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const openModal = (
|
||||||
|
title: string,
|
||||||
|
message: string,
|
||||||
|
type: 'info' | 'success' | 'warning' | 'error' = 'info',
|
||||||
|
onConfirm?: () => void,
|
||||||
|
onCancel?: () => void,
|
||||||
|
) => {
|
||||||
|
setModalState({
|
||||||
|
isOpen: true,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-close after 5 seconds if no confirm action
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (onCancel) {
|
||||||
|
setModalState((prev) => ({ ...prev, isOpen: false }));
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setModalState((prev) => prev ? { ...prev, isOpen: false } : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
modalState,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationModal;
|
||||||
Vendored
+8
-6
@@ -6,6 +6,7 @@ import { useTourStore } from './useTourStore.js';
|
|||||||
import { AddLocationModal } from './AddLocationModal.js';
|
import { AddLocationModal } from './AddLocationModal.js';
|
||||||
import { AddMemberModal } from './AddMemberModal.js';
|
import { AddMemberModal } from './AddMemberModal.js';
|
||||||
import { ConfirmModal } from './ConfirmModal.js';
|
import { ConfirmModal } from './ConfirmModal.js';
|
||||||
|
import { NotificationModal, useNotificationModal } from './components/NotificationModal.js';
|
||||||
import { MapContainer, TileLayer, Marker, Popup, useMapEvents, Polyline } from 'react-leaflet';
|
import { MapContainer, TileLayer, Marker, Popup, useMapEvents, Polyline } from 'react-leaflet';
|
||||||
import _MarkerClusterGroup from 'react-leaflet-cluster';
|
import _MarkerClusterGroup from 'react-leaflet-cluster';
|
||||||
const MarkerClusterGroup = _MarkerClusterGroup.default || _MarkerClusterGroup;
|
const MarkerClusterGroup = _MarkerClusterGroup.default || _MarkerClusterGroup;
|
||||||
@@ -104,6 +105,7 @@ export const TourDetailPage = ({ onBack }) => {
|
|||||||
const [joinRequestActionId, setJoinRequestActionId] = useState(null);
|
const [joinRequestActionId, setJoinRequestActionId] = useState(null);
|
||||||
const [confirmState, setConfirmState] = useState({ open: false });
|
const [confirmState, setConfirmState] = useState({ open: false });
|
||||||
const { currentTour, legs, fetchTour, fetchPublicTours, publicTours, userRole, mapCenter, setMapCenter, updateTourStartPoint, updateTourEndPoint, initializeLegs, addLocation, addMember, removeMember, fetchJoinRequests, acceptJoinRequest, rejectJoinRequest } = useTourStore();
|
const { currentTour, legs, fetchTour, fetchPublicTours, publicTours, userRole, mapCenter, setMapCenter, updateTourStartPoint, updateTourEndPoint, initializeLegs, addLocation, addMember, removeMember, fetchJoinRequests, acceptJoinRequest, rejectJoinRequest } = useTourStore();
|
||||||
|
const notificationModal = useNotificationModal();
|
||||||
const [initialViewState] = useState(() => {
|
const [initialViewState] = useState(() => {
|
||||||
const saved = localStorage.getItem('map_view_state');
|
const saved = localStorage.getItem('map_view_state');
|
||||||
if (saved) {
|
if (saved) {
|
||||||
@@ -267,7 +269,7 @@ export const TourDetailPage = ({ onBack }) => {
|
|||||||
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
alert(e.message || 'Không thể chấp nhận yêu cầu');
|
notificationModal.openModal('Thông báo', e.message || 'Không thể chấp nhận yêu cầu', 'error');
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setJoinRequestActionId(null);
|
setJoinRequestActionId(null);
|
||||||
@@ -290,7 +292,7 @@ export const TourDetailPage = ({ onBack }) => {
|
|||||||
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
alert(e.message || 'Không thể từ chối yêu cầu');
|
notificationModal.openModal('Thông báo', e.message || 'Không thể từ chối yêu cầu', 'error');
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setJoinRequestActionId(null);
|
setJoinRequestActionId(null);
|
||||||
@@ -333,7 +335,7 @@ export const TourDetailPage = ({ onBack }) => {
|
|||||||
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
alert(e.message || 'Không thể chấp nhận yêu cầu');
|
notificationModal.openModal('Thông báo', e.message || 'Không thể chấp nhận yêu cầu', 'error');
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setJoinRequestActionId(null);
|
setJoinRequestActionId(null);
|
||||||
@@ -355,7 +357,7 @@ export const TourDetailPage = ({ onBack }) => {
|
|||||||
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
setJoinRequests((prev) => prev.filter((r) => r.id !== req.id));
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
alert(e.message || 'Không thể từ chối yêu cầu');
|
notificationModal.openModal('Thông báo', e.message || 'Không thể từ chối yêu cầu', 'error');
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setJoinRequestActionId(null);
|
setJoinRequestActionId(null);
|
||||||
@@ -376,11 +378,11 @@ export const TourDetailPage = ({ onBack }) => {
|
|||||||
setIsMemberDetailOpen(false);
|
setIsMemberDetailOpen(false);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
alert('Không thể xóa thành viên');
|
notificationModal.openModal('Thông báo', 'Không thể xóa thành viên', 'error');
|
||||||
}
|
}
|
||||||
}, className: "px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded-xl text-sm font-bold", children: "X\u00F3a" })), canEdit && selectedMember.role === 'OWNER' && (_jsx("button", { onClick: () => {
|
}, className: "px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded-xl text-sm font-bold", children: "X\u00F3a" })), canEdit && selectedMember.role === 'OWNER' && (_jsx("button", { onClick: () => {
|
||||||
setIsMemberDetailOpen(false);
|
setIsMemberDetailOpen(false);
|
||||||
setIsAddMemberOpen(true);
|
setIsAddMemberOpen(true);
|
||||||
}, className: "px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-xl text-sm font-bold", children: "M\u1EDDi th\u00EAm ng\u01B0\u1EDDi" }))] })] })] })), _jsx(ConfirmModal, { isOpen: confirmState.open, title: confirmState.title, message: confirmState.message, onConfirm: () => confirmState.onConfirm?.(), onCancel: () => setConfirmState({ open: false }) })] }));
|
}, className: "px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-xl text-sm font-bold", children: "M\u1EDDi th\u00EAm ng\u01B0\u1EDDi" }))] })] })] })), _jsx(ConfirmModal, { isOpen: confirmState.open, title: confirmState.title, message: confirmState.message, onConfirm: () => confirmState.onConfirm?.(), onCancel: () => setConfirmState({ open: false }) }), _jsx(NotificationModal, { isOpen: notificationModal.modalState?.isOpen ?? false, title: notificationModal.modalState?.title, message: notificationModal.modalState?.message, type: notificationModal.modalState?.type, onConfirm: () => notificationModal.closeModal() })] }));
|
||||||
};
|
};
|
||||||
//# sourceMappingURL=TourDetailPage.js.map
|
//# sourceMappingURL=TourDetailPage.js.map
|
||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
export interface NotificationModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
onConfirm?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
type?: 'info' | 'success' | 'warning' | 'error';
|
||||||
|
confirmButtonText?: string;
|
||||||
|
cancelButtonText?: string;
|
||||||
|
}
|
||||||
|
export declare const NotificationModal: React.FC<NotificationModalProps>;
|
||||||
|
export declare const useNotificationModal: () => {
|
||||||
|
modalState: {
|
||||||
|
isOpen: boolean;
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
type?: "info" | "success" | "warning" | "error";
|
||||||
|
onConfirm?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
};
|
||||||
|
openModal: (title: string, message: string, type?: "info" | "success" | "warning" | "error", onConfirm?: () => void, onCancel?: () => void) => () => void;
|
||||||
|
closeModal: () => void;
|
||||||
|
};
|
||||||
|
export default NotificationModal;
|
||||||
Vendored
+81
@@ -0,0 +1,81 @@
|
|||||||
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||||
|
import { useState } from 'react';
|
||||||
|
const Icons = {
|
||||||
|
info: (props) => (_jsxs("svg", { ...props, viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2" }), _jsx("path", { d: "M12 16v-4", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("path", { d: "M12 8h.01", stroke: "currentColor", strokeWidth: "3", strokeLinecap: "round" })] })),
|
||||||
|
success: (props) => (_jsxs("svg", { ...props, viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2" }), _jsx("path", { d: "M8 12l2.5 2.5L15.5 9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })] })),
|
||||||
|
warning: (props) => (_jsxs("svg", { ...props, viewBox: "0 0 24 24", fill: "none", children: [_jsx("path", { d: "M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z", stroke: "currentColor", strokeWidth: "2" }), _jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("circle", { cx: "12", cy: "17", r: "1", fill: "currentColor" })] })),
|
||||||
|
error: (props) => (_jsxs("svg", { ...props, viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2" }), _jsx("line", { x1: "15", y1: "9", x2: "9", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "9", y1: "9", x2: "15", y2: "15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] })),
|
||||||
|
};
|
||||||
|
const defaultProps = {
|
||||||
|
title: 'Thông báo',
|
||||||
|
type: 'info',
|
||||||
|
confirmButtonText: 'OK',
|
||||||
|
cancelButtonText: 'Hủy',
|
||||||
|
};
|
||||||
|
export const NotificationModal = ({ isOpen, title = defaultProps.title, message, onConfirm, onCancel, type = defaultProps.type, confirmButtonText = defaultProps.confirmButtonText, cancelButtonText = defaultProps.cancelButtonText, }) => {
|
||||||
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
const getTypeStyles = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'success':
|
||||||
|
return { bg: '#d4edda', border: '#c3e6cb', text: '#155724' };
|
||||||
|
case 'warning':
|
||||||
|
return { bg: '#fff3cd', border: '#ffeeba', text: '#856404' };
|
||||||
|
case 'error':
|
||||||
|
return { bg: '#f8d7da', border: '#f5c6cb', text: '#721c24' };
|
||||||
|
default:
|
||||||
|
return { bg: '#e2e3e5', border: '#d6d8db', text: '#383d41' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const styles = getTypeStyles();
|
||||||
|
const getAnimationClass = () => {
|
||||||
|
if (!isOpen)
|
||||||
|
return 'opacity-0 translate-y-4';
|
||||||
|
if (isAnimating && onCancel)
|
||||||
|
return 'animate-fade-out';
|
||||||
|
return 'animate-fade-in';
|
||||||
|
};
|
||||||
|
const handleConfirm = () => {
|
||||||
|
setIsAnimating(true);
|
||||||
|
onConfirm?.();
|
||||||
|
setTimeout(() => setIsAnimating(false), 300);
|
||||||
|
};
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsAnimating(true);
|
||||||
|
onCancel?.();
|
||||||
|
setTimeout(() => setIsAnimating(false), 300);
|
||||||
|
};
|
||||||
|
if (!isOpen)
|
||||||
|
return null;
|
||||||
|
return (_jsx("div", { className: "fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4", children: _jsxs("div", { className: `bg-white rounded-lg shadow-xl max-w-md w-full transform transition-all duration-300 ${getAnimationClass()}`, role: "alertdialog", "aria-modal": "true", "aria-labelledby": "modal-title", "aria-describedby": "modal-message", children: [_jsx("div", { className: `p-6 border-b ${styles.border}`, children: _jsxs("div", { className: "flex items-center gap-3", children: [type === 'success' && _jsx(Icons.success, { className: "w-5 h-5 text-green-600" }), type === 'warning' && _jsx(Icons.warning, { className: "w-5 h-5 text-yellow-600" }), type === 'error' && _jsx(Icons.error, { className: "w-5 h-5 text-red-600" }), type === 'info' && _jsx(Icons.info, { className: "w-5 h-5 text-blue-600" }), _jsx("h2", { id: "modal-title", className: `text-xl font-semibold ${styles.text}`, children: title })] }) }), _jsx("div", { className: "p-6", children: _jsx("p", { id: "modal-message", className: "text-gray-700 leading-relaxed", children: message }) }), _jsxs("div", { className: `px-6 py-4 flex justify-end gap-3 border-t ${styles.border}`, children: [onCancel && (_jsx("button", { onClick: handleCancel, className: "px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-md transition-colors", children: cancelButtonText })), onConfirm && (_jsx("button", { onClick: handleConfirm, className: `px-4 py-2 text-white rounded-md font-medium transition-colors ${type === 'error'
|
||||||
|
? 'bg-red-600 hover:bg-red-700'
|
||||||
|
: 'bg-blue-600 hover:bg-blue-700'}`, children: confirmButtonText }))] })] }) }));
|
||||||
|
};
|
||||||
|
export const useNotificationModal = () => {
|
||||||
|
const [modalState, setModalState] = useState(null);
|
||||||
|
const openModal = (title, message, type = 'info', onConfirm, onCancel) => {
|
||||||
|
setModalState({
|
||||||
|
isOpen: true,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
});
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (onCancel) {
|
||||||
|
setModalState((prev) => ({ ...prev, isOpen: false }));
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
};
|
||||||
|
const closeModal = () => {
|
||||||
|
setModalState((prev) => prev ? { ...prev, isOpen: false } : null);
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
modalState,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export default NotificationModal;
|
||||||
|
//# sourceMappingURL=NotificationModal.js.map
|
||||||
+1
File diff suppressed because one or more lines are too long
Vendored
+21
-3
@@ -342,7 +342,13 @@ let TourController = class TourController {
|
|||||||
include: { user: { select: { id: true, name: true, email: true } } },
|
include: { user: { select: { id: true, name: true, email: true } } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const currentRole = req.user.tourParticipation?.role;
|
let currentRole = req.user.tourParticipation?.role;
|
||||||
|
if (!currentRole) {
|
||||||
|
const participation = await this.prisma.tourParticipant.findUnique({
|
||||||
|
where: { tourId_userId: { tourId, userId: req.user.id } },
|
||||||
|
});
|
||||||
|
currentRole = participation?.role;
|
||||||
|
}
|
||||||
if (!currentRole || !(currentRole === 'OWNER' || currentRole === 'MANAGER')) {
|
if (!currentRole || !(currentRole === 'OWNER' || currentRole === 'MANAGER')) {
|
||||||
const joinRequest = await this.prisma.joinRequest.create({
|
const joinRequest = await this.prisma.joinRequest.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -407,7 +413,13 @@ let TourController = class TourController {
|
|||||||
return joinRequest;
|
return joinRequest;
|
||||||
}
|
}
|
||||||
async acceptJoinRequest(tourId, requestId, req) {
|
async acceptJoinRequest(tourId, requestId, req) {
|
||||||
const role = req.user.tourParticipation?.role;
|
let role = req.user.tourParticipation?.role;
|
||||||
|
if (!role) {
|
||||||
|
const participation = await this.prisma.tourParticipant.findUnique({
|
||||||
|
where: { tourId_userId: { tourId, userId: req.user.id } },
|
||||||
|
});
|
||||||
|
role = participation?.role;
|
||||||
|
}
|
||||||
if (!role || !(role === 'OWNER' || role === 'MANAGER')) {
|
if (!role || !(role === 'OWNER' || role === 'MANAGER')) {
|
||||||
throw new ForbiddenException('Bạn không có quyền chấp nhận yêu cầu tham gia.');
|
throw new ForbiddenException('Bạn không có quyền chấp nhận yêu cầu tham gia.');
|
||||||
}
|
}
|
||||||
@@ -446,7 +458,13 @@ let TourController = class TourController {
|
|||||||
return { success: true, message: 'Đã chấp nhận yêu cầu tham gia.' };
|
return { success: true, message: 'Đã chấp nhận yêu cầu tham gia.' };
|
||||||
}
|
}
|
||||||
async rejectJoinRequest(tourId, requestId, req) {
|
async rejectJoinRequest(tourId, requestId, req) {
|
||||||
const role = req.user.tourParticipation?.role;
|
let role = req.user.tourParticipation?.role;
|
||||||
|
if (!role) {
|
||||||
|
const participation = await this.prisma.tourParticipant.findUnique({
|
||||||
|
where: { tourId_userId: { tourId, userId: req.user.id } },
|
||||||
|
});
|
||||||
|
role = participation?.role;
|
||||||
|
}
|
||||||
if (!role || !(role === 'OWNER' || role === 'MANAGER')) {
|
if (!role || !(role === 'OWNER' || role === 'MANAGER')) {
|
||||||
throw new ForbiddenException('Bạn không có quyền từ chối yêu cầu tham gia.');
|
throw new ForbiddenException('Bạn không có quyền từ chối yêu cầu tham gia.');
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -21,7 +21,7 @@ let TourRoleGuard = class TourRoleGuard {
|
|||||||
if (!user || !tourId) {
|
if (!user || !tourId) {
|
||||||
throw new ForbiddenException("Thông tin xác thực hoặc mã Tour không hợp lệ.");
|
throw new ForbiddenException("Thông tin xác thực hoặc mã Tour không hợp lệ.");
|
||||||
}
|
}
|
||||||
if (path.includes('/join-requests') && request.method === 'POST' && !request.params.requestId) {
|
if (path.includes('/join-requests') && request.method === 'POST' && (!request.params.requestId || /\/join-requests\/[^/]+\/(accept|reject)$/.test(path))) {
|
||||||
request.tourParticipation = null;
|
request.tourParticipation = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"rbac.middleware.js","sourceRoot":"","sources":["../rbac.middleware.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,UAAU,EAAiC,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAG7C,IAAM,aAAa,GAAnB,MAAM,aAAa;IACxB,YAAoB,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE7C,KAAK,CAAC,WAAW,CAAC,OAAyB;QACzC,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAE1B,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;QAED,IAAI,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YAC9F,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;YACjC,OAAO,IAAI,CAAC;QACd,CAAC;QAED,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;QAED,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;QAEjD,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;AA/CY,aAAa;IADzB,UAAU,EAAE;qCAEiB,aAAa;GAD9B,aAAa,CA+CzB"}
|
{"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;QAE1B,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;QAED,IAAI,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,0CAA0C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACzJ,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;YACjC,OAAO,IAAI,CAAC;QACd,CAAC;QAED,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;QAED,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;QAEjD,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;AA/CY,aAAa;IADzB,UAAU,EAAE;qCAEiB,aAAa;GAD9B,aAAa,CA+CzB"}
|
||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -378,7 +378,13 @@ class TourController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentRole = req.user.tourParticipation?.role;
|
let currentRole = req.user.tourParticipation?.role;
|
||||||
|
if (!currentRole) {
|
||||||
|
const participation = await this.prisma.tourParticipant.findUnique({
|
||||||
|
where: { tourId_userId: { tourId, userId: req.user.id } },
|
||||||
|
});
|
||||||
|
currentRole = participation?.role;
|
||||||
|
}
|
||||||
if (!currentRole || !(currentRole === 'OWNER' || currentRole === 'MANAGER')) {
|
if (!currentRole || !(currentRole === 'OWNER' || currentRole === 'MANAGER')) {
|
||||||
const joinRequest = await this.prisma.joinRequest.create({
|
const joinRequest = await this.prisma.joinRequest.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -458,7 +464,13 @@ class TourController {
|
|||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Post(':tourId/join-requests/:requestId/accept')
|
@Post(':tourId/join-requests/:requestId/accept')
|
||||||
async acceptJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('requestId') requestId: string, @Req() req: any) {
|
async acceptJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('requestId') requestId: string, @Req() req: any) {
|
||||||
const role = req.user.tourParticipation?.role;
|
let role = req.user.tourParticipation?.role;
|
||||||
|
if (!role) {
|
||||||
|
const participation = await this.prisma.tourParticipant.findUnique({
|
||||||
|
where: { tourId_userId: { tourId, userId: req.user.id } },
|
||||||
|
});
|
||||||
|
role = participation?.role;
|
||||||
|
}
|
||||||
if (!role || !(role === 'OWNER' || role === 'MANAGER')) {
|
if (!role || !(role === 'OWNER' || role === 'MANAGER')) {
|
||||||
throw new ForbiddenException('Bạn không có quyền chấp nhận yêu cầu tham gia.');
|
throw new ForbiddenException('Bạn không có quyền chấp nhận yêu cầu tham gia.');
|
||||||
}
|
}
|
||||||
@@ -504,7 +516,13 @@ class TourController {
|
|||||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||||
@Post(':tourId/join-requests/:requestId/reject')
|
@Post(':tourId/join-requests/:requestId/reject')
|
||||||
async rejectJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('requestId') requestId: string, @Req() req: any) {
|
async rejectJoinRequest(@Param('tourId', ParseUUIDPipe) tourId: string, @Param('requestId') requestId: string, @Req() req: any) {
|
||||||
const role = req.user.tourParticipation?.role;
|
let role = req.user.tourParticipation?.role;
|
||||||
|
if (!role) {
|
||||||
|
const participation = await this.prisma.tourParticipant.findUnique({
|
||||||
|
where: { tourId_userId: { tourId, userId: req.user.id } },
|
||||||
|
});
|
||||||
|
role = participation?.role;
|
||||||
|
}
|
||||||
if (!role || !(role === 'OWNER' || role === 'MANAGER')) {
|
if (!role || !(role === 'OWNER' || role === 'MANAGER')) {
|
||||||
throw new ForbiddenException('Bạn không có quyền từ chối yêu cầu tham gia.');
|
throw new ForbiddenException('Bạn không có quyền từ chối yêu cầu tham gia.');
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ export class TourRoleGuard implements CanActivate {
|
|||||||
throw new ForbiddenException("Thông tin xác thực hoặc mã Tour không hợp lệ.");
|
throw new ForbiddenException("Thông tin xác thực hoặc mã Tour không hợp lệ.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.includes('/join-requests') && request.method === 'POST' && !request.params.requestId) {
|
if (path.includes('/join-requests') && request.method === 'POST' && (!request.params.requestId || /\/join-requests\/[^/]+\/(accept|reject)$/.test(path))) {
|
||||||
request.tourParticipation = null;
|
request.tourParticipation = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user