This commit is contained in:
2026-06-30 15:53:01 +07:00
parent 595aebd8a5
commit 74b540cac2
19 changed files with 1719 additions and 252 deletions

View File

@@ -0,0 +1,131 @@
import React, { useCallback, useEffect, useState } from 'react';
import { apiAppTemplates, type AppTemplateItem } from '../api';
import { countKeywords } from '../utils/appKeywords';
interface AppTemplatePickerModalProps {
open: boolean;
onClose: () => void;
onSelect: (keywords: string) => void;
allowedApps: string;
}
export const AppTemplatePickerModal: React.FC<AppTemplatePickerModalProps> = ({
open,
onClose,
onSelect,
allowedApps,
}) => {
const [items, setItems] = useState<AppTemplateItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState('');
const load = useCallback(async () => {
try {
setLoading(true);
setError(null);
const res = await apiAppTemplates.list();
setItems(res.data || []);
} catch (e: any) {
setItems([]);
setError(e?.message || 'Không tải được khung ứng dụng');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (!open) return;
load();
}, [open, load]);
useEffect(() => {
if (!open) {
setSearch('');
setError(null);
}
}, [open]);
if (!open) return null;
const q = search.trim().toLowerCase();
const filtered = q
? items.filter(
(t) =>
t.name.toLowerCase().includes(q) ||
t.description?.toLowerCase().includes(q) ||
t.keywords.toLowerCase().includes(q),
)
: items;
return (
<div className="modal-overlay app-pool-overlay" onClick={onClose}>
<div className="modal-container app-pool-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<div>
<h2 className="modal-title" style={{ margin: 0 }}>Chọn khung ng dụng</h2>
<p className="app-pool-modal-sub">
Ghép bộ keyword đã lưu vào danh sách ng dụng đưc phép. Quản khung tại Hệ thống Khung ng dụng.
</p>
</div>
<button type="button" className="btn btn-secondary" onClick={onClose}>
Đóng
</button>
</div>
<div className="app-pool-toolbar">
<input
type="search"
className="app-pool-search"
placeholder="Tìm theo tên, mô tả, keyword..."
value={search}
onChange={(e) => setSearch(e.target.value)}
autoFocus
/>
<button type="button" className="btn btn-secondary" onClick={load} disabled={loading}>
{loading ? 'Đang tải...' : 'Làm mới'}
</button>
</div>
<div className="app-pool-body">
{error ? (
<div className="discovered-apps-empty discovered-apps-error">{error}</div>
) : loading && items.length === 0 ? (
<div className="app-pool-status">Đang tải...</div>
) : filtered.length === 0 ? (
<div className="app-pool-status">
{q ? `Không tìm thấy "${search}"` : 'Chưa có khung nào. Tạo tại Hệ thống → Khung ứng dụng.'}
</div>
) : (
<div className="template-picker-list">
{filtered.map((tpl) => (
<div key={tpl.id} className="template-picker-card">
<div className="template-picker-card-head">
<strong>{tpl.name}</strong>
<span className="template-picker-count">{countKeywords(tpl.keywords)} keyword</span>
</div>
{tpl.description && <p className="template-picker-desc">{tpl.description}</p>}
<code className="template-picker-kw">{tpl.keywords}</code>
<button
type="button"
className="btn btn-primary btn-sm template-picker-apply"
onClick={() => {
onSelect(tpl.keywords);
onClose();
}}
>
Áp dụng khung
</button>
</div>
))}
</div>
)}
</div>
<div className="app-pool-footer">
Đang {countKeywords(allowedApps)} keyword trong whitelist hiện tại
</div>
</div>
</div>
);
};