fix: lưu ghi chú ở địa điểm vào ghi chú
This commit is contained in:
@@ -48,7 +48,7 @@ export const ItineraryTimeline = ({
|
||||
}: {
|
||||
onAddLocation?: (legId: string, isStart?: boolean, isEnd?: boolean) => void,
|
||||
onEditLocation?: (location: any) => void,
|
||||
onQuickNote?: (name: string) => void,
|
||||
onQuickNote?: (data: { legId: string; location: any; leg: any }) => void,
|
||||
onNavigate?: (location: any) => void,
|
||||
onSuccess?: () => void,
|
||||
isPublicView?: boolean
|
||||
@@ -470,12 +470,12 @@ export const ItineraryTimeline = ({
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onQuickNote(location.name);
|
||||
onQuickNote({ legId: leg.id, location, leg });
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-amber-50 hover:bg-amber-100 text-amber-600 rounded-lg text-[10px] font-bold transition-all border border-amber-100"
|
||||
className="p-1.5 text-yotripLight-steel hover:text-yotripLight-text dark:text-yotripDark-muted dark:hover:text-yotripDark-lime rounded-lg hover:bg-gray-100 dark:hover:bg-yotripDark-surface-muted transition-colors duration-200"
|
||||
title="Ghi chú nhanh"
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
<FileText className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
|
||||
@@ -355,22 +355,22 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
|
||||
{!isCreating && (
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Tìm kiếm nội dung ghi chú..."
|
||||
className="w-full pl-10 pr-4 py-3 bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all text-sm"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-muted)]" />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsCreating(true)}
|
||||
className="p-3 bg-amber-500 text-white rounded-2xl shadow-lg shadow-amber-200 hover:bg-amber-600 transition-all active:scale-95"
|
||||
>
|
||||
<Plus className="w-6 h-6" />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Tìm kiếm nội dung ghi chú..."
|
||||
className="w-full pl-10 pr-4 py-3 bg-[var(--surface)] dark:bg-[var(--background-alt)] rounded-2xl border border-[var(--border)] dark:border-[var(--border)] shadow-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all text-sm text-[var(--text-primary)] dark:text-[var(--text-primary)] placeholder-[var(--text-muted)]"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-muted)]" />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsCreating(true)}
|
||||
className="p-3 bg-amber-500 text-white rounded-2xl shadow-lg shadow-amber-200 hover:bg-amber-600 transition-all active:scale-95"
|
||||
>
|
||||
<Plus className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
@@ -387,7 +387,7 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
|
||||
value={noteForm.title}
|
||||
onChange={(e) => setNoteForm({ ...noteForm, title: e.target.value })}
|
||||
/>
|
||||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm overflow-hidden min-h-[500px] flex flex-col">
|
||||
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm overflow-hidden min-h-[500px] flex flex-col h-full">
|
||||
<ReactQuill
|
||||
ref={quillRef}
|
||||
theme="snow"
|
||||
@@ -395,7 +395,7 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
|
||||
onChange={(content) => setNoteForm({ ...noteForm, content })}
|
||||
modules={quillModules}
|
||||
placeholder="Bắt đầu viết cảm nhận của bạn tại đây..."
|
||||
className="flex-1 flex flex-col [&_.ql-container]:flex-1 [&_.ql-container]:flex [&_.ql-container]:flex-col [&_.ql-editor]:flex-1 [&_.ql-editor]:text-base [&_.ql-toolbar]:flex [&_.ql-toolbar]:flex-wrap sm:[&_.ql-toolbar]:flex-nowrap [&_.ql-toolbar]:border-0 [&_.ql-toolbar]:border-b [&_.ql-toolbar]:border-[var(--border)] [&_.ql-editor_table]:border-collapse [&_.ql-editor_table]:w-full [&_.ql-editor_td]:border [&_.ql-editor_td]:border-[var(--border)] [&_.ql-editor_td]:p-2 [&_.ql-editor_.ql-align-center]:text-center [&_.ql-editor_.ql-align-right]:text-right [&_.ql-editor_.ql-align-justify]:text-justify [&_.ql-picker-options]:!z-[50] [&_.ql-picker-options]:!shadow-xl [&_.ql-picker-options]:!rounded-xl [&_.ql-picker-options]:!border-[var(--border)] [&_.ql-stroke]:!stroke-[var(--text-secondary)] [&_.ql-fill]:!fill-[var(--text-secondary)] [&_button:hover_.ql-stroke]:!stroke-amber-500 [&_button:hover_.ql-fill]:!stroke-amber-500 [&_button.ql-active_.ql-stroke]:!stroke-amber-500"
|
||||
className="flex-1 flex flex-col [&_.ql-container]:flex-1 [&_.ql-container]:flex [&_.ql-container]:flex-col [&_.ql-container]:h-full [&_.ql-container]:min-h-0 [&_.ql-editor]:flex-1 [&_.ql-editor]:overflow-y-auto [&_.ql-editor]:text-base [&_.ql-toolbar]:flex [&_.ql-toolbar]:flex-wrap sm:[&_.ql-toolbar]:flex-nowrap [&_.ql-toolbar]:border-0 [&_.ql-toolbar]:border-b [&_.ql-toolbar]:border-[var(--border)] [&_.ql-editor_table]:border-collapse [&_.ql-editor_table]:w-full [&_.ql-editor_td]:border [&_.ql-editor_td]:border-[var(--border)] [&_.ql-editor_td]:p-2 [&_.ql-editor_.ql-align-center]:text-center [&_.ql-editor_.ql-align-right]:text-right [&_.ql-editor_.ql-align-justify]:text-justify [&_.ql-picker-options]:!z-[50] [&_.ql-picker-options]:!shadow-xl [&_.ql-picker-options]:!rounded-xl [&_.ql-picker-options]:!border-[var(--border)] [&_.ql-stroke]:!stroke-[var(--text-secondary)] [&_.ql-fill]:!fill-[var(--text-secondary)] [&_button:hover_.ql-stroke]:!stroke-amber-500 [&_button:hover_.ql-fill]:!stroke-amber-500 [&_button.ql-active_.ql-stroke]:!stroke-amber-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
@@ -416,26 +416,26 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
|
||||
) : filteredNotes.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{filteredNotes.map((note) => (
|
||||
<div key={note.id} className="bg-[var(--surface)] p-5 rounded-3xl border border-[var(--border)] shadow-sm hover:shadow-md transition-all group">
|
||||
<div key={note.id} className="bg-[var(--surface)] dark:bg-[var(--surface-muted)] p-5 rounded-3xl border border-[var(--border)] dark:border-[var(--border)] shadow-sm hover:shadow-md transition-all group">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h4 className="font-bold text-[var(--text-primary)]">{note.title}</h4>
|
||||
<div className="flex items-center gap-2 text-[10px] text-[var(--text-muted)] font-bold uppercase mt-1">
|
||||
<h4 className="font-bold text-[var(--text-primary)] dark:text-[var(--text-primary)]">{note.title}</h4>
|
||||
<div className="flex items-center gap-2 text-[10px] text-[var(--text-muted)] dark:text-[var(--text-muted)] font-bold uppercase mt-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{new Date(note.createdAt).toLocaleDateString('vi-VN')}
|
||||
{new Date(note.createdAt).toLocaleDateString("vi-VN")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => handleEditNote(note)}
|
||||
className="p-2 text-[var(--text-muted)] hover:text-blue-500 hover:bg-[var(--background)] rounded-xl transition-all opacity-0 group-hover:opacity-100"
|
||||
className="p-2 text-[var(--text-muted)] dark:text-[var(--text-muted)] hover:text-blue-500 hover:bg-[var(--background)] dark:hover:bg-[var(--background)] rounded-xl transition-all opacity-0 group-hover:opacity-100"
|
||||
title="Sửa ghi chú"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteNote(note.id)}
|
||||
className="p-2 text-[var(--text-muted)] hover:text-red-500 hover:bg-[var(--background)] rounded-xl transition-all opacity-0 group-hover:opacity-100"
|
||||
className="p-2 text-[var(--text-muted)] dark:text-[var(--text-muted)] hover:text-red-500 hover:bg-[var(--background)] dark:hover:bg-[var(--background)] rounded-xl transition-all opacity-0 group-hover:opacity-100"
|
||||
title="Xóa ghi chú"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
@@ -443,30 +443,30 @@ export const MyNotePage = ({ tourId, onBack }: { tourId: string; onBack: () => v
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="text-sm text-[var(--text-secondary)] line-clamp-3 prose prose-sm max-w-none ql-editor !p-0 [&_table]:border-collapse [&_table]:w-full [&_td]:border [&_td]:border-[var(--border)] [&_td]:p-2 [&_.ql-align-center]:text-center [&_.ql-align-right]:text-right [&_.ql-align-justify]:text-justify"
|
||||
className="text-sm text-[var(--text-secondary)] dark:text-[var(--text-secondary)] line-clamp-3 prose prose-sm max-w-none ql-editor !p-0 [&_table]:border-collapse [&_table]:w-full [&_table]:text-[var(--text-primary)] dark:[&_table]:text-[var(--text-primary)] [&_td]:border [&_td]:border-[var(--border)] dark:[&_td]:border-[var(--border)] [&_td]:p-2 [&_td]:bg-[var(--surface)] dark:[&_td]:bg-[var(--background-alt)] [&_.ql-align-center]:text-center [&_.ql-align-right]:text-right [&_.ql-align-justify]:text-justify [&_table]:table-fixed [&_td]:break-words"
|
||||
dangerouslySetInnerHTML={{ __html: note.content }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-24 bg-[var(--surface)] rounded-[40px] border-2 border-dashed border-[var(--border)] shadow-inner">
|
||||
<div className="w-20 h-20 bg-amber-50 rounded-3xl flex items-center justify-center mx-auto mb-6 text-amber-500">
|
||||
<FileText className="w-10 h-10" />
|
||||
<div className="text-center py-24 bg-[var(--surface)] dark:bg-[var(--background-alt)] rounded-[40px] border-2 border-dashed border-[var(--border)] dark:border-[var(--border)] shadow-inner">
|
||||
<div className="w-20 h-20 bg-amber-50 rounded-3xl flex items-center justify-center mx-auto mb-6 text-amber-500">
|
||||
<FileText className="w-10 h-10" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white">{searchQuery ? 'Không tìm thấy ghi chú' : 'Chưa có ghi chú nào'}</h3>
|
||||
<p className="text-sm text-gray-400 dark:text-slate-400 max-w-xs mx-auto mt-2">
|
||||
{searchQuery ? 'Hãy thử tìm kiếm với từ khóa khác.' : 'Hãy lưu lại những cảm nhận, lịch trình riêng hoặc các lưu ý quan trọng cho hành trình của bạn.'}
|
||||
</p>
|
||||
{!searchQuery && (
|
||||
<button
|
||||
onClick={() => setIsCreating(true)}
|
||||
className="mt-8 inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-white px-8 py-4 rounded-2xl font-black uppercase tracking-widest text-xs transition-all shadow-lg shadow-amber-200 active:scale-95"
|
||||
>
|
||||
<Plus className="w-5 h-5" /> Tạo ghi chú mới
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900">{searchQuery ? 'Không tìm thấy ghi chú' : 'Chưa có ghi chú nào'}</h3>
|
||||
<p className="text-sm text-gray-400 max-w-xs mx-auto mt-2">
|
||||
{searchQuery ? 'Hãy thử tìm kiếm với từ khóa khác.' : 'Hãy lưu lại những cảm nhận, lịch trình riêng hoặc các lưu ý quan trọng cho hành trình của bạn.'}
|
||||
</p>
|
||||
{!searchQuery && (
|
||||
<button
|
||||
onClick={() => setIsCreating(true)}
|
||||
className="mt-8 inline-flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-white px-8 py-4 rounded-2xl font-black uppercase tracking-widest text-xs transition-all shadow-lg shadow-amber-200 active:scale-95"
|
||||
>
|
||||
<Plus className="w-5 h-5" /> Tạo ghi chú mới
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -969,6 +969,8 @@ export const TourDetailPage = ({
|
||||
|
||||
// State cho Modal ghi chú nhanh
|
||||
const [quickNoteLocName, setQuickNoteLocName] = useState<string | null>(null);
|
||||
const [quickNoteLegId, setQuickNoteLegId] = useState<string | null>(null);
|
||||
const [quickNoteLocation, setQuickNoteLocation] = useState<any>(null);
|
||||
const [quickNoteInput, setQuickNoteInput] = useState('');
|
||||
|
||||
// Đồng bộ hóa các ô nhập dữ liệu cài đặt với currentTour khi tour tải/cập nhật
|
||||
@@ -1559,62 +1561,76 @@ export const TourDetailPage = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Hàm ghi chú nhanh cho từng địa điểm, tự động append vào note chung của Tour
|
||||
const handleQuickNote = (locationName: string) => {
|
||||
if (isPublicView) return;
|
||||
setQuickNoteLocName(locationName);
|
||||
const reverseGeocode = async (lat: number, lng: number): Promise<string> => {
|
||||
try {
|
||||
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
return data.display_name || '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Reverse geocode failed:', e);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const handleQuickNote = async (data: { legId: string; location: any; leg: any }) => {
|
||||
if (isPublicView || !currentTour) return;
|
||||
setQuickNoteLocName(data.location.name);
|
||||
setQuickNoteLegId(data.legId);
|
||||
setQuickNoteLocation(data.location);
|
||||
setQuickNoteInput('');
|
||||
};
|
||||
|
||||
// Hàm xử lý submit ghi chú nhanh từ modal
|
||||
const submitQuickNote = () => {
|
||||
if (!quickNoteLocName || !quickNoteInput.trim() || !currentTour) return;
|
||||
const submitQuickNote = async () => {
|
||||
if (!quickNoteLocName || !quickNoteInput.trim() || !currentTour || !quickNoteLegId || !quickNoteLocation) return;
|
||||
|
||||
const content = quickNoteInput;
|
||||
|
||||
const storedUser = localStorage.getItem('user');
|
||||
const user = storedUser ? JSON.parse(storedUser) : { name: 'Thành viên' };
|
||||
const userName = user.name || 'Thành viên';
|
||||
const now = new Date().toLocaleString('vi-VN');
|
||||
|
||||
const noteTitle = `Ghi chú của hành trình: ${currentTour.title}`;
|
||||
const savedNotes = localStorage.getItem('my_journey_notes');
|
||||
let notes = [];
|
||||
try {
|
||||
notes = savedNotes ? JSON.parse(savedNotes) : [];
|
||||
} catch (e) { notes = []; }
|
||||
|
||||
let targetNote = notes.find((n: any) => n.tourId === currentTour.id || n.title === noteTitle);
|
||||
|
||||
// Tạo khối nội dung dạng "Textbox" chuyên nghiệp
|
||||
const newContentLine = `
|
||||
<div class="quick-note-box" style="border-left: 4px solid #f59e0b; padding: 12px; margin: 16px 0; background: #fffbeb; border-radius: 8px; border: 1px solid #fef3c7; box-shadow: 0 2px 4px rgba(0,0,0,0.02);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
||||
<span style="font-size: 10px; color: #b45309; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em;">🕒 ${now} • 👤 ${userName}</span>
|
||||
</div>
|
||||
<p style="margin: 0; color: #4b5563; font-size: 14px; line-height: 1.5;"><strong>📍 ${quickNoteLocName}:</strong> ${content}</p>
|
||||
</div>
|
||||
<p></p>
|
||||
`;
|
||||
|
||||
if (targetNote) {
|
||||
targetNote.tourId = currentTour.id;
|
||||
targetNote.title = noteTitle;
|
||||
targetNote.content += newContentLine;
|
||||
} else {
|
||||
const newNote = {
|
||||
id: Date.now().toString(),
|
||||
tourId: currentTour.id,
|
||||
title: noteTitle,
|
||||
content: `<p>Bắt đầu lập kế hoạch cho chuyến đi <strong>${currentTour.title}</strong> của bạn tại đây...</p>` + newContentLine,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
notes.unshift(newNote);
|
||||
let resolvedAddress = quickNoteLocation.address || '';
|
||||
const hasCoords = quickNoteLocation.latitude && quickNoteLocation.longitude;
|
||||
if ((!resolvedAddress || resolvedAddress.trim() === '') && hasCoords) {
|
||||
const geo = await reverseGeocode(+quickNoteLocation.latitude, +quickNoteLocation.longitude);
|
||||
resolvedAddress = geo || `${quickNoteLocation.latitude}, ${quickNoteLocation.longitude}`;
|
||||
}
|
||||
|
||||
localStorage.setItem('my_journey_notes', JSON.stringify(notes));
|
||||
notify({ title: 'Thành công', message: 'Đã lưu vào ghi chú chung!', type: 'success' });
|
||||
setQuickNoteLocName(null);
|
||||
setQuickNoteInput('');
|
||||
const formattedTime = quickNoteLocation.plannedStart
|
||||
? new Date(quickNoteLocation.plannedStart).toLocaleString('vi-VN')
|
||||
: "Thời gian tùy hứng";
|
||||
|
||||
const userInputText = content;
|
||||
const noteSnippet = `<h4>📅 ${formattedTime} | 📍 ${resolvedAddress}</h4><ul><li><strong>Nhật ký:</strong> ${userInputText}</li></ul>`;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const res = await fetch(`/api/v1/tours/${currentTour.id}/notes/insert`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
legId: quickNoteLegId,
|
||||
noteSnippet
|
||||
})
|
||||
});
|
||||
if (res.ok) {
|
||||
notify({ title: 'Thành công', message: 'Đã lưu vào ghi chú chung!', type: 'success' });
|
||||
} else {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
notify({ title: 'Lỗi', message: err.message || 'Lỗi khi lưu ghi chú nhanh.', type: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Lỗi khi lưu ghi chú nhanh:', error);
|
||||
notify({ title: 'Lỗi', message: 'Lỗi mạng khi lưu ghi chú nhanh.', type: 'error' });
|
||||
} finally {
|
||||
setQuickNoteLocName(null);
|
||||
setQuickNoteLegId(null);
|
||||
setQuickNoteLocation(null);
|
||||
setQuickNoteInput('');
|
||||
}
|
||||
};
|
||||
|
||||
// Hàm xử lý xóa Tour vĩnh viễn
|
||||
@@ -2088,7 +2104,7 @@ export const TourDetailPage = ({
|
||||
|
||||
setIsAddLocationOpen(true);
|
||||
}}
|
||||
onQuickNote={(locName: string) => handleQuickNote(locName)}
|
||||
onQuickNote={(data) => handleQuickNote(data)}
|
||||
onSuccess={() => fetchTour(tourId)}
|
||||
isPublicView={isPublicView}
|
||||
onNavigate={handleNavigateToLocation}
|
||||
@@ -2274,13 +2290,16 @@ export const TourDetailPage = ({
|
||||
<div className="text-[10px] text-gray-400 mb-2 uppercase tracking-widest">{loc.type}</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={() => handleQuickNote(loc.name)}
|
||||
className="w-full flex items-center justify-center gap-1.5 py-1.5 bg-amber-50 hover:bg-amber-100 text-amber-600 rounded-lg text-[10px] font-black transition-all border border-amber-100"
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
GHI CHÚ NHANH
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const leg = legs.find((l: any) => l.locations.some((lo: any) => lo.id === loc.id));
|
||||
handleQuickNote({ legId: loc.legId || leg?.id || '', location: loc, leg: leg || {} });
|
||||
}}
|
||||
className="flex items-center justify-center gap-1.5 py-1.5 text-yotripLight-steel hover:text-yotripLight-text dark:text-yotripDark-muted dark:hover:text-yotripDark-lime rounded-lg hover:bg-gray-100 dark:hover:bg-yotripDark-surface-muted transition-colors duration-200 border border-gray-100"
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
GHI CHÚ NHANH
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCommentLocationId(loc.id);
|
||||
|
||||
Reference in New Issue
Block a user