Files
rikkei_simple_care/management/src/components/AppTemplatePickerModal.tsx
Pham Hung c41f465d8a
Some checks failed
Deploy on Master Change / deploy (pull_request) Has been cancelled
tt
2026-07-23 10:15:00 +07:00

213 lines
7.5 KiB
TypeScript

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;
}
type IconProps = { size?: number };
const IconLayers = ({ size = 20 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="m12 2 9 4.5-9 4.5L3 6.5 12 2z" />
<path d="m3 12 9 4.5 9-4.5" />
<path d="m3 17.5 9 4.5 9-4.5" />
</svg>
);
const IconSearch = ({ size = 15 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<circle cx="11" cy="11" r="7" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
);
const IconRefresh = ({ size = 15 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M21 12a9 9 0 1 1-2.6-6.3" />
<polyline points="21 3 21 9 15 9" />
</svg>
);
const IconCheck = ({ size = 14 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M20 6 9 17l-5-5" />
</svg>
);
const IconInbox = ({ size = 32 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
</svg>
);
function keywordList(csv: string): string[] {
return csv.split(',').map((s) => s.trim()).filter(Boolean);
}
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 app-picker-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header app-picker-header">
<div className="app-picker-header-text">
<h2 className="modal-title app-picker-title">
<span className="app-picker-title-icon" aria-hidden>
<IconLayers />
</span>
Chọn khung ng dụng
</h2>
<p className="app-pool-modal-sub">
Ghép bộ keyword đã lưu vào whitelist. Quản khung tại Hệ thống Khung ng dụng.
</p>
</div>
<button type="button" className="btn btn-secondary app-picker-close" onClick={onClose}>
Đóng
</button>
</div>
<div className="app-pool-toolbar">
<div className="app-picker-search-wrap">
<span className="app-picker-search-icon" aria-hidden>
<IconSearch />
</span>
<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
/>
</div>
<button
type="button"
className="btn btn-secondary app-picker-refresh"
onClick={load}
disabled={loading}
>
<IconRefresh />
{loading ? 'Đang tải...' : 'Làm mới'}
</button>
</div>
<div className="app-pool-body">
{error ? (
<div className="app-picker-state app-picker-state--error">{error}</div>
) : loading && items.length === 0 ? (
<div className="app-picker-state">
<div className="sync-spinner" style={{ width: 28, height: 28 }} />
<p>Đang tải khung ng dụng...</p>
</div>
) : filtered.length === 0 ? (
<div className="app-picker-state">
<IconInbox />
<p>
{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.'}
</p>
</div>
) : (
<div className="template-picker-list">
{filtered.map((tpl) => {
const kws = keywordList(tpl.keywords);
const count = countKeywords(tpl.keywords);
return (
<article key={tpl.id} className="template-picker-card">
<div className="template-picker-card-main">
<div className="template-picker-card-head">
<strong className="template-picker-name">{tpl.name}</strong>
<span className="template-picker-count">{count} keyword</span>
</div>
{tpl.description ? (
<p className="template-picker-desc">{tpl.description}</p>
) : null}
<div className="template-picker-chips" title={tpl.keywords}>
{kws.map((kw) => (
<span key={kw} className="template-picker-chip">
{kw}
</span>
))}
</div>
</div>
<button
type="button"
className="btn btn-primary btn-sm template-picker-apply"
onClick={() => {
onSelect(tpl.keywords);
onClose();
}}
>
<IconCheck size={13} />
Áp dụng
</button>
</article>
);
})}
</div>
)}
</div>
<div className="app-pool-footer">
Whitelist hiện tại: <strong>{countKeywords(allowedApps)}</strong> keyword
{filtered.length > 0 ? ` · ${filtered.length} khung` : ''}
</div>
</div>
</div>
);
};