冲突检查
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 课表冲突检查:单维度检查(POST /timetable-conflict/check)
|
||||||
|
// body: { nd 年度(必填), dimensionCode 维度编码(必填), writeToDb?(落库), jcpch?(批次号) }
|
||||||
|
// 返回 TimetableConflictCardVO { dimensionCode, title, description, checked, conflictCount }
|
||||||
|
export function checkConflict(data) {
|
||||||
|
return request({
|
||||||
|
url: '/timetable-conflict/check',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 课表冲突检查:执行全部维度检查(POST /timetable-conflict/check-all)
|
||||||
|
// body: { nd 年度(必填), writeToDb? }
|
||||||
|
// 返回 TimetableConflictSummaryVO { nd, cards[], totalConflictCount, fullyChecked }
|
||||||
|
export function checkAllConflict(data) {
|
||||||
|
return request({
|
||||||
|
url: '/timetable-conflict/check-all',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 课表冲突检查:重置检查结果(POST /timetable-conflict/reset)
|
||||||
|
// body: { nd 年度(必填), clearDb? 是否同时清空落库表 }
|
||||||
|
// 返回 TimetableConflictSummaryVO(所有卡片未检查状态)
|
||||||
|
export function resetConflict(data) {
|
||||||
|
return request({
|
||||||
|
url: '/timetable-conflict/reset',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 课表冲突检查:分页查询冲突明细(GET /timetable-conflict/details)
|
||||||
|
// params: { nd 年度(必填), dimensionCode? 维度编码, pageNum?, pageSize?, useDb?, jcpch? }
|
||||||
|
// 返回 PageResult<TimetableConflictDetailVO>
|
||||||
|
export function getConflictDetails(params) {
|
||||||
|
return request({
|
||||||
|
url: '/timetable-conflict/details',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,9 +5,16 @@
|
|||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="section-title">教学资源冲突检查</div>
|
<div class="section-title">教学资源冲突检查</div>
|
||||||
<div class="section-desc">2026年秋季学期教学资源冲突检查,可单独检查或执行全部检查</div>
|
<div class="section-desc">当前检查年度:{{ nd ? nd + ' 年' : '未选择' }},可单独检查或执行全部检查</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
<el-form inline class="toolbar-form" @submit.native.prevent>
|
||||||
|
<el-form-item label="年度" class="year-item">
|
||||||
|
<el-select v-model="nd" placeholder="请选择年度" style="width: 130px" @change="handleNdChange">
|
||||||
|
<el-option v-for="y in yearOptions" :key="y" :label="y + ' 年'" :value="y" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
<el-button size="mini" :disabled="loading" @click="handleReset">重置</el-button>
|
<el-button size="mini" :disabled="loading" @click="handleReset">重置</el-button>
|
||||||
<el-button type="primary" size="mini" :loading="loading" @click="handleCheckAll">执行全部检查</el-button>
|
<el-button type="primary" size="mini" :loading="loading" @click="handleCheckAll">执行全部检查</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,7 +27,7 @@
|
|||||||
:key="item.key"
|
:key="item.key"
|
||||||
class="check-item-card"
|
class="check-item-card"
|
||||||
:class="{ active: activeKey === item.key }"
|
:class="{ active: activeKey === item.key }"
|
||||||
@click="activeKey = item.checked ? item.key : activeKey"
|
@click="selectCard(item)"
|
||||||
>
|
>
|
||||||
<div class="check-item-header">
|
<div class="check-item-header">
|
||||||
<span class="check-item-name">{{ item.label }}</span>
|
<span class="check-item-name">{{ item.label }}</span>
|
||||||
@@ -45,145 +52,228 @@
|
|||||||
冲突明细
|
冲突明细
|
||||||
<span v-if="activeKey" class="active-label">— {{ activeLabel }}</span>
|
<span v-if="activeKey" class="active-label">— {{ activeLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="activeKey" class="detail-count">共 {{ activeDetails.length }} 条记录</span>
|
<span v-if="activeKey" class="detail-count">共 {{ detailTotal }} 条记录</span>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-if="activeKey" :data="activeDetails" border stripe size="mini" max-height="500">
|
<template v-if="activeKey">
|
||||||
|
<el-table v-loading="detailLoading" :data="activeDetails" border stripe size="mini" max-height="500">
|
||||||
<el-table-column type="index" label="序号" width="55" align="center" />
|
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||||
<el-table-column label="冲突号" width="80" align="center">
|
<el-table-column label="冲突号" width="90" align="center" show-overflow-tooltip>
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">{{ scope.row.conflictNo || '-' }}</template>
|
||||||
<el-tag type="danger" size="mini">#{{ Math.floor(scope.$index / 2) + 1 }}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="kcmc" label="课程" min-width="130" show-overflow-tooltip />
|
<el-table-column prop="courseNames" label="课程" min-width="150" show-overflow-tooltip header-align="center" />
|
||||||
<el-table-column label="日期" width="100">
|
<el-table-column label="日期" width="100" align="center">
|
||||||
<template slot-scope="scope">{{ formatDateStr(scope.row.rq) }}</template>
|
<template slot-scope="scope">{{ fmtDate(scope.row.rq) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="jc" label="节次" width="70" align="center" />
|
<el-table-column prop="jc" label="节次" width="70" align="center" />
|
||||||
<el-table-column prop="bc" label="班次" width="130" show-overflow-tooltip />
|
<el-table-column prop="xydNames" label="班次" min-width="140" show-overflow-tooltip header-align="center" />
|
||||||
<el-table-column prop="skjy" label="教员" width="90" />
|
<el-table-column prop="teacherNames" label="教员" min-width="120" show-overflow-tooltip header-align="center" />
|
||||||
<el-table-column prop="jxcd" label="场地" width="130" show-overflow-tooltip />
|
<el-table-column prop="classroomNames" label="场地" min-width="140" show-overflow-tooltip header-align="center" />
|
||||||
<el-table-column prop="zrdw" label="责任单位" width="100" />
|
<el-table-column prop="responsibleDept" label="责任单位" min-width="110" show-overflow-tooltip header-align="center" />
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-if="detailTotal > 0"
|
||||||
|
class="detail-pagination"
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next"
|
||||||
|
:current-page="detailQuery.pageNum"
|
||||||
|
:page-size="detailQuery.pageSize"
|
||||||
|
:total="detailTotal"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
<el-empty v-if="detailTotal === 0 && !detailLoading" description="暂无冲突明细" />
|
||||||
|
</template>
|
||||||
<el-empty v-else v-show="!loading" description="点击上方卡片「检查」按钮,在此查看冲突明细" />
|
<el-empty v-else v-show="!loading" description="点击上方卡片「检查」按钮,在此查看冲突明细" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 教学计划静态示例数据(待对接 getTeachingPlanList 接口)
|
import { checkConflict, checkAllConflict, resetConflict, getConflictDetails } from '@/api/teachBusiness/timetableConflict'
|
||||||
const MOCK_PLANS = [
|
import { listAllSemester } from '@/api/teachBusiness/semester'
|
||||||
{ kcmc: '高等数学', rq: '2026-09-01', jc: 1, bc: '一班', skjy: '张教员', jxcd: '1号教学楼-101', zrdw: '基础部' },
|
|
||||||
{ kcmc: '高等数学', rq: '2026-09-01', jc: 1, bc: '一班', skjy: '张教员', jxcd: '1号教学楼-101', zrdw: '基础部' },
|
|
||||||
{ kcmc: '大学英语', rq: '2026-09-01', jc: 1, bc: '二班', skjy: '李教员', jxcd: '1号教学楼-101', zrdw: '基础部' },
|
|
||||||
{ kcmc: '大学英语', rq: '2026-09-02', jc: 3, bc: '二班', skjy: '李教员', jxcd: '2号教学楼-205', zrdw: '基础部' },
|
|
||||||
{ kcmc: '计算机基础', rq: '2026-09-01', jc: 1, bc: '三班', skjy: '王教员', jxcd: '实验楼-303', zrdw: '信息工程系' },
|
|
||||||
{ kcmc: '军事理论', rq: '2026-09-02', jc: 3, bc: '一班', skjy: '赵教员', jxcd: '3号教学楼-110', zrdw: '政工系' }
|
|
||||||
]
|
|
||||||
|
|
||||||
function formatDateStr(rq) {
|
// 四类检查卡片定义(标题/描述与后端 TimetableConflictDimension 枚举一致,作为页面布局常量;
|
||||||
return (rq || '').substring(0, 10)
|
// 检查状态与冲突数完全来自后端接口)
|
||||||
}
|
const CARD_DEFS = [
|
||||||
|
{ key: 'TEAM_CONFLICT', label: '教学班时间冲突检查', desc: '同一教学班次在同一时间段被安排多门课程' },
|
||||||
|
{ key: 'ELECTIVE_REQUIRED_CONFLICT', label: '教学班必修与选修时间冲突检查', desc: '教学班次必修课与选修课时间重叠' },
|
||||||
|
{ key: 'TEACHER_CONFLICT', label: '教员时间冲突检查', desc: '同一教员在同一时间段被安排多门课程' },
|
||||||
|
{ key: 'CLASSROOM_CONFLICT', label: '教室时间冲突检查', desc: '同一教室在同一时间段被多门课程占用' }
|
||||||
|
]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TimetableConflict',
|
name: 'TimetableConflict',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
allRecords: [],
|
detailLoading: false,
|
||||||
|
// 年度(数据来自 /semester/all,真实后端数据)
|
||||||
|
nd: undefined,
|
||||||
|
yearOptions: [],
|
||||||
|
conflictItems: CARD_DEFS.map(c => ({ ...c, count: 0, checked: false })),
|
||||||
activeKey: '',
|
activeKey: '',
|
||||||
conflictItems: [
|
activeDetails: [],
|
||||||
{ key: 'jxbssj', label: '教学班次时间冲突检查', desc: '同一教学班次在同一时间段被安排多门课程', count: 0, checked: false, details: [] },
|
detailTotal: 0,
|
||||||
{ key: 'jxbskcsj', label: '教学班次必修与选修时间冲突检查', desc: '教学班次必修课与选修课时间重叠', count: 0, checked: false, details: [] },
|
detailQuery: { pageNum: 1, pageSize: 20 }
|
||||||
{ key: 'jysj', label: '教员时间冲突检查', desc: '同一教员在同一时间段被安排多门课程', count: 0, checked: false, details: [] },
|
|
||||||
{ key: 'jssj', label: '教室时间冲突检查', desc: '同一教室在同一时间段被多门课程占用', count: 0, checked: false, details: [] }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
activeLabel() {
|
activeLabel() {
|
||||||
const item = this.conflictItems.find(i => i.key === this.activeKey)
|
const item = this.conflictItems.find(i => i.key === this.activeKey)
|
||||||
return item ? item.label : ''
|
return item ? item.label : ''
|
||||||
},
|
|
||||||
// 当前选中检查项的展开明细(打平所有冲突记录)
|
|
||||||
activeDetails() {
|
|
||||||
const item = this.conflictItems.find(i => i.key === this.activeKey)
|
|
||||||
if (!item) return []
|
|
||||||
const flat = []
|
|
||||||
item.details.forEach(d => {
|
|
||||||
d.items.forEach(it => flat.push(it))
|
|
||||||
})
|
|
||||||
return flat
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
// 初始化演示数据(模拟接口返回)
|
this.loadYearOptions()
|
||||||
this.allRecords = MOCK_PLANS.map((item, index) => ({ ...item, xh: index + 1 }))
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/* ---------- 年度下拉(数据来自 /semester/all) ---------- */
|
||||||
|
loadYearOptions() {
|
||||||
|
return listAllSemester().then(response => {
|
||||||
|
const list = response.data || []
|
||||||
|
// 去重并按年度倒序
|
||||||
|
const map = {}
|
||||||
|
list.forEach(item => {
|
||||||
|
if (item.nd !== null && item.nd !== undefined && item.nd !== '') map[item.nd] = item
|
||||||
|
})
|
||||||
|
const arr = Object.keys(map).sort((a, b) => String(b).localeCompare(String(a)))
|
||||||
|
this.yearOptions = arr
|
||||||
|
// 默认年度:优先当前学期(dqxq=true),否则取最新年度
|
||||||
|
const current = list.find(item => item.dqxq === true)
|
||||||
|
this.nd = (current && current.nd) || arr[0]
|
||||||
|
return arr
|
||||||
|
}).catch(() => {
|
||||||
|
this.yearOptions = []
|
||||||
|
this.nd = undefined
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fmtDate(rq) {
|
||||||
|
return (rq || '').substring(0, 10)
|
||||||
|
},
|
||||||
getStatus(item) {
|
getStatus(item) {
|
||||||
if (!item.checked) return { type: 'info', text: '未检查' }
|
if (!item.checked) return { type: 'info', text: '未检查' }
|
||||||
return item.count > 0
|
return item.count > 0
|
||||||
? { type: 'danger', text: '存在 ' + item.count + ' 处冲突' }
|
? { type: 'danger', text: '存在 ' + item.count + ' 处冲突' }
|
||||||
: { type: 'success', text: '无冲突' }
|
: { type: 'success', text: '无冲突' }
|
||||||
},
|
},
|
||||||
/** 通用冲突检测:按分组字段找 > 1 的记录 */
|
/* 用后端返回的单卡片结果同步对应卡片状态 */
|
||||||
detectConflicts(records, groupFields) {
|
applyCard(card) {
|
||||||
const map = {}
|
if (!card) return
|
||||||
records.forEach(item => {
|
const item = this.conflictItems.find(i => i.key === card.dimensionCode)
|
||||||
const key = groupFields.map(f => f + '=' + (item[f] !== undefined && item[f] !== null ? item[f] : '')).join('|') +
|
if (!item) return
|
||||||
'|rq=' + formatDateStr(item.rq) + '|jc=' + (item.jc !== undefined && item.jc !== null ? item.jc : '')
|
item.checked = card.checked
|
||||||
if (!map[key]) map[key] = []
|
item.count = card.conflictCount
|
||||||
map[key].push(item)
|
|
||||||
})
|
|
||||||
return Object.keys(map)
|
|
||||||
.filter(k => map[k].length > 1)
|
|
||||||
.map(k => ({ key: k, items: map[k] }))
|
|
||||||
.sort((a, b) => a.key.localeCompare(b.key))
|
|
||||||
},
|
},
|
||||||
|
applySummary(summary) {
|
||||||
|
if (!summary || !summary.cards) return
|
||||||
|
summary.cards.forEach(card => this.applyCard(card))
|
||||||
|
},
|
||||||
|
/* ---------- 单个检查 ---------- */
|
||||||
handleCheck(item) {
|
handleCheck(item) {
|
||||||
if (this.allRecords.length === 0) return
|
if (!this.nd) {
|
||||||
let details = []
|
this.$message.warning('请先选择年度')
|
||||||
switch (item.key) {
|
return
|
||||||
case 'jxbssj':
|
|
||||||
details = this.detectConflicts(this.allRecords, ['bc'])
|
|
||||||
break
|
|
||||||
case 'jxbskcsj':
|
|
||||||
details = this.detectConflicts(this.allRecords, ['bc'])
|
|
||||||
break
|
|
||||||
case 'jysj':
|
|
||||||
details = this.detectConflicts(this.allRecords, ['skjy'])
|
|
||||||
break
|
|
||||||
case 'jssj':
|
|
||||||
details = this.detectConflicts(this.allRecords, ['jxcd'])
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
item.details = details
|
this.loading = true
|
||||||
item.count = details.length
|
checkConflict({ nd: this.nd, dimensionCode: item.key }).then(response => {
|
||||||
item.checked = true
|
this.applyCard(response.data)
|
||||||
this.activeKey = item.key
|
this.activeKey = item.key
|
||||||
|
this.detailQuery.pageNum = 1
|
||||||
|
this.loadDetails()
|
||||||
if (item.count > 0) {
|
if (item.count > 0) {
|
||||||
this.$message.warning('「' + item.label + '」发现 ' + item.count + ' 处冲突')
|
this.$message.warning('「' + item.label + '」检查完成,发现 ' + item.count + ' 处冲突')
|
||||||
} else {
|
} else {
|
||||||
this.$message.success('「' + item.label + '」未发现冲突')
|
this.$message.success('「' + item.label + '」检查完成,未发现冲突')
|
||||||
}
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
/* ---------- 全部检查 ---------- */
|
||||||
handleCheckAll() {
|
handleCheckAll() {
|
||||||
this.conflictItems.forEach(item => this.handleCheck(item))
|
if (!this.nd) {
|
||||||
const firstConflict = this.conflictItems.find(i => i.count > 0)
|
this.$message.warning('请先选择年度')
|
||||||
if (firstConflict) {
|
return
|
||||||
this.activeKey = firstConflict.key
|
|
||||||
}
|
}
|
||||||
const total = this.conflictItems.reduce((sum, item) => sum + item.count, 0)
|
this.loading = true
|
||||||
|
checkAllConflict({ nd: this.nd }).then(response => {
|
||||||
|
const summary = response.data
|
||||||
|
this.applySummary(summary)
|
||||||
|
const total = summary ? summary.totalConflictCount : 0
|
||||||
this.$message.success('全部检查完成,共发现 ' + total + ' 处冲突')
|
this.$message.success('全部检查完成,共发现 ' + total + ' 处冲突')
|
||||||
|
const firstConflict = this.conflictItems.find(i => i.checked && i.count > 0)
|
||||||
|
const firstChecked = this.conflictItems.find(i => i.checked)
|
||||||
|
this.activeKey = firstConflict ? firstConflict.key : (firstChecked ? firstChecked.key : '')
|
||||||
|
this.detailQuery.pageNum = 1
|
||||||
|
if (this.activeKey) {
|
||||||
|
this.loadDetails()
|
||||||
|
} else {
|
||||||
|
this.activeDetails = []
|
||||||
|
this.detailTotal = 0
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
/* ---------- 重置 ---------- */
|
||||||
handleReset() {
|
handleReset() {
|
||||||
this.conflictItems.forEach(item => {
|
if (!this.nd) {
|
||||||
item.count = 0
|
this.$message.warning('请先选择年度')
|
||||||
item.checked = false
|
return
|
||||||
item.details = []
|
}
|
||||||
|
resetConflict({ nd: this.nd }).then(response => {
|
||||||
|
this.applySummary(response.data)
|
||||||
|
this.activeKey = ''
|
||||||
|
this.activeDetails = []
|
||||||
|
this.detailTotal = 0
|
||||||
|
this.$message.success('检查结果已重置')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/* 点击卡片:仅已检查的卡片可查看其明细 */
|
||||||
|
selectCard(item) {
|
||||||
|
if (!item.checked) return
|
||||||
|
this.activeKey = item.key
|
||||||
|
this.detailQuery.pageNum = 1
|
||||||
|
this.loadDetails()
|
||||||
|
},
|
||||||
|
/* ---------- 冲突明细 ---------- */
|
||||||
|
loadDetails() {
|
||||||
|
if (!this.activeKey) {
|
||||||
|
this.activeDetails = []
|
||||||
|
this.detailTotal = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.detailLoading = true
|
||||||
|
getConflictDetails({
|
||||||
|
nd: this.nd,
|
||||||
|
dimensionCode: this.activeKey,
|
||||||
|
pageNum: this.detailQuery.pageNum,
|
||||||
|
pageSize: this.detailQuery.pageSize
|
||||||
|
}).then(response => {
|
||||||
|
const page = response.data || {}
|
||||||
|
this.activeDetails = page.records || []
|
||||||
|
this.detailTotal = page.total || 0
|
||||||
|
}).catch(() => {
|
||||||
|
this.activeDetails = []
|
||||||
|
this.detailTotal = 0
|
||||||
|
}).finally(() => {
|
||||||
|
this.detailLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handlePageChange(page) {
|
||||||
|
this.detailQuery.pageNum = page
|
||||||
|
this.loadDetails()
|
||||||
|
},
|
||||||
|
/* 切换年度:该年度检查状态未知,重置为未检查 */
|
||||||
|
handleNdChange() {
|
||||||
|
this.conflictItems.forEach(i => {
|
||||||
|
i.checked = false
|
||||||
|
i.count = 0
|
||||||
})
|
})
|
||||||
this.activeKey = ''
|
this.activeKey = ''
|
||||||
this.$message.info('已重置检查结果')
|
this.activeDetails = []
|
||||||
|
this.detailTotal = 0
|
||||||
|
this.detailQuery.pageNum = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,6 +306,14 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
||||||
|
.toolbar-form {
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
|
.year-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,5 +408,10 @@ export default {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-pagination {
|
||||||
|
margin-top: 12px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user