forked from hangshuo652/aurak
feat: implement QuestionBank CRUD with pagination and template query
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { KnowledgeGroup, CreateGroupData, UpdateGroupData } from '../types';
|
||||
import { knowledgeGroupService } from '../services/knowledgeGroupService';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useConfirm } from '../contexts/ConfirmContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { Folder, Plus, Edit2, Trash2, X } from 'lucide-react';
|
||||
|
||||
interface GroupManagerProps {
|
||||
groups: KnowledgeGroup[];
|
||||
onGroupsChange: (groups: KnowledgeGroup[]) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_COLORS = [
|
||||
'#3B82F6', '#10B981', '#F59E0B', '#EF4444',
|
||||
'#8B5CF6', '#06B6D4', '#84CC16', '#F97316'
|
||||
];
|
||||
|
||||
export const GroupManager: React.FC<GroupManagerProps> = ({ groups, onGroupsChange }) => {
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [editingGroup, setEditingGroup] = useState<KnowledgeGroup | null>(null);
|
||||
const [formData, setFormData] = useState<CreateGroupData>({
|
||||
name: '',
|
||||
description: '',
|
||||
color: DEFAULT_COLORS[0],
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { showSuccess, showError } = useToast();
|
||||
const { confirm } = useConfirm();
|
||||
const { t } = useLanguage();
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
color: DEFAULT_COLORS[0],
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.name.trim()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const newGroup = await knowledgeGroupService.createGroup(formData);
|
||||
onGroupsChange([...groups, newGroup]);
|
||||
setIsCreateModalOpen(false);
|
||||
resetForm();
|
||||
showSuccess(t('successNoteCreated')); // Note: Should probably have a more specific translation for group
|
||||
} catch (error) {
|
||||
showError(t('createFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!editingGroup || !formData.name.trim()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const updatedGroup = await knowledgeGroupService.updateGroup(editingGroup.id, formData);
|
||||
onGroupsChange(groups.map(g => g.id === editingGroup.id ? updatedGroup : g));
|
||||
setEditingGroup(null);
|
||||
resetForm();
|
||||
showSuccess(t('successNoteUpdated'));
|
||||
} catch (error) {
|
||||
showError(t('updateFailedRetry'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (group: KnowledgeGroup) => {
|
||||
if (!(await confirm(t('confirmDeleteGroup').replace('$1', group.name)))) return;
|
||||
|
||||
try {
|
||||
await knowledgeGroupService.deleteGroup(group.id);
|
||||
onGroupsChange(groups.filter(g => g.id !== group.id));
|
||||
showSuccess(t('successNoteDeleted'));
|
||||
} catch (error) {
|
||||
showError(t('deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const openEditModal = (group: KnowledgeGroup) => {
|
||||
setEditingGroup(group);
|
||||
setFormData({
|
||||
name: group.name,
|
||||
description: group.description || '',
|
||||
color: group.color,
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsCreateModalOpen(false);
|
||||
setEditingGroup(null);
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const isModalOpen = isCreateModalOpen || editingGroup !== null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Group list */}
|
||||
<div className="space-y-2">
|
||||
{groups.map((group) => (
|
||||
<div
|
||||
key={group.id}
|
||||
className="flex items-center justify-between p-3 bg-white rounded-lg border hover:shadow-sm transition-shadow"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className="w-4 h-4 rounded-full"
|
||||
style={{ backgroundColor: group.color }}
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{group.name}</div>
|
||||
{group.description && (
|
||||
<div className="text-sm text-gray-500">{group.description}</div>
|
||||
)}
|
||||
<div className="text-xs text-gray-400">
|
||||
{group.fileCount} files
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => openEditModal(group)}
|
||||
className="p-1 text-gray-400 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(group)}
|
||||
className="p-1 text-gray-400 hover:text-red-600 transition-colors"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Create button */}
|
||||
<button
|
||||
onClick={() => setIsCreateModalOpen(true)}
|
||||
className="w-full flex items-center justify-center p-2 border-2 border-dashed border-gray-300 rounded-lg text-gray-500 hover:border-blue-400 hover:text-blue-600 transition-colors"
|
||||
title={t('createNotebook')}
|
||||
>
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
|
||||
{/* Create/Edit modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{editingGroup ? t('editNotebookTitle') : t('createNotebookTitle')}
|
||||
</h3>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={editingGroup ? handleUpdate : handleCreate} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('name')} *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder={t('namePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('shortDescription')}
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder={t('descPlaceholder')}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Color indicator
|
||||
</label>
|
||||
<div className="flex space-x-2">
|
||||
{DEFAULT_COLORS.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, color })}
|
||||
className={`w-8 h-8 rounded-full border-2 ${formData.color === color ? 'border-gray-400' : 'border-gray-200'
|
||||
}`}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="flex-1 px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
{t('cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !formData.name.trim()}
|
||||
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{loading ? t('saving') : (editingGroup ? t('save') : t('create'))}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user