ge-tool/components/CustomPathManagerModal.tsx

402 lines
20 KiB
TypeScript
Raw Permalink Normal View History

2025-12-10 06:41:43 +00:00
import React, { useState, useEffect } from 'react';
interface CustomPath {
ge_id: string;
lang: string;
custom_path: string;
created_at?: string;
updated_at?: string;
}
interface CustomPathManagerModalProps {
isOpen: boolean;
onClose: () => void;
onPathUpdated?: () => void; // Callback khi có thay đổi
}
const CustomPathManagerModal: React.FC<CustomPathManagerModalProps> = ({ isOpen, onClose, onPathUpdated }) => {
const [searchTerm, setSearchTerm] = useState('');
const [allPaths, setAllPaths] = useState<CustomPath[]>([]);
const [filteredPaths, setFilteredPaths] = useState<CustomPath[]>([]);
const [visibleCount, setVisibleCount] = useState(10);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [editingPath, setEditingPath] = useState<CustomPath | null>(null);
const [editForm, setEditForm] = useState({ ge_id: '', lang: '', custom_path: '' });
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
loadPaths();
} else {
document.body.style.overflow = 'auto';
}
return () => {
document.body.style.overflow = 'auto';
};
}, [isOpen]);
useEffect(() => {
// Filter paths when search term changes
if (searchTerm.trim() === '') {
setFilteredPaths(allPaths.slice(0, visibleCount));
} else {
const filtered = allPaths
.filter(path =>
path.ge_id.toLowerCase().includes(searchTerm.toLowerCase()) ||
path.lang.toLowerCase().includes(searchTerm.toLowerCase()) ||
path.custom_path.toLowerCase().includes(searchTerm.toLowerCase())
)
.slice(0, visibleCount);
setFilteredPaths(filtered);
}
}, [searchTerm, allPaths, visibleCount]);
const loadPaths = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/custom-paths');
const data = await response.json();
if (data.success && Array.isArray(data.custom_paths)) {
setAllPaths(data.custom_paths);
setFilteredPaths(data.custom_paths.slice(0, 10));
} else {
setError('Không thể tải danh sách custom paths');
}
} catch (err) {
console.error('Error loading custom paths:', err);
setError('Lỗi khi tải danh sách custom paths');
} finally {
setIsLoading(false);
}
};
if (!isOpen) {
return null;
}
const handleSearch = () => {
setVisibleCount(10);
const filtered = allPaths
.filter(path =>
path.ge_id.toLowerCase().includes(searchTerm.toLowerCase()) ||
path.lang.toLowerCase().includes(searchTerm.toLowerCase()) ||
path.custom_path.toLowerCase().includes(searchTerm.toLowerCase())
)
.slice(0, 10);
setFilteredPaths(filtered);
};
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSearch();
}
};
const handleLoadMore = () => {
const newCount = visibleCount + 10;
setVisibleCount(newCount);
if (searchTerm.trim() === '') {
setFilteredPaths(allPaths.slice(0, newCount));
} else {
const filtered = allPaths
.filter(path =>
path.ge_id.toLowerCase().includes(searchTerm.toLowerCase()) ||
path.lang.toLowerCase().includes(searchTerm.toLowerCase()) ||
path.custom_path.toLowerCase().includes(searchTerm.toLowerCase())
)
.slice(0, newCount);
setFilteredPaths(filtered);
}
};
const handleEdit = (path: CustomPath) => {
setEditingPath(path);
setEditForm({
ge_id: path.ge_id,
lang: path.lang,
custom_path: path.custom_path
});
};
const handleCancelEdit = () => {
setEditingPath(null);
setEditForm({ ge_id: '', lang: '', custom_path: '' });
};
const handleSaveEdit = async () => {
if (!editForm.ge_id || !editForm.lang || !editForm.custom_path) {
setError('Vui lòng điền đầy đủ thông tin');
return;
}
setIsLoading(true);
setError(null);
try {
// Delete old record if ge_id changed
if (editingPath && editingPath.ge_id !== editForm.ge_id) {
await fetch(`/api/custom-paths/${editingPath.ge_id}`, {
method: 'DELETE'
});
}
// Create/update new record
const response = await fetch('/api/custom-paths', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(editForm)
});
const data = await response.json();
if (data.success) {
await loadPaths();
handleCancelEdit();
if (onPathUpdated) onPathUpdated();
} else {
setError(data.message || 'Không thể lưu custom path');
}
} catch (err) {
console.error('Error saving custom path:', err);
setError('Lỗi khi lưu custom path');
} finally {
setIsLoading(false);
}
};
const handleDeleteClick = (geId: string) => {
setDeleteTarget(geId);
setShowDeleteConfirm(true);
};
const handleConfirmDelete = async () => {
if (!deleteTarget) return;
setIsLoading(true);
setError(null);
try {
const response = await fetch(`/api/custom-paths/${deleteTarget}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
await loadPaths();
setShowDeleteConfirm(false);
setDeleteTarget(null);
if (onPathUpdated) onPathUpdated();
} else {
setError(data.message || 'Không thể xóa custom path');
}
} catch (err) {
console.error('Error deleting custom path:', err);
setError('Lỗi khi xóa custom path');
} finally {
setIsLoading(false);
}
};
const handleCancelDelete = () => {
setShowDeleteConfirm(false);
setDeleteTarget(null);
};
return (
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-slate-800 rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col border border-slate-700">
{/* Header */}
<div className="flex justify-between items-center p-6 border-b border-slate-700">
<h2 className="text-2xl font-bold text-white">Quản Custom Paths</h2>
<button
onClick={onClose}
className="text-slate-400 hover:text-white transition-colors p-2 hover:bg-slate-700 rounded-lg"
>
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
<path fillRule="evenodd" d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z" clipRule="evenodd" />
</svg>
</button>
</div>
{/* Search Bar */}
<div className="p-6 border-b border-slate-700">
<div className="flex gap-3">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={handleInputKeyDown}
placeholder="Tìm kiếm GE ID, Lang, hoặc Path..."
className="flex-1 bg-slate-900/50 border border-slate-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 px-4 py-2.5"
/>
<button
onClick={handleSearch}
className="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors"
>
Tìm kiếm
</button>
</div>
</div>
{/* Error Message */}
{error && (
<div className="mx-6 mt-4 p-4 bg-red-900/20 border border-red-500/50 rounded-lg">
<p className="text-red-400 text-sm">{error}</p>
</div>
)}
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
{isLoading && filteredPaths.length === 0 ? (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-500"></div>
</div>
) : filteredPaths.length === 0 ? (
<div className="text-center py-12">
<p className="text-slate-400">Không tìm thấy custom path nào</p>
</div>
) : (
<div className="space-y-3">
{filteredPaths.map((path) => (
<div
key={path.ge_id}
className="bg-slate-900/50 border border-slate-700 rounded-lg p-4 hover:border-slate-600 transition-colors"
>
{editingPath?.ge_id === path.ge_id ? (
/* Edit Mode */
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<input
type="text"
value={editForm.ge_id}
onChange={(e) => setEditForm({ ...editForm, ge_id: e.target.value })}
placeholder="GE ID"
className="bg-slate-800 border border-slate-600 text-white text-sm rounded-lg px-3 py-2"
/>
<input
type="text"
value={editForm.lang}
onChange={(e) => setEditForm({ ...editForm, lang: e.target.value.toUpperCase() })}
placeholder="Lang"
className="bg-slate-800 border border-slate-600 text-white text-sm rounded-lg px-3 py-2"
/>
<input
type="text"
value={editForm.custom_path}
onChange={(e) => setEditForm({ ...editForm, custom_path: e.target.value })}
placeholder="Custom Path"
className="bg-slate-800 border border-slate-600 text-white text-sm rounded-lg px-3 py-2"
/>
</div>
<div className="flex gap-2">
<button
onClick={handleSaveEdit}
disabled={isLoading}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white text-sm rounded-lg disabled:opacity-50"
>
Lưu
</button>
<button
onClick={handleCancelEdit}
className="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white text-sm rounded-lg"
>
Hủy
</button>
</div>
</div>
) : (
/* View Mode */
<div className="flex items-center justify-between">
<div className="flex-1 space-y-1">
<div className="flex items-center gap-3">
<span className="text-white font-semibold">{path.ge_id}</span>
<span className="px-2 py-0.5 bg-indigo-600/20 text-indigo-300 text-xs rounded">
{path.lang}
</span>
</div>
<p className="text-slate-300 text-sm font-mono">{path.custom_path}</p>
</div>
<div className="flex gap-2">
<button
onClick={() => handleEdit(path)}
className="p-2 text-indigo-400 hover:text-indigo-300 hover:bg-slate-800 rounded-lg transition-colors"
title="Sửa"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32l8.4-8.4Z" />
<path d="M5.25 5.25a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3V13.5a.75.75 0 0 0-1.5 0v5.25a1.5 1.5 0 0 1-1.5 1.5H5.25a1.5 1.5 0 0 1-1.5-1.5V8.25a1.5 1.5 0 0 1 1.5-1.5h5.25a.75.75 0 0 0 0-1.5H5.25Z" />
</svg>
</button>
<button
onClick={() => handleDeleteClick(path.ge_id)}
className="p-2 text-red-400 hover:text-red-300 hover:bg-slate-800 rounded-lg transition-colors"
title="Xóa"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path fillRule="evenodd" d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
)}
</div>
))}
</div>
)}
{/* Load More Button */}
{filteredPaths.length < allPaths.length && (
<div className="mt-6 text-center">
<button
onClick={handleLoadMore}
className="px-6 py-2.5 bg-slate-700 hover:bg-slate-600 text-white font-medium rounded-lg transition-colors"
>
Tải thêm
</button>
</div>
)}
</div>
{/* Footer Info */}
<div className="p-4 border-t border-slate-700 bg-slate-900/50">
<p className="text-slate-400 text-sm text-center">
Tổng số: <span className="text-white font-semibold">{allPaths.length}</span> custom paths
</p>
</div>
</div>
{/* Delete Confirmation Modal */}
{showDeleteConfirm && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-[60]">
<div className="bg-slate-800 rounded-xl shadow-2xl p-6 max-w-md w-full mx-4 border border-slate-700">
<h3 className="text-xl font-bold text-white mb-4">Xác nhận xóa</h3>
<p className="text-slate-300 mb-6">
Bạn chắc chắn muốn xóa custom path cho <span className="text-white font-semibold">{deleteTarget}</span>?
</p>
<div className="flex gap-3">
<button
onClick={handleConfirmDelete}
disabled={isLoading}
className="flex-1 px-4 py-2.5 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg disabled:opacity-50"
>
Xóa
</button>
<button
onClick={handleCancelDelete}
className="flex-1 px-4 py-2.5 bg-slate-700 hover:bg-slate-600 text-white font-medium rounded-lg"
>
Hủy
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default CustomPathManagerModal;