import React, { useEffect, useMemo, useState } from "react" import { Alert, Button, Card, Form, Input, Pagination, Popconfirm, Select, Space, Switch, Table, Tag, message, } from "antd" import { type ImmutableTree } from "@react-awesome-query-builder/antd" import type { ColumnsType } from "antd/es/table" import { useTranslation } from "react-i18next" import { fetchAllTags } from "./TagEditorDialog" import { ActionEditor } from "./AutoScore/ActionEditor" import { TriggerRuleBuilder } from "./AutoScore/TriggerRuleBuilder" import { actionDraftsToPayload, actionsToDrafts, createDefaultActionDraft, createEmptyTriggerTree, createTriggerQueryConfig, hasUnsupportedTriggerLogic, normalizeActionDrafts, queryTreeToTriggers, triggersToQueryTree, type ActionDraft, type AutoScoreRule, type AutoScoreTagOption, } from "./AutoScore/AutoScoreUtils" interface StudentItem { id: number name: string } interface TagItem { id: number name: string } interface RuleFormValues { name?: string studentNames?: string[] } interface AutoScoreManagerProps { canEdit: boolean } function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element { const { t, i18n } = useTranslation() const [form] = Form.useForm() const [messageApi, contextHolder] = message.useMessage() const [tags, setTags] = useState([]) const tagOptions = useMemo( () => tags.map((tag) => ({ label: tag.name, value: tag.name, })), [tags] ) const triggerConfig = useMemo( () => createTriggerQueryConfig(t, tagOptions), [i18n.resolvedLanguage, i18n.language, tagOptions] ) const [triggerTree, setTriggerTree] = useState(() => createEmptyTriggerTree(triggerConfig) ) const [actionDrafts, setActionDrafts] = useState([createDefaultActionDraft()]) const [students, setStudents] = useState([]) const [rules, setRules] = useState([]) const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [editingRuleId, setEditingRuleId] = useState(null) const [currentPage, setCurrentPage] = useState(1) const [pageSize, setPageSize] = useState(10) useEffect(() => { const maxPage = Math.max(1, Math.ceil(rules.length / pageSize)) if (currentPage > maxPage) { setCurrentPage(maxPage) } }, [currentPage, pageSize, rules.length]) const resetEditor = () => { setEditingRuleId(null) form.setFieldsValue({ name: "", studentNames: [] }) setTriggerTree(createEmptyTriggerTree(triggerConfig)) setActionDrafts([createDefaultActionDraft()]) } const emitDataUpdated = () => { window.dispatchEvent(new CustomEvent("ss:data-updated", { detail: { category: "all" } })) } const fetchStudents = async () => { const api = (window as any).api if (!api || !canEdit) return try { const res = await api.queryStudents({}) if (res.success && Array.isArray(res.data)) { setStudents(res.data) } } catch { void 0 } } const fetchTags = async () => { if (!canEdit) return try { const tagList = await fetchAllTags() if (Array.isArray(tagList)) { setTags(tagList) } } catch { void 0 } } const fetchRules = async () => { const api = (window as any).api if (!api || !canEdit) return setLoading(true) try { const res = await api.autoScoreGetRules() if (res.success && Array.isArray(res.data)) { setRules(res.data) } else { messageApi.error(res.message || t("autoScore.fetchFailed")) } } catch { messageApi.error(t("autoScore.fetchFailed")) } finally { setLoading(false) } } useEffect(() => { if (!canEdit) return fetchTags().catch(() => void 0) fetchStudents().catch(() => void 0) fetchRules().catch(() => void 0) }, [canEdit]) const handleSubmit = async () => { const api = (window as any).api if (!api) return if (!canEdit) { messageApi.error(t("common.readOnly")) return } const values = await form.validateFields() const name = String(values.name || "").trim() const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [] if (!name) { messageApi.warning(t("autoScore.nameRequired")) return } if (hasUnsupportedTriggerLogic(triggerTree, triggerConfig)) { messageApi.warning(t("autoScore.unsupportedLogic")) return } const triggers = queryTreeToTriggers(triggerTree, triggerConfig) if (triggers.length === 0) { messageApi.warning(t("autoScore.triggerRequired")) return } const normalizedActionDrafts = normalizeActionDrafts(actionDrafts) const actionPayload = actionDraftsToPayload(normalizedActionDrafts) if (actionDrafts.length === 0) { setActionDrafts(normalizedActionDrafts) } if (actionPayload.error === "action_required") { messageApi.warning(t("autoScore.actionRequired")) return } if (actionPayload.error === "score_required") { messageApi.warning(t("autoScore.scorePlaceholder")) return } if (actionPayload.error === "tag_required") { messageApi.warning(t("autoScore.tagNamePlaceholder")) return } if (actionPayload.actions.length === 0) { messageApi.warning(t("autoScore.actionRequired")) return } setSaving(true) try { const payload = { name, enabled: true, studentNames, triggers, actions: actionPayload.actions, } const res = editingRuleId === null ? await api.autoScoreAddRule(payload) : await api.autoScoreUpdateRule({ ...payload, id: editingRuleId }) if (res.success) { messageApi.success( editingRuleId === null ? t("autoScore.createSuccess") : t("autoScore.updateSuccess") ) resetEditor() await fetchRules() emitDataUpdated() } else { messageApi.error( res.message || (editingRuleId === null ? t("autoScore.createFailed") : t("autoScore.updateFailed")) ) } } catch { messageApi.error( editingRuleId === null ? t("autoScore.createFailed") : t("autoScore.updateFailed") ) } finally { setSaving(false) } } const handleEdit = (rule: AutoScoreRule) => { setEditingRuleId(rule.id) form.setFieldsValue({ name: rule.name, studentNames: rule.studentNames || [], }) setTriggerTree(triggersToQueryTree(triggerConfig, rule.triggers || [])) setActionDrafts(actionsToDrafts(rule.actions || [])) } const handleDelete = async (ruleId: number) => { const api = (window as any).api if (!api) return if (!canEdit) { messageApi.error(t("common.readOnly")) return } const res = await api.autoScoreDeleteRule(ruleId) if (res.success) { messageApi.success(t("autoScore.deleteSuccess")) if (editingRuleId === ruleId) { resetEditor() } await fetchRules() emitDataUpdated() } else { messageApi.error(res.message || t("autoScore.deleteFailed")) } } const handleToggle = async (ruleId: number, enabled: boolean) => { const api = (window as any).api if (!api) return if (!canEdit) { messageApi.error(t("common.readOnly")) return } const res = await api.autoScoreToggleRule({ ruleId, enabled }) if (res.success) { setRules((prevRules) => prevRules.map((rule) => (rule.id === ruleId ? { ...rule, enabled } : rule)) ) messageApi.success(enabled ? t("autoScore.enabled") : t("autoScore.disabled")) emitDataUpdated() } else { messageApi.error( res.message || (enabled ? t("autoScore.enableFailed") : t("autoScore.disableFailed")) ) } } const formatLastExecuted = (value?: string | null) => { if (!value) return t("autoScore.notExecuted") const date = new Date(value) if (Number.isNaN(date.getTime())) return t("autoScore.invalidTime") return date.toLocaleString() } const columns: ColumnsType = [ { title: t("autoScore.name"), dataIndex: "name", key: "name", ellipsis: true, }, { title: t("autoScore.status"), key: "status", width: 96, render: (_, row) => ( { handleToggle(row.id, checked).catch(() => void 0) }} /> ), }, { title: t("autoScore.applicableStudents"), key: "studentNames", width: 150, ellipsis: true, render: (_, row) => row.studentNames.length > 0 ? ( {t("autoScore.studentCount", { count: row.studentNames.length })} ) : ( {t("autoScore.allStudents")} ), }, { title: t("autoScore.triggers"), key: "triggers", width: 120, render: (_, row) => {t("autoScore.triggerCount", { count: row.triggers.length })}, }, { title: t("autoScore.actions"), key: "actions", width: 120, render: (_, row) => {t("autoScore.actionCount", { count: row.actions.length })}, }, { title: t("autoScore.lastExecuted"), dataIndex: "lastExecuted", key: "lastExecuted", width: 160, ellipsis: true, render: (value: string | null | undefined) => formatLastExecuted(value), }, { title: t("common.operation"), key: "operation", width: 140, render: (_, row) => ( { handleDelete(row.id).catch(() => void 0) }} > ), }, ] const pagedRules = rules.slice((currentPage - 1) * pageSize, currentPage * pageSize) return (
{contextHolder}

{t("autoScore.title")}

{!canEdit && ( )}