v0.5测试
This commit is contained in:
59
.cospec/plan/changes/fix-admin-multi-issues/proposal.md
Normal file
59
.cospec/plan/changes/fix-admin-multi-issues/proposal.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 变更:管理端多项功能修复与改版
|
||||
|
||||
## 原因
|
||||
管理端存在多项问题需要修复:跨域配置导致API调用失败、导航栏各页面不一致、作业模块用途不符需求、考勤模块交互需改版、家长手机号需要权限控制。
|
||||
|
||||
## 变更内容
|
||||
|
||||
### 1. 跨域问题修复(确认)
|
||||
- 之前的 500 错误已通过添加 `PermissionChecker.get_user_class_id` 方法修复
|
||||
- CORS 中间件配置本身正确(`CORSMiddleware` 已注册且包含 OPTIONS 方法)
|
||||
- `AuthMiddleware` 虽已导入但未在 `main.py` 中注册为全局中间件,不影响当前功能(认证由路由层 `get_current_user` 处理)
|
||||
- 后端 `.env` 文件由服务器单独配置,不在版本控制中,无需添加默认值
|
||||
|
||||
### 2. 统一导航栏
|
||||
- 创建 `frontend/includes/nav.php` 共享模板,所有 admin 页面统一引入
|
||||
- 导航项角色条件统一为:操行分管理=班主任/班长,作业管理=班主任/学习委员,考勤管理=班主任/考勤委员,科目管理=班主任/学习委员,管理员管理=班主任
|
||||
- 修复 `dashboard.php` 密码链接拼写错误 `passwork.php` → `password.php`
|
||||
- 接受参数:`$role`、`$current_page`(用于 active 状态)
|
||||
|
||||
### 3. 作业管理改版(仅加减操行分)
|
||||
- 移除作业发布功能(创建作业、查看提交记录)
|
||||
- 改为类似操行分管理页面的交互:选择学生 + 选择扣分类型(未交作业/迟交作业等)+ 原因 + 提交扣分
|
||||
- 前端 `homework.php` 完全重写:学生列表表格 + 批量加减分模态框,扣分类型为作业相关(未交-2分、迟交-1分)
|
||||
- 后端保留 `add_conduct_points` 接口(已有),通过 `related_type="homework"` 关联
|
||||
- 角色权限不变:班主任 + 学习委员
|
||||
|
||||
### 4. 考勤管理改版(学生方格扣分制)
|
||||
- 前端 `attendance.php` 重写UI:
|
||||
- 顶部日期选择器
|
||||
- 主体区域:学生方格网格(flex-wrap 布局,每行7个)
|
||||
- 每个方格显示学生姓名,点击可选中(高亮变红表示缺勤扣分)
|
||||
- 底部工具栏:全选/取消、批量选择状态(缺勤/迟到/请假)、提交按钮
|
||||
- 下方保留历史记录表格
|
||||
- 后端考勤逻辑不变,新增批量提交支持(或前端循环调用现有单个添加接口)
|
||||
- 扣分规则不变:缺勤-5分、迟到-2分、请假-1分
|
||||
|
||||
### 5. 家长手机号权限控制
|
||||
- `frontend/admin/students.php` 表格中 `家长手机号` 列仅 `班主任` 角色可见
|
||||
- 非班主任角色显示为 `***` 或隐藏该列
|
||||
- 新增/导入学生表单中的手机号字段保持不变(任何有学生管理权限的角色都能录入)
|
||||
|
||||
### 6. 附带修复
|
||||
- 修复 `dashboard.php` 密码链接 `passwork.php` → `password.php`(如果导航模板方案未覆盖的话)
|
||||
- 统一所有角色判断为 `学习委员`(当前 homework.php 用 `科代表`)
|
||||
|
||||
## 影响
|
||||
- **受影响的规范**:管理端导航、作业管理、考勤管理、学生管理
|
||||
- **受影响的代码**:
|
||||
- `frontend/includes/nav.php`: 新建 - 统一导航栏模板
|
||||
- `frontend/admin/dashboard.php`: 移除硬编码导航栏,引入 nav.php
|
||||
- `frontend/admin/students.php`: 移除硬编码导航栏,引入 nav.php;手机号列增加角色判断
|
||||
- `frontend/admin/conduct.php`: 移除硬编码导航栏,引入 nav.php
|
||||
- `frontend/admin/homework.php`: 完全重写 - 改为加减操行分模式
|
||||
- `frontend/admin/attendance.php`: 完全重写 - 改为学生方格扣分制
|
||||
- `frontend/admin/history.php`: 移除硬编码导航栏,引入 nav.php
|
||||
- `frontend/admin/subjects.php`: 移除硬编码导航栏,引入 nav.php
|
||||
- `frontend/admin/admins.php`: 移除硬编码导航栏,引入 nav.php
|
||||
- `frontend/admin/password.php`: 移除硬编码导航栏,引入 nav.php
|
||||
- `frontend/assets/css/admin.css`: 新增学生方格网格样式
|
||||
156
.cospec/plan/changes/fix-admin-multi-issues/task.md
Normal file
156
.cospec/plan/changes/fix-admin-multi-issues/task.md
Normal file
@@ -0,0 +1,156 @@
|
||||
## 实施
|
||||
|
||||
### 阶段 1:统一导航栏
|
||||
|
||||
- [x] 1.1 创建统一导航栏模板
|
||||
【目标对象】`frontend/includes/nav.php`(新建)
|
||||
【修改目的】将所有 admin 页面硬编码的导航栏抽取为共享模板,解决各页面导航不一致的问题;同时修复 dashboard.php 中密码链接 `passwork.php` 的拼写错误
|
||||
【修改方式】新建 PHP 文件,定义导航栏 HTML 结构,直接读取 `$role` 和 `$current_page` 变量(已由 `header.php` 第 17-19 行定义)来动态生成导航项和 active 状态
|
||||
【相关依赖】`frontend/includes/header.php`(第 17-19 行已定义 `$current_page`、`$user_type`、`$role` 变量,在 include nav.php 之前已可用)
|
||||
【修改内容】
|
||||
- 直接使用 `$role` 和 `$current_page` 变量(无需参数传递,因为 header.php 在 nav.php 之前被 include)
|
||||
- 导航项及角色条件统一为:
|
||||
- 首页(dashboard):所有管理员可见
|
||||
- 学生管理(students):所有管理员可见
|
||||
- 操行分管理(conduct):$role==='班主任' || $role==='班长'
|
||||
- 作业管理(homework):$role==='班主任' || $role==='学习委员'
|
||||
- 考勤管理(attendance):$role==='班主任' || $role==='考勤委员'
|
||||
- 科目管理(subjects):$role==='班主任' || $role==='学习委员'
|
||||
- 管理员管理(admins):$role==='班主任'
|
||||
- 历史记录(history):所有管理员可见
|
||||
- 修改密码(password):所有管理员可见
|
||||
- 根据 `$current_page`(值为不含 `.php` 后缀的文件名,如 `dashboard`、`students`)为当前页面对应的导航项添加 `active` class
|
||||
- 密码链接统一写为 `password.php`(修复 dashboard 中 `passwork.php` 拼写错误)
|
||||
- 导航栏外层容器沿用现有 `<div class="nav">` 结构
|
||||
|
||||
- [x] 1.2 各 admin 页面引入统一导航模板
|
||||
【目标对象】所有 `frontend/admin/*.php` 页面
|
||||
【修改目的】移除各页面硬编码的 `<div class="nav">...</div>` 块,替换为 `include nav.php`,统一导航栏
|
||||
【修改方式】在以下每个页面中,找到 `<div class="nav">` 到对应的 `</div>` 之间的导航栏块,整块替换为 `<?php include __DIR__ . '/../includes/nav.php'; ?>`
|
||||
【相关依赖】`frontend/includes/nav.php`(任务 1.1 创建)
|
||||
【修改内容】
|
||||
- `frontend/admin/dashboard.php`:替换第 25-43 行的 `<div class="nav">...</div>` 为 include nav.php
|
||||
- `frontend/admin/students.php`:替换第 25-43 行的 `<div class="nav">...</div>` 为 include nav.php
|
||||
- `frontend/admin/conduct.php`:替换第 31-47 行的 `<div class="nav">...</div>` 为 include nav.php
|
||||
- `frontend/admin/homework.php`:替换第 31-47 行的 `<div class="nav">...</div>` 为 include nav.php(注意:此页面后续会在任务 2.2 完全重写,此处仅替换导航栏部分)
|
||||
- `frontend/admin/attendance.php`:替换第 31-47 行的 `<div class="nav">...</div>` 为 include nav.php(注意:此页面后续会在任务 3.3 完全重写,此处仅替换导航栏部分)
|
||||
- `frontend/admin/history.php`:替换第 25-43 行的 `<div class="nav">...</div>` 为 include nav.php
|
||||
- `frontend/admin/subjects.php`:替换第 30-48 行的 `<div class="nav">...</div>` 为 include nav.php
|
||||
- `frontend/admin/admins.php`:替换第 30-46 行的 `<div class="nav">...</div>` 为 include nav.php
|
||||
- `frontend/admin/password.php`:替换第 25-43 行的 `<div class="nav">...</div>` 为 include nav.php
|
||||
- 替换后需确认 include 语句位于 `include header.php;` 之后、页面主体内容之前
|
||||
|
||||
### 阶段 2:作业管理改版
|
||||
|
||||
- [x] 2.1 清理 `admin.js` 中废弃的作业管理函数
|
||||
【目标对象】`frontend/assets/js/admin.js`
|
||||
【修改目的】移除作业管理改版后不再使用的旧作业管理函数,避免全局函数污染和潜在冲突
|
||||
【修改方式】删除第 174-220 行的三个函数:`showAddAssignmentModal()`、`loadSubjectsForSelect()`、`submitAddAssignment()`
|
||||
【相关依赖】无(homework.php 将在任务 2.2 完全重写,不再依赖这些全局函数)
|
||||
【修改内容】
|
||||
- 删除第 174-178 行的 `showAddAssignmentModal()` 函数
|
||||
- 删除第 180-192 行的 `loadSubjectsForSelect()` 函数
|
||||
- 删除第 194-220 行的 `submitAddAssignment()` 函数
|
||||
- 注意:`admin.js` 中其他通用函数(如 `submitBatchPoints()`、`closeModal()`、`escapeHtml()`、`toggleSelectAll()`)保持不变,仍被 conduct.php 和 students.php 使用
|
||||
|
||||
- [x] 2.2 重写作业管理前端页面
|
||||
【目标对象】`frontend/admin/homework.php`
|
||||
【修改目的】将作业发布/提交管理模式改为单纯的加减操行分模式,交互方式参照 `conduct.php`
|
||||
【修改方式】完全重写页面 HTML 内容和 `<script>` 中的 JavaScript 逻辑(保留文件头部的 PHP 鉴权部分和 include 结构)
|
||||
【相关依赖】
|
||||
- `frontend/includes/nav.php`(任务 1.1,导航栏引入)
|
||||
- `frontend/assets/js/admin.js`(提供 `submitBatchPoints()`、`showBatchPointsModal()`、`closeModal()`、`escapeHtml()`、`toggleSelectAll()` 等全局复用函数)
|
||||
- 后端 API `GET /api/admin/students`(加载学生列表,参见 `conduct.php` 第 77-95 行的 `loadStudents()` 调用方式)
|
||||
- 后端 API `POST /api/admin/conduct/add`(加减分接口,请求体:`{ student_ids: number[], points_change: number, reason: string }`)
|
||||
【修改内容】
|
||||
- 角色权限判断(第 23 行)统一为 `in_array($role, ['班主任', '学习委员'])`(之前是 `科代表`,与后端 `admin.py` 第 228 行的角色检查一致)
|
||||
- 移除发布作业按钮和发布作业模态框
|
||||
- 移除作业列表和提交记录展示
|
||||
- 新增学生列表表格(列:复选框、学号、姓名、当前操行分、操作-加减分按钮),参照 `conduct.php` 第 57-71 行的表格结构
|
||||
- 新增批量加减分模态框(复用 `conduct.php` 第 116-143 行和 `students.php` 第 214-241 行的 `batchPointsModal` 模态框结构),包含:
|
||||
- 已选学生数量显示
|
||||
- 扣分类型快捷选择:未交作业(-2分)、迟交作业(-1分)、自定义(选择后自动填入分数变动输入框)
|
||||
- 分数变动输入框(支持正数加分、负数扣分)
|
||||
- 原因输入框(textarea)
|
||||
- 确认提交按钮
|
||||
- 页面级 JS 中定义 `let selectedStudentIds = [];` 和 `loadStudents()`、`showSinglePointsModal()` 函数(与 conduct.php 和 students.php 的页面级变量风格一致)
|
||||
- 提交时复用 `admin.js` 中已有的 `submitBatchPoints()` 函数(该函数调用 `POST /api/admin/conduct/add`),`reason` 字段格式如 `"[作业扣分] 未交作业 - 原因内容"` 以区分来源
|
||||
- 错误处理遵循仓库既有风格:调用 `apiPost()` 后检查 `res.success`,失败时使用 `showToast(res.message || '操作失败', 'error')` 提示(参见 `admin.js` 第 57-64 行的 `submitBatchPoints` 错误处理模式)
|
||||
- 加载学生列表失败时,在表格区域显示错误提示文本
|
||||
|
||||
### 阶段 3:考勤管理改版
|
||||
|
||||
- [x] 3.1 清理 `admin.js` 中废弃的考勤管理函数
|
||||
【目标对象】`frontend/assets/js/admin.js`
|
||||
【修改目的】移除考勤管理改版后不再使用的旧考勤添加函数,避免全局函数污染和潜在冲突
|
||||
【修改方式】删除原始文件第 222-268 行的三个函数(因任务 2.1 已删除第 174-220 行,实际操作时行号已前移约 47 行,请按函数名定位):`showAddAttendanceModal()`、`loadStudentsForSelect()`、`submitAddAttendance()`
|
||||
【相关依赖】任务 2.1(先删除作业相关函数,行号会偏移)
|
||||
【修改内容】
|
||||
- 删除 `showAddAttendanceModal()` 函数(原始第 222-227 行)
|
||||
- 删除 `loadStudentsForSelect()` 函数(原始第 229-238 行)
|
||||
- 删除 `submitAddAttendance()` 函数(原始第 241-268 行)
|
||||
- 建议按函数名搜索定位删除,而非依赖行号
|
||||
- 注意:attendance.php 页面重写后,所有考勤相关 JS 逻辑将在页面级 `<script>` 中定义,不依赖 `admin.js` 中的旧函数
|
||||
|
||||
- [x] 3.2 新增考勤方格网格 CSS 样式
|
||||
【目标对象】`frontend/assets/css/admin.css`
|
||||
【修改目的】为考勤页面的学生方格网格提供样式
|
||||
【修改方式】在文件末尾追加新样式规则
|
||||
【相关依赖】无
|
||||
【修改内容】
|
||||
- `.student-grid` 容器:`display: flex; flex-wrap: wrap; gap: 10px;` 布局
|
||||
- `.student-cell` 方格:`width: calc(100% / 7 - 10px);`(每行 7 个),带边框、圆角、居中文字、内边距、cursor: pointer
|
||||
- `.student-cell.selected` 选中状态:红色/粉色背景高亮(如 `background: #fee2e2; border-color: #ef4444;`)
|
||||
- `.student-cell.has-record` 已有考勤记录标记:灰色虚线边框(如 `border: 2px dashed #9ca3af;`),用于标识当天已有考勤记录的学生
|
||||
- `.student-cell:hover` 悬停效果:浅色背景变化
|
||||
- `.attendance-toolbar` 工具栏样式:flex 布局,按钮间距
|
||||
- 移动端响应式:小屏幕下 `.student-cell` 宽度调整为 `calc(100% / 4 - 10px)` 或更宽
|
||||
|
||||
- [x] 3.3 重写考勤管理前端页面
|
||||
【目标对象】`frontend/admin/attendance.php`
|
||||
【修改目的】将单个学生下拉选择模式改为学生方格网格模式(一行 7 个),选中扣分制
|
||||
【修改方式】完全重写页面 UI 和 `<script>` 中的 JavaScript 逻辑(保留文件头部的 PHP 鉴权部分和 include 结构)
|
||||
【相关依赖】
|
||||
- `frontend/includes/nav.php`(任务 1.1,导航栏引入)
|
||||
- `frontend/assets/css/admin.css`(任务 3.2,方格网格样式)
|
||||
- 后端 API `GET /api/admin/students`(加载学生列表)
|
||||
- 后端 API `POST /api/admin/attendance`(添加考勤记录,请求体:`{ student_id: number, date: string, status: string, reason?: string, apply_deduction: boolean }`)
|
||||
- 后端 API `GET /api/admin/attendance/records`(查询考勤记录,参数:`{ date: string }`)
|
||||
【修改内容】
|
||||
- 页面顶部工具栏:日期选择器(默认当天)+ 考勤状态选择(缺勤/迟到/请假单选按钮组)+ 原因输入框
|
||||
- 主体区域:加载所有学生,以方格网格展示(使用 `.student-grid` 容器和 `.student-cell` 方格)
|
||||
- 每个方格显示学生姓名,点击切换选中/取消状态(添加/移除 `.selected` class)
|
||||
- 底部工具栏:全选按钮 + 取消全选按钮 + 提交按钮
|
||||
- 提交逻辑:遍历所有选中的学生,对每个学生调用 `POST /api/admin/attendance`,参数为 `{ student_id, date, status, reason, apply_deduction: true }`
|
||||
- 提交过程中的错误处理:使用 `Promise.allSettled()` 并发提交,提交完成后汇总成功/失败数量,使用 `showToast()` 提示结果(与仓库既有风格一致)
|
||||
- 重复考勤记录边界处理:后端 `AttendanceService.add_attendance` 不检查同一学生同一天是否已有考勤记录,允许重复提交。前端在提交前先调用 `GET /api/admin/attendance/records` 获取当天已有记录,对已有考勤记录的学生在方格上添加视觉标记(如 `.has-record` 样式,灰色虚线边框),并在提交时 `confirm()` 提示"以下学生已有考勤记录,是否继续提交?"
|
||||
- 下方保留历史考勤记录表格:调用 `GET /api/admin/attendance/records` 查询指定日期的记录并渲染(参照现有 `attendance.php` 第 112-131 行的渲染逻辑)
|
||||
- 移除原有的单个学生下拉选择添加考勤模态框
|
||||
- 加载学生列表失败时,在网格区域显示错误提示文本
|
||||
- 错误提示风格遵循仓库既有模式:使用 `showToast(message, 'error')` 函数
|
||||
|
||||
### 阶段 4:家长手机号权限控制
|
||||
|
||||
- [x] 4.1 家长手机号权限控制
|
||||
|
||||
### 阶段 5:扣分规则配置化
|
||||
|
||||
- [x] 5.1 作业和考勤扣分规则配置化
|
||||
【目标对象】`frontend/.env.example`、`frontend/config.php`、`frontend/includes/header.php`、`frontend/admin/homework.php`、`frontend/admin/attendance.php`
|
||||
【修改目的】将作业和考勤的默认扣分量从 .env 文件配置,前端快捷按钮动态读取配置值,作业加减分有上限限制
|
||||
【修改方式】
|
||||
- .env.example 新增6个扣分配置项(有默认值)
|
||||
- config.php 读取并 define 为常量
|
||||
- header.php 注入到 JS 全局变量
|
||||
- homework.php 快捷按钮使用配置值 + 自定义输入 + ±HOMEWORK_MAX_POINTS 限制
|
||||
- attendance.php 状态按钮使用配置值
|
||||
【目标对象】`frontend/admin/students.php`
|
||||
【修改目的】除班主任角色外,隐藏家长手机号列的显示内容,保护隐私
|
||||
【修改方式】在表头 HTML 和 JS 渲染处添加 `$role` 判断
|
||||
【相关依赖】`$_SESSION['role']`(已存储在 `$role` 变量中,由各页面顶部从 session 读取)
|
||||
【修改内容】
|
||||
- 第 68 行表头处:`<th>家长手机号</th>` 改为 `<?php if ($role === '班主任'): ?><th>家长手机号</th><?php endif; ?>`(表头列有条件显示)
|
||||
- 在页面 `<script>` 标签开头注入角色信息:`const userRole = '<?php echo $role; ?>';`
|
||||
- 第 148 行 JS 渲染处:`<td>${student.parent_phone || '-'}</td>` 改为根据 `userRole` 判断渲染内容:班主任显示真实手机号,其他角色显示 `***`
|
||||
- 第 156 行空数据提示处:`colspan="6"` 需根据角色动态调整——班主任 6 列,非班主任 5 列(可用 PHP 输出:`colspan="<?php echo $role === '班主任' ? '6' : '5'; ?>"`)
|
||||
- 新增学生表单(第 101-129 行)和导入学生功能中的手机号字段保持不变,任何有学生管理权限的角色都能录入(与 proposal 一致)
|
||||
@@ -32,3 +32,21 @@ SESSION_TIMEOUT=30
|
||||
ICP_ENABLED=false
|
||||
# ICP备案号
|
||||
ICP_NUMBER=京ICP备1234567890号-x
|
||||
|
||||
# ===========================================
|
||||
# 扣分规则配置
|
||||
# ===========================================
|
||||
|
||||
# 作业-未交扣分
|
||||
DEDUCTION_HOMEWORK_NOT_SUBMIT=2
|
||||
# 作业-迟交扣分
|
||||
DEDUCTION_HOMEWORK_LATE=1
|
||||
# 作业-每次加减分上限(绝对值)
|
||||
HOMEWORK_MAX_POINTS=3
|
||||
|
||||
# 考勤-缺勤扣分
|
||||
DEDUCTION_ATTENDANCE_ABSENT=5
|
||||
# 考勤-迟到扣分
|
||||
DEDUCTION_ATTENDANCE_LATE=2
|
||||
# 考勤-请假扣分
|
||||
DEDUCTION_ATTENDANCE_LEAVE=1
|
||||
@@ -27,23 +27,7 @@ $page_title = '管理员管理';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/admins.php" class="nav-item active">管理员管理</a>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
|
||||
@@ -28,34 +28,40 @@ if (!in_array($role, ['班主任', '考勤委员'])) {
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/attendance.php" class="nav-item active">考勤管理</a>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<!-- 考勤操作工具栏 -->
|
||||
<div class="card">
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-primary" onclick="showAddAttendanceModal()">添加考勤</button>
|
||||
<div class="search-bar">
|
||||
<div class="attendance-toolbar">
|
||||
<div class="form-group" style="margin:0">
|
||||
<label>日期</label>
|
||||
<input type="date" id="attendanceDate" value="<?php echo date('Y-m-d'); ?>">
|
||||
<button class="btn btn-primary" onclick="loadAttendanceRecords()">查询</button>
|
||||
</div>
|
||||
<div class="status-group">
|
||||
<button class="status-btn active" data-status="absent" onclick="selectStatus(this)">缺勤(-<span class="att-absent"></span>分)</button>
|
||||
<button class="status-btn" data-status="late" onclick="selectStatus(this)">迟到(-<span class="att-late"></span>分)</button>
|
||||
<button class="status-btn" data-status="leave" onclick="selectStatus(this)">请假(-<span class="att-leave"></span>分)</button>
|
||||
</div>
|
||||
<input type="text" id="attendanceReason" placeholder="原因(可选)" style="flex:1;min-width:150px;">
|
||||
<button class="btn btn-primary" onclick="selectAllStudents()">全选</button>
|
||||
<button class="btn" onclick="deselectAllStudents()">取消全选</button>
|
||||
<button class="btn btn-danger" onclick="submitAttendance()">提交考勤</button>
|
||||
<button class="btn btn-secondary" onclick="loadAttendanceRecords()">查询记录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 学生方格网格 -->
|
||||
<div class="card">
|
||||
<div class="card-title">点击选择有考勤异常的学生</div>
|
||||
<div class="student-grid" id="studentGrid">
|
||||
<!-- JS 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史考勤记录 -->
|
||||
<div class="card">
|
||||
<div class="card-title">考勤记录</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -67,54 +73,151 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加考勤模态框 -->
|
||||
<div id="addAttendanceModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>添加考勤记录</h3>
|
||||
<button class="modal-close" onclick="closeModal('addAttendanceModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitAddAttendance()">
|
||||
<div class="form-group">
|
||||
<label>学生</label>
|
||||
<select id="attendanceStudentId" required></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>日期</label>
|
||||
<input type="date" id="attAttendanceDate" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>状态</label>
|
||||
<select id="attendanceStatus" required>
|
||||
<option value="present">出勤</option>
|
||||
<option value="absent">缺勤</option>
|
||||
<option value="late">迟到</option>
|
||||
<option value="leave">请假</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>原因</label>
|
||||
<input type="text" id="attendanceReason" placeholder="缺勤/迟到/请假原因">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><input type="checkbox" id="attendanceDeduct"> 同时扣分</label>
|
||||
<small>扣分规则:缺勤-5分,迟到-2分,请假-1分</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">提交</button>
|
||||
<button type="button" class="btn" onclick="closeModal('addAttendanceModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentStatus = 'absent';
|
||||
let studentsData = [];
|
||||
let existingRecords = [];
|
||||
|
||||
// 初始化考勤扣分配置
|
||||
const attAbsent = window.DEDUCTION_ATTENDANCE_ABSENT || 5;
|
||||
const attLate = window.DEDUCTION_ATTENDANCE_LATE || 2;
|
||||
const attLeave = window.DEDUCTION_ATTENDANCE_LEAVE || 1;
|
||||
|
||||
// 更新页面中的配置值显示
|
||||
document.querySelectorAll('.att-absent').forEach(el => el.textContent = attAbsent);
|
||||
document.querySelectorAll('.att-late').forEach(el => el.textContent = attLate);
|
||||
document.querySelectorAll('.att-leave').forEach(el => el.textContent = attLeave);
|
||||
|
||||
// 选择考勤状态
|
||||
function selectStatus(btn) {
|
||||
document.querySelectorAll('.status-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentStatus = btn.dataset.status;
|
||||
}
|
||||
|
||||
// 加载学生列表
|
||||
async function loadStudents() {
|
||||
const res = await apiGet('/api/admin/students');
|
||||
if (res && res.success) {
|
||||
studentsData = res.data.students;
|
||||
renderStudentGrid();
|
||||
// 同时加载当天已有考勤记录
|
||||
await loadExistingRecords();
|
||||
} else {
|
||||
document.getElementById('studentGrid').innerHTML = '<div style="text-align:center;padding:20px;color:#999;">加载学生列表失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染学生方格
|
||||
function renderStudentGrid() {
|
||||
let html = '';
|
||||
studentsData.forEach(student => {
|
||||
const hasRecord = existingRecords.some(r => r.student_id === student.student_id);
|
||||
html += `<div class="student-cell${hasRecord ? ' has-record' : ''}"
|
||||
data-id="${student.student_id}"
|
||||
data-name="${escapeHtml(student.name)}"
|
||||
onclick="toggleStudent(this)">
|
||||
${escapeHtml(student.name)}
|
||||
</div>`;
|
||||
});
|
||||
if (studentsData.length === 0) {
|
||||
html = '<div style="text-align:center;padding:20px;color:#999;width:100%;">暂无学生数据</div>';
|
||||
}
|
||||
document.getElementById('studentGrid').innerHTML = html;
|
||||
}
|
||||
|
||||
// 切换学生选中状态
|
||||
function toggleStudent(cell) {
|
||||
cell.classList.toggle('selected');
|
||||
}
|
||||
|
||||
// 全选
|
||||
function selectAllStudents() {
|
||||
document.querySelectorAll('.student-cell:not(.has-record)').forEach(cell => {
|
||||
cell.classList.add('selected');
|
||||
});
|
||||
}
|
||||
|
||||
// 取消全选
|
||||
function deselectAllStudents() {
|
||||
document.querySelectorAll('.student-cell').forEach(cell => {
|
||||
cell.classList.remove('selected');
|
||||
});
|
||||
}
|
||||
|
||||
// 加载当天已有考勤记录(用于标记 .has-record)
|
||||
async function loadExistingRecords() {
|
||||
const date = document.getElementById('attendanceDate').value;
|
||||
const res = await apiGet('/api/admin/attendance/records', { date });
|
||||
if (res && res.success) {
|
||||
existingRecords = res.data.records || [];
|
||||
renderStudentGrid(); // 重新渲染以标记 has-record
|
||||
}
|
||||
}
|
||||
|
||||
// 提交考勤
|
||||
async function submitAttendance() {
|
||||
const selectedCells = document.querySelectorAll('.student-cell.selected');
|
||||
if (selectedCells.length === 0) {
|
||||
showToast('请先选择有考勤异常的学生', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const date = document.getElementById('attendanceDate').value;
|
||||
const reason = document.getElementById('attendanceReason').value;
|
||||
|
||||
// 检查是否有已存在记录的学生
|
||||
const hasRecordStudents = [];
|
||||
selectedCells.forEach(cell => {
|
||||
if (cell.classList.contains('has-record')) {
|
||||
hasRecordStudents.push(cell.dataset.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasRecordStudents.length > 0) {
|
||||
const confirmed = confirm(`以下学生已有考勤记录:${hasRecordStudents.join('、')},是否继续提交?`);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
// 批量提交
|
||||
const promises = [];
|
||||
selectedCells.forEach(cell => {
|
||||
const studentId = parseInt(cell.dataset.id);
|
||||
promises.push(
|
||||
apiPost('/api/admin/attendance', {
|
||||
student_id: studentId,
|
||||
date: date,
|
||||
status: currentStatus,
|
||||
reason: reason,
|
||||
apply_deduction: true
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const succeeded = results.filter(r => r.status === 'fulfilled' && r.value?.success).length;
|
||||
const failed = results.length - succeeded;
|
||||
|
||||
if (failed === 0) {
|
||||
showToast(`考勤提交成功(${succeeded}条)`);
|
||||
} else {
|
||||
showToast(`提交完成:成功${succeeded}条,失败${failed}条`, 'error');
|
||||
}
|
||||
|
||||
// 刷新
|
||||
deselectAllStudents();
|
||||
await loadExistingRecords();
|
||||
loadAttendanceRecords();
|
||||
}
|
||||
|
||||
// 查询并渲染考勤记录
|
||||
async function loadAttendanceRecords() {
|
||||
const date = document.getElementById('attendanceDate').value;
|
||||
const res = await apiGet('/api/admin/attendance/records', { date });
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.records.forEach(record => {
|
||||
const records = res.data.records || [];
|
||||
records.forEach(record => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(record.student_no)}</td>
|
||||
<td>${escapeHtml(record.student_name)}</td>
|
||||
@@ -124,59 +227,21 @@ async function loadAttendanceRecords() {
|
||||
<td>${record.deduction_applied ? '已扣分' : '-'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.records.length === 0) {
|
||||
if (records.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;">暂无考勤记录</td></tr>';
|
||||
}
|
||||
document.getElementById('attendanceList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStudentsForSelect() {
|
||||
const res = await apiGet('/api/admin/students');
|
||||
if (res && res.success) {
|
||||
let html = '<option value="">请选择学生</option>';
|
||||
res.data.students.forEach(s => {
|
||||
html += `<option value="${s.student_id}">${escapeHtml(s.student_no)} - ${escapeHtml(s.name)}</option>`;
|
||||
});
|
||||
document.getElementById('attendanceStudentId').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function showAddAttendanceModal() {
|
||||
await loadStudentsForSelect();
|
||||
document.getElementById('addAttendanceModal').style.display = 'flex';
|
||||
document.getElementById('attAttendanceDate').value = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
async function submitAddAttendance() {
|
||||
const studentId = document.getElementById('attendanceStudentId').value;
|
||||
const date = document.getElementById('attAttendanceDate').value;
|
||||
const status = document.getElementById('attendanceStatus').value;
|
||||
const reason = document.getElementById('attendanceReason').value;
|
||||
const applyDeduction = document.getElementById('attendanceDeduct').checked;
|
||||
|
||||
if (!studentId || !date || !status) {
|
||||
showToast('请填写完整信息', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/attendance', {
|
||||
student_id: parseInt(studentId),
|
||||
date: date,
|
||||
status: status,
|
||||
reason: reason,
|
||||
apply_deduction: applyDeduction
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('考勤记录添加成功');
|
||||
closeModal('addAttendanceModal');
|
||||
loadAttendanceRecords();
|
||||
} else {
|
||||
showToast(res?.message || '添加失败', 'error');
|
||||
}
|
||||
}
|
||||
// 日期变化时重新加载
|
||||
document.getElementById('attendanceDate').addEventListener('change', function() {
|
||||
loadExistingRecords();
|
||||
loadAttendanceRecords();
|
||||
});
|
||||
|
||||
// 页面初始化
|
||||
loadStudents();
|
||||
loadAttendanceRecords();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
@@ -28,23 +28,7 @@ if (!in_array($role, ['班主任', '班长'])) {
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<a href="/admin/conduct.php" class="nav-item active">操行分管理</a>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
|
||||
@@ -22,25 +22,7 @@ $role = $_SESSION['role'] ?? '';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item active">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/passwork.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="stats-grid" id="dashboardStats"></div>
|
||||
|
||||
@@ -22,25 +22,7 @@ $role = $_SESSION['role'] ?? '';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item active">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
|
||||
@@ -20,7 +20,7 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
$page_title = '作业管理';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
|
||||
if (!in_array($role, ['班主任', '科代表'])) {
|
||||
if (!in_array($role, ['班主任', '学习委员'])) {
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit();
|
||||
}
|
||||
@@ -28,142 +28,152 @@ if (!in_array($role, ['班主任', '科代表'])) {
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/homework.php" class="nav-item active">作业管理</a>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="action-bar">
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<button class="btn btn-primary" onclick="showAddAssignmentModal()">发布作业</button>
|
||||
<?php endif; ?>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-primary" onclick="showBatchPointsModal()">批量加减分</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
|
||||
<th>学号</th>
|
||||
<th>姓名</th>
|
||||
<th>当前操行分</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="studentList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="assignmentsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发布作业模态框 -->
|
||||
<div id="addAssignmentModal" class="modal">
|
||||
<div id="batchPointsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>发布作业</h3>
|
||||
<button class="modal-close" onclick="closeModal('addAssignmentModal')">×</button>
|
||||
<h3>批量加减分</h3>
|
||||
<button class="modal-close" onclick="closeModal('batchPointsModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitAddAssignment()">
|
||||
<form onsubmit="event.preventDefault(); handleSubmitPoints()">
|
||||
<div class="form-group">
|
||||
<label>科目</label>
|
||||
<select id="assignmentSubjectId" required></select>
|
||||
<label>已选学生</label>
|
||||
<div id="selectedStudentsCount" class="selected-info">未选择学生</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>作业标题</label>
|
||||
<input type="text" id="assignmentTitle" required>
|
||||
<label>扣分类型</label>
|
||||
<div class="deduction-types">
|
||||
<button type="button" class="btn btn-sm" onclick="selectDeductionType(-window.DEDUCTION_HOMEWORK_NOT_SUBMIT, '未交作业')">未交作业(-<span class="hw-not-submit"></span>分)</button>
|
||||
<button type="button" class="btn btn-sm" onclick="selectDeductionType(-window.DEDUCTION_HOMEWORK_LATE, '迟交作业')">迟交作业(-<span class="hw-late"></span>分)</button>
|
||||
<button type="button" class="btn btn-sm" onclick="selectDeductionType(0, '')">自定义</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>作业描述</label>
|
||||
<textarea id="assignmentDescription" rows="3"></textarea>
|
||||
<label>分数变动</label>
|
||||
<input type="number" id="pointsChange" required min="-3" max="3" step="1" placeholder="正数加分,负数扣分">
|
||||
<small>每次加减分不超过<span class="hw-max"></span>分</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>截止日期</label>
|
||||
<input type="date" id="assignmentDeadline" required>
|
||||
<label>原因</label>
|
||||
<textarea id="pointsReason" rows="3" required placeholder="请输入加减分原因"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">发布</button>
|
||||
<button type="button" class="btn" onclick="closeModal('addAssignmentModal')">取消</button>
|
||||
<button type="submit" class="btn btn-primary">确认提交</button>
|
||||
<button type="button" class="btn" onclick="closeModal('batchPointsModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadAssignments() {
|
||||
const res = await apiGet('/api/admin/homework/assignments');
|
||||
let selectedStudentIds = [];
|
||||
|
||||
// 初始化扣分配置
|
||||
const hwMaxPoints = window.HOMEWORK_MAX_POINTS || 3;
|
||||
const hwNotSubmit = window.DEDUCTION_HOMEWORK_NOT_SUBMIT || 2;
|
||||
const hwLate = window.DEDUCTION_HOMEWORK_LATE || 1;
|
||||
|
||||
// 更新页面中的配置值显示
|
||||
document.querySelectorAll('.hw-not-submit').forEach(el => el.textContent = hwNotSubmit);
|
||||
document.querySelectorAll('.hw-late').forEach(el => el.textContent = hwLate);
|
||||
document.querySelectorAll('.hw-max').forEach(el => el.textContent = hwMaxPoints);
|
||||
|
||||
// 更新输入框的 min/max
|
||||
document.getElementById('pointsChange').setAttribute('min', -hwMaxPoints);
|
||||
document.getElementById('pointsChange').setAttribute('max', hwMaxPoints);
|
||||
|
||||
async function loadStudents() {
|
||||
const res = await apiGet('/api/admin/students');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
for (const assignment of res.data.assignments) {
|
||||
html += `
|
||||
<div class="card assignment-card">
|
||||
<div class="assignment-header">
|
||||
<div><span class="assignment-title">${escapeHtml(assignment.title)}</span> <span class="assignment-meta">(${escapeHtml(assignment.subject_name)})</span></div>
|
||||
<div class="assignment-meta">截止: ${assignment.deadline}</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead><tr><th>学生</th><th>状态</th><th>备注</th><th>操作</th></tr></thead>
|
||||
<tbody id="submissions-${assignment.assignment_id}"><tr><td colspan="4" class="loading">加载中...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (res.data.assignments.length === 0) {
|
||||
html = '<div style="text-align:center;padding:40px;">暂无作业</div>';
|
||||
}
|
||||
document.getElementById('assignmentsList').innerHTML = html;
|
||||
|
||||
for (const assignment of res.data.assignments) {
|
||||
await loadSubmissions(assignment.assignment_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubmissions(assignmentId) {
|
||||
const res = await apiGet(`/api/admin/homework/submissions/${assignmentId}`);
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.submissions.forEach(sub => {
|
||||
res.data.students.forEach(student => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(sub.student_name)}</td>
|
||||
<td>${getStatusBadge(sub.status, 'homework')}</td>
|
||||
<td>${escapeHtml(sub.comments || '-')}</td>
|
||||
<td>
|
||||
<select id="status-${sub.submission_id}" class="status-select">
|
||||
<option value="submitted" ${sub.status === 'submitted' ? 'selected' : ''}>已提交</option>
|
||||
<option value="not_submitted" ${sub.status === 'not_submitted' ? 'selected' : ''}>未提交</option>
|
||||
<option value="late" ${sub.status === 'late' ? 'selected' : ''}>迟交</option>
|
||||
</select>
|
||||
<label><input type="checkbox" id="deduct-${sub.submission_id}"> 同时扣分</label>
|
||||
<button class="btn btn-sm btn-primary" onclick="updateSubmission(${sub.submission_id})">更新</button>
|
||||
</td>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
<td><button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
document.getElementById(`submissions-${assignmentId}`).innerHTML = html;
|
||||
if (res.data.students.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无学生数据</td></tr>';
|
||||
}
|
||||
document.getElementById('studentList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSubmission(submissionId) {
|
||||
const status = document.getElementById(`status-${submissionId}`).value;
|
||||
const applyDeduction = document.getElementById(`deduct-${submissionId}`).checked;
|
||||
function showSinglePointsModal(studentId, studentName) {
|
||||
selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
const res = await apiPut('/api/admin/homework/submission', {
|
||||
submission_id: submissionId,
|
||||
status: status,
|
||||
apply_deduction: applyDeduction
|
||||
function showBatchPointsModal() {
|
||||
selectedStudentIds = [];
|
||||
document.querySelectorAll('.student-checkbox:checked').forEach(cb => {
|
||||
selectedStudentIds.push(parseInt(cb.dataset.id));
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('更新成功');
|
||||
loadAssignments();
|
||||
if (selectedStudentIds.length === 0) {
|
||||
showToast('请先选择学生', 'warning');
|
||||
return;
|
||||
}
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `已选择 ${selectedStudentIds.length} 名学生`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
function selectDeductionType(points, reason) {
|
||||
document.getElementById('pointsChange').value = points;
|
||||
if (points !== 0) {
|
||||
document.getElementById('pointsReason').value = reason;
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('pointsReason').focus();
|
||||
}
|
||||
}
|
||||
|
||||
loadAssignments();
|
||||
function handleSubmitPoints() {
|
||||
const pointsChange = parseInt(document.getElementById('pointsChange').value);
|
||||
if (isNaN(pointsChange) || pointsChange === 0) {
|
||||
showToast('请输入有效的加减分值', 'warning');
|
||||
return;
|
||||
}
|
||||
if (Math.abs(pointsChange) > hwMaxPoints) {
|
||||
showToast(`每次加减分不超过${hwMaxPoints}分`, 'warning');
|
||||
return;
|
||||
}
|
||||
submitBatchPoints();
|
||||
}
|
||||
|
||||
loadStudents();
|
||||
loadStudents();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
|
||||
@@ -22,25 +22,7 @@ $role = $_SESSION['role'] ?? '';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item active">修改密码</a>
|
||||
</div>
|
||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
|
||||
@@ -22,25 +22,7 @@ $role = $_SESSION['role'] ?? '';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item active">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
@@ -65,7 +47,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
<th>学号</th>
|
||||
<th>姓名</th>
|
||||
<th>操行分</th>
|
||||
<th>家长手机号</th>
|
||||
<?php if ($role === '班主任'): ?><th>家长手机号</th><?php endif; ?>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -129,6 +111,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const userRole = '<?php echo $role; ?>';
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
|
||||
@@ -145,7 +128,7 @@ async function loadStudents(page = 1) {
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
<td>${student.parent_phone || '-'}</td>
|
||||
${userRole === '班主任' ? `<td>${student.parent_phone || '-'}</td>` : ''}
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
|
||||
</td>
|
||||
@@ -153,7 +136,7 @@ async function loadStudents(page = 1) {
|
||||
});
|
||||
|
||||
if (res.data.students.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;">暂无学生数据</td></tr>';
|
||||
html = `<tr><td colspan="${userRole === '班主任' ? '6' : '5'}" style="text-align:center;">暂无学生数据</td></tr>`;
|
||||
}
|
||||
|
||||
document.getElementById('studentList').innerHTML = html;
|
||||
|
||||
@@ -27,25 +27,7 @@ $page_title = '科目管理';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<a href="/admin/subjects.php" class="nav-item active">科目管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
|
||||
@@ -128,3 +128,85 @@
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* 考勤学生方格网格 */
|
||||
.student-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.student-cell {
|
||||
width: calc(100% / 7 - 10px);
|
||||
min-height: 60px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
padding: 8px 4px;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.student-cell:hover {
|
||||
background: #f3f4f6;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.student-cell.selected {
|
||||
background: #fee2e2;
|
||||
border-color: #ef4444;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.student-cell.has-record {
|
||||
border: 2px dashed #9ca3af;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.attendance-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.attendance-toolbar .status-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.attendance-toolbar .status-btn {
|
||||
padding: 6px 16px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.attendance-toolbar .status-btn.active {
|
||||
border-color: #667eea;
|
||||
background: #eef2ff;
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.student-cell {
|
||||
width: calc(100% / 4 - 10px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.student-cell {
|
||||
width: calc(100% / 3 - 10px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,103 +171,6 @@ async function submitAddStudent() {
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加作业模态框
|
||||
function showAddAssignmentModal() {
|
||||
document.getElementById('addAssignmentModal').style.display = 'flex';
|
||||
loadSubjectsForSelect();
|
||||
}
|
||||
|
||||
// 加载科目下拉框
|
||||
async function loadSubjectsForSelect() {
|
||||
const res = await apiGet('/api/subject/list');
|
||||
if (res && res.success) {
|
||||
let html = '<option value="">请选择科目</option>';
|
||||
res.data.subjects.forEach(s => {
|
||||
if (s.is_active) {
|
||||
html += `<option value="${s.subject_id}">${s.subject_name}</option>`;
|
||||
}
|
||||
});
|
||||
document.getElementById('assignmentSubjectId').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交添加作业
|
||||
async function submitAddAssignment() {
|
||||
const subjectId = document.getElementById('assignmentSubjectId').value;
|
||||
const title = document.getElementById('assignmentTitle').value.trim();
|
||||
const description = document.getElementById('assignmentDescription').value;
|
||||
const deadline = document.getElementById('assignmentDeadline').value;
|
||||
|
||||
if (!subjectId || !title || !deadline) {
|
||||
showToast('请填写完整信息', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/homework/assignment', {
|
||||
subject_id: parseInt(subjectId),
|
||||
title: title,
|
||||
description: description,
|
||||
deadline: deadline
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('作业发布成功');
|
||||
closeModal('addAssignmentModal');
|
||||
loadAssignments();
|
||||
} else {
|
||||
showToast(res?.message || '发布失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加考勤模态框
|
||||
async function showAddAttendanceModal() {
|
||||
await loadStudentsForSelect();
|
||||
document.getElementById('addAttendanceModal').style.display = 'flex';
|
||||
document.getElementById('attendanceDate').value = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
// 加载学生下拉框
|
||||
async function loadStudentsForSelect() {
|
||||
const res = await apiGet('/api/admin/students');
|
||||
if (res && res.success) {
|
||||
let html = '<option value="">请选择学生</option>';
|
||||
res.data.students.forEach(s => {
|
||||
html += `<option value="${s.student_id}">${s.student_no} - ${s.name}</option>`;
|
||||
});
|
||||
document.getElementById('attendanceStudentId').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交添加考勤
|
||||
async function submitAddAttendance() {
|
||||
const studentId = document.getElementById('attendanceStudentId').value;
|
||||
const date = document.getElementById('attendanceDate').value;
|
||||
const status = document.getElementById('attendanceStatus').value;
|
||||
const reason = document.getElementById('attendanceReason').value;
|
||||
const applyDeduction = document.getElementById('attendanceDeduct').checked;
|
||||
|
||||
if (!studentId || !date || !status) {
|
||||
showToast('请填写完整信息', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/attendance', {
|
||||
student_id: parseInt(studentId),
|
||||
date: date,
|
||||
status: status,
|
||||
reason: reason,
|
||||
apply_deduction: applyDeduction
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('考勤记录添加成功');
|
||||
closeModal('addAttendanceModal');
|
||||
loadAttendanceRecords();
|
||||
} else {
|
||||
showToast(res?.message || '添加失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加管理员模态框
|
||||
function showAddAdminModal() {
|
||||
document.getElementById('addAdminModal').style.display = 'flex';
|
||||
|
||||
@@ -61,6 +61,14 @@ define('SESSION_TIMEOUT', (int)$config['SESSION_TIMEOUT']);
|
||||
define('ICP_ENABLED', $config['ICP_ENABLED'] === 'false');
|
||||
define('ICP_NUMBER', $config['ICP_NUMBER'] ?? '');
|
||||
|
||||
// 扣分规则配置(有默认值,不强制要求在.env中配置)
|
||||
define('DEDUCTION_HOMEWORK_NOT_SUBMIT', (int)($config['DEDUCTION_HOMEWORK_NOT_SUBMIT'] ?? 2));
|
||||
define('DEDUCTION_HOMEWORK_LATE', (int)($config['DEDUCTION_HOMEWORK_LATE'] ?? 1));
|
||||
define('HOMEWORK_MAX_POINTS', (int)($config['HOMEWORK_MAX_POINTS'] ?? 3));
|
||||
define('DEDUCTION_ATTENDANCE_ABSENT', (int)($config['DEDUCTION_ATTENDANCE_ABSENT'] ?? 5));
|
||||
define('DEDUCTION_ATTENDANCE_LATE', (int)($config['DEDUCTION_ATTENDANCE_LATE'] ?? 2));
|
||||
define('DEDUCTION_ATTENDANCE_LEAVE', (int)($config['DEDUCTION_ATTENDANCE_LEAVE'] ?? 1));
|
||||
|
||||
// 会话配置
|
||||
ini_set('session.cookie_httponly', 1);
|
||||
ini_set('session.use_only_cookies', 1);
|
||||
|
||||
@@ -45,5 +45,11 @@ $page_title = $page_title ?? '首页';
|
||||
window.API_BASE_URL = '<?php echo API_BASE_URL; ?>';
|
||||
window.JWT_STORAGE_KEY = '<?php echo JWT_STORAGE_KEY; ?>';
|
||||
window.USER_STORAGE_KEY = '<?php echo USER_STORAGE_KEY; ?>';
|
||||
window.DEDUCTION_HOMEWORK_NOT_SUBMIT = <?php echo DEDUCTION_HOMEWORK_NOT_SUBMIT; ?>;
|
||||
window.DEDUCTION_HOMEWORK_LATE = <?php echo DEDUCTION_HOMEWORK_LATE; ?>;
|
||||
window.HOMEWORK_MAX_POINTS = <?php echo HOMEWORK_MAX_POINTS; ?>;
|
||||
window.DEDUCTION_ATTENDANCE_ABSENT = <?php echo DEDUCTION_ATTENDANCE_ABSENT; ?>;
|
||||
window.DEDUCTION_ATTENDANCE_LATE = <?php echo DEDUCTION_ATTENDANCE_LATE; ?>;
|
||||
window.DEDUCTION_ATTENDANCE_LEAVE = <?php echo DEDUCTION_ATTENDANCE_LEAVE; ?>;
|
||||
</script>
|
||||
<script src="/assets/js/common.js"></script>
|
||||
21
frontend/includes/nav.php
Normal file
21
frontend/includes/nav.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item<?php echo $current_page === 'dashboard' ? ' active' : ''; ?>">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item<?php echo $current_page === 'students' ? ' active' : ''; ?>">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item<?php echo $current_page === 'conduct' ? ' active' : ''; ?>">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item<?php echo $current_page === 'homework' ? ' active' : ''; ?>">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item<?php echo $current_page === 'attendance' ? ' active' : ''; ?>">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item<?php echo $current_page === 'subjects' ? ' active' : ''; ?>">科目管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/admins.php" class="nav-item<?php echo $current_page === 'admins' ? ' active' : ''; ?>">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item<?php echo $current_page === 'history' ? ' active' : ''; ?>">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item<?php echo $current_page === 'password' ? ' active' : ''; ?>">修改密码</a>
|
||||
</div>
|
||||
Reference in New Issue
Block a user