Files
rikkei_simple_care/management/src/components/AppTemplatesSection.tsx
2026-06-30 15:53:01 +07:00

211 lines
6.8 KiB
TypeScript

import { useEffect, useState } from 'react';
import { apiAppTemplates, type AppTemplateItem } from '../api';
import { AppPoolModal } from './AppPoolModal';
import { countKeywords, mergeKeywordCSV } from '../utils/appKeywords';
export function AppTemplatesSection() {
const [items, setItems] = useState<AppTemplateItem[]>([]);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState<AppTemplateItem | null>(null);
const [creating, setCreating] = useState(false);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [keywords, setKeywords] = useState('');
const [busy, setBusy] = useState(false);
const [poolOpen, setPoolOpen] = useState(false);
const [error, setError] = useState('');
const load = async () => {
setLoading(true);
try {
const res = await apiAppTemplates.list();
setItems(res.data || []);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, []);
const openCreate = () => {
setCreating(true);
setEditing(null);
setName('');
setDescription('');
setKeywords('');
setError('');
};
const openEdit = (tpl: AppTemplateItem) => {
setEditing(tpl);
setCreating(false);
setName(tpl.name);
setDescription(tpl.description || '');
setKeywords(tpl.keywords);
setError('');
};
const closeForm = () => {
setCreating(false);
setEditing(null);
setError('');
};
const save = async () => {
if (!name.trim()) {
setError('Nhập tên khung.');
return;
}
if (!keywords.trim()) {
setError('Thêm ít nhất một keyword.');
return;
}
setBusy(true);
setError('');
try {
if (editing) {
await apiAppTemplates.update(editing.id, {
name: name.trim(),
description: description.trim(),
keywords,
});
} else {
await apiAppTemplates.create({
name: name.trim(),
description: description.trim(),
keywords,
});
}
closeForm();
await load();
} catch (e: any) {
setError(e?.message || 'Lỗi lưu khung');
} finally {
setBusy(false);
}
};
const remove = async (tpl: AppTemplateItem) => {
if (!confirm(`Xóa khung "${tpl.name}"?`)) return;
try {
await apiAppTemplates.delete(tpl.id);
if (editing?.id === tpl.id) closeForm();
await load();
} catch (e: any) {
alert(e?.message || 'Không xóa được');
}
};
const addPoolKeyword = (kw: string) => {
setKeywords((prev) => mergeKeywordCSV(prev, kw));
};
const showForm = creating || editing;
return (
<div className="system-section">
<div className="system-block">
<div className="system-block-head">
<div>
<h2 className="system-block-title">Khung ng dụng</h2>
<p className="system-block-desc">
Tạo bộ keyword whitelist dùng chung ghép từ kho app hoặc tự nhập. Lớp học phòng thi thể áp dụng nhanh thay chỉ chọn từng app trong kho.
</p>
</div>
<button type="button" className="btn btn-primary" onClick={openCreate}>
+ Tạo khung
</button>
</div>
{showForm && (
<div className="system-form-card">
<h3 className="system-form-title">{editing ? 'Sửa khung' : 'Khung mới'}</h3>
<div className="system-form-grid">
<label className="login-field">
<span>Tên khung</span>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="VD: Java IDE, Thi cuối kỳ..."
/>
</label>
<label className="login-field system-form-full">
<span> tả (tuỳ chọn)</span>
<input
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Ghi chú ngắn cho giáo viên"
/>
</label>
<label className="login-field system-form-full">
<span>Keywords (phân tách bằng dấu phẩy)</span>
<textarea
className="app-textarea"
rows={4}
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
placeholder="chrome, vscode, cursor, goland, client"
/>
</label>
</div>
<div className="system-form-actions">
<button type="button" className="btn btn-secondary" onClick={() => setPoolOpen(true)}>
Thêm từ kho app
</button>
<div className="system-form-actions-right">
<button type="button" className="btn btn-ghost" onClick={closeForm}>
Hủy
</button>
<button type="button" className="btn btn-primary" onClick={save} disabled={busy}>
{busy ? 'Đang lưu...' : 'Lưu khung'}
</button>
</div>
</div>
{error && <div className="login-error" style={{ marginTop: '0.75rem' }}>{error}</div>}
</div>
)}
{loading ? (
<div className="system-empty">Đang tải...</div>
) : items.length === 0 ? (
<div className="system-empty">
Chưa khung nào. Bấm <strong>Tạo khung</strong> đ ghép keyword từ kho hoặc tự ghi.
</div>
) : (
<div className="template-grid">
{items.map((tpl) => (
<article key={tpl.id} className="template-card">
<div className="template-card-head">
<h3>{tpl.name}</h3>
<span className="template-card-badge">{countKeywords(tpl.keywords)} kw</span>
</div>
{tpl.description && <p className="template-card-desc">{tpl.description}</p>}
<code className="template-card-kw">{tpl.keywords}</code>
<div className="template-card-actions">
<button type="button" className="btn btn-secondary btn-sm" onClick={() => openEdit(tpl)}>
Sửa
</button>
<button type="button" className="btn btn-ghost btn-sm" onClick={() => remove(tpl)}>
Xóa
</button>
</div>
</article>
))}
</div>
)}
</div>
<AppPoolModal
open={poolOpen}
onClose={() => setPoolOpen(false)}
onSelect={addPoolKeyword}
allowedApps={keywords}
/>
</div>
);
}