ecs: ec year list page

dev_cs
p31729568 5 years ago
parent eabad1165a
commit ac67cdafad

@ -1,4 +1,6 @@
class Ecs::EcMajorSchoolsController < Ecs::BaseController
skip_before_action :check_user_permission!, only: [:show]
def index
major_schools = current_school.ec_major_schools.not_template
@ -23,6 +25,17 @@ class Ecs::EcMajorSchoolsController < Ecs::BaseController
@template_major_school = EcMajorSchool.is_template.first #示例专业
end
# :show是 /api/ec_major_schools/:id
def show
@major = EcMajorSchool.find(params[:id])
school = @major.school
return if current_user.admin? || school.manager?(current_user)
return if @major.manager?(current_user)
render_forbidden
end
def create
ActiveRecord::Base.transaction do
Array(params[:major_ids].presence).each do |id|

@ -10,7 +10,7 @@ class Ecs::EcYearsController < Ecs::BaseController
end
@count = ec_years.count
@ec_years = paginate ec_years
@ec_years = paginate ec_years.order(year: :desc)
return if @ec_years.blank?
@ -33,7 +33,7 @@ class Ecs::EcYearsController < Ecs::BaseController
return
end
@ec_year = CopyEcYearService.call(current_major_school, params[:year].to_i)
@ec_year = Ecs::CopyEcYearService.call(current_major_school, params[:year].to_i)
end
def destroy

@ -0,0 +1,22 @@
module Ecs::EcYearsHelper
def achieved_graduation_course_count(ec_year)
return 0 if ec_year.ec_courses.count.zero?
course_ids = ec_year.ec_courses.map(&:id)
target_count_map = EcCourseTarget.where(ec_course_id: course_ids).group(:ec_course_id).count
ec_year.ec_courses.sum { |course| course.complete_target_count == target_count_map[course.id] ? 1 : 0 }
end
def achieved_graduation_objective_count(ec_year)
return 0 if ec_year.ec_graduation_subitems.count.zero?
subitem_ids = ec_year.ec_graduation_subitems.reorder(nil).pluck(:id)
relations = EcGraduationRequirementCalculation.joins(:ec_course_support).where(ec_course_supports: { ec_graduation_subitem_id: subitem_ids })
reached_map = relations.where(status: true).group('ec_graduation_subitem_id').count
reached_map.keys.size
end
end

@ -3,7 +3,6 @@ class EcCourseSupport < ApplicationRecord
belongs_to :ec_course
belongs_to :ec_graduation_subitem
# TODO: 将 ec_graduation_subitem_courses 移除,这个表作为关系表
has_one :ec_graduation_requirement_calculation, dependent: :destroy

@ -12,6 +12,8 @@ class EcMajorSchool < ApplicationRecord
scope :is_template, -> { where(template_major: true) }
scope :not_template, -> { where(template_major: false) }
delegate :code, :name, to: :ec_major
# 是否为该专业管理员
def manager?(user)
ec_major_school_users.exists?(user_id: user.id)

@ -12,4 +12,8 @@ class EcYear < ApplicationRecord
has_many :ec_course_users, dependent: :destroy
has_many :managers, through: :ec_course_users, source: :user
def prev_year
self.class.find_by(year: year.to_i - 1)
end
end

@ -1,4 +1,4 @@
class CopyEcYearService < ApplicationService
class Ecs::CopyEcYearService < ApplicationService
attr_reader :major_school, :to_year
def initialize(major_school, year)

@ -0,0 +1,6 @@
json.extract! @major, :id, :code, :name, :template_major
json.school_id @major.school.id
json.school_name @major.school.name
can_manager = @major.manager?(current_user) || @major.school.manager?(current_user) || current_user.admin_or_business?
json.can_manager can_manager

@ -8,7 +8,16 @@ json.ec_years do
json.training_subitem_count @training_subitem_count_map.fetch(ec_year.id, 0)
json.graduation_requirement_count @graduation_requirement_count_map.fetch(ec_year.id, 0)
json.course_count @course_count_map.fetch(ec_year.id, 0)
json.course_target_count @course_target_count_map.fetch(ec_year.id, 0)
json.graduation_subitem_count @graduation_subitem_count_map.fetch(ec_year.id, 0)
course_target = @course_target_count_map.fetch(ec_year.id, 0)
graduation_subitem = @graduation_subitem_count_map.fetch(ec_year.id, 0)
achieved_course = achieved_graduation_course_count(ec_year)
achieved_objective = achieved_graduation_objective_count(ec_year)
json.course_target_count course_target
json.graduation_subitem_count graduation_subitem
json.achieved_graduation_course_count achieved_course
json.achieved_graduation_objective_count achieved_objective
json.status graduation_subitem == achieved_objective ? 'achieved' : 'not_achieved'
end
end

@ -707,7 +707,7 @@ Rails.application.routes.draw do
# 为避免url过长以及层级过深路由定义和controller继承都做了处理
scope module: :ecs do
resources :ec_major_schools, only: [] do
resources :ec_major_schools, only: [:show] do
resources :users, only: [:index]
resources :major_managers, only: [:create, :destroy]
resources :ec_years, only: [:index, :create, :destroy]

@ -0,0 +1,11 @@
class ChangeEcCourseSupports < ActiveRecord::Migration[5.2]
def change
add_column :ec_course_supports, :ec_graduation_subitem_id, :integer, index: true
execute <<-SQL
UPDATE ec_course_supports ecs SET ec_graduation_subitem_id = (
SELECT ec_graduation_subitem_id FROM ec_graduation_subitem_courses egsc WHERE egsc.ec_course_support_id = ecs.id
)
SQL
end
end

@ -262,8 +262,8 @@ const Help = Loadable({
loading: Loading,
})
const EcsHome = Loadable({
loader: () => import('./modules/ecs/Home'),
const Ecs = Loadable({
loader: () => import('./modules/ecs/Ecs'),
loading: Loading,
})
@ -521,9 +521,9 @@ class App extends Component {
render={
(props)=>(<Help {...this.props} {...props} {...this.state}></Help>)
}/>
<Route exact path="/ecs/department"
<Route path="/ecs"
render={
(props)=>(<EcsHome {...this.props} {...props} {...this.state}></EcsHome>)
(props)=>(<Ecs {...this.props} {...props} {...this.state}></Ecs>)
}/>
<Route exact path="/" component={ShixunsHome}/>
<Route component={Shixunnopage}/>

@ -50,7 +50,6 @@ export function initAxiosInterceptors(props) {
// wy
// proxy="http://192.168.2.63:3001"
proxy = "http://localhost:3001"
// 在这里使用requestMap控制避免用户通过双击等操作发出重复的请求
// 如果需要支持重复的请求考虑config里面自定义一个allowRepeat参考来控制
const requestMap = {};

@ -0,0 +1,108 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Modal, Select, message } from 'antd';
import axios from 'axios';
import './AddYearModal.scss';
const { Option } = Select;
class AddYearModal extends React.Component {
constructor(props) {
super(props);
this.state = {
confirmLoading: false,
error: '',
year: '',
currentYear: new Date().getFullYear()
}
}
handleOk = () => {
let { year } = this.state;
if(!year || year.length === 0){
this.setState({ error: '请选择届别' });
return;
}
this.submitYear();
}
handleCancel = () => {
this.props.onHide(false);
}
onAfterModalClose = () => {
this.setState({ year: '' });
}
submitYear = () => {
let { schoolId, majorId } = this.props;
let { year } = this.state;
this.setState({ confirmLoading: true });
axios.post(`/ec_major_schools/${majorId}/ec_years.json`, { school_id: schoolId, year: year }).then(res => {
if(res.status === 200){
message.success('操作成功');
this.setState({ confirmLoading: false });
this.props.onHide(true);
}
}).catch(e => {
console.log(e);
this.setState({ confirmLoading: false });
})
}
render() {
let { confirmLoading, year, currentYear } = this.state;
return (
<div>
<Modal
title="添加届别"
wrapClassName="add-year-modal"
visible={this.props.visible}
confirmLoading={confirmLoading}
afterClose={this.onAfterModalClose}
onOk={this.handleOk}
onCancel={this.handleCancel}>
<div class="add-year-container">
<div className="add-year-tip">
基础数据除学生列表与成绩录入以外的所有基础数据<br/>
将自动复制上届别的数据数据均可再编辑
</div>
<div className="add-year-content">
<div className="add-year-content-label">选择届别</div>
<div className="add-year-content-select">
<Select defaultValue="" value={year} onChange={ value => this.setState({ year: value })} style={{width: '100%'}} placeholder="请选择届别">
{
[...Array(10)].map((_, index) => {
let y = currentYear - 5 + index;
return (
<Option value={ y }>{y}</Option>
)
})
}
</Select>
</div>
</div>
</div>
</Modal>
</div>
)
}
}
AddYearModal.propTypes = {
schoolId: PropTypes.number,
majorId: PropTypes.number,
visible: PropTypes.bool,
onHide: PropTypes.func
}
export default AddYearModal

@ -0,0 +1,34 @@
.add-year-modal {
.add-year {
&-container {
padding: 0 40px;
}
&-tip {
margin-bottom: 20px;
color: #666666;
line-height: 30px;
text-align: center;
}
&-content {
padding: 0 50px;
display: flex;
align-items: center;
&-label {
}
&-select {
flex: 1;
}
}
}
.ant-modal-footer {
padding-bottom: 20px;
text-align: center;
border-top: unset;
}
}

@ -0,0 +1,207 @@
import React from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import { Spin, Button, Table, Input, Divider, Modal, message } from 'antd';
import './index.scss';
import AddYearModal from "./AddYearModal";
const { Search } = Input;
const { confirm } = Modal;
const defaultPagination = { current: 1, pageSize: 20, total: 0 };
class EcYear extends React.Component {
constructor (props) {
super(props);
this.state = {
majorId: props.match.params.majorId,
spin: true,
loading: true,
keyword: '',
pagination: {...defaultPagination},
major: {},
yearData: [],
// add year modal vars
addYearModalVisible: false
}
}
componentDidMount() {
this.getMajor();
}
getMajor = () => {
axios.get(`/ec_major_schools/${this.state.majorId}.json`).then(res => {
if(res.status === 200){
window.document.title = res.data.name;
this.setState({ spin: false, major: res.data });
this.getYearData();
}
}).catch(e => console.log(e));
}
onSearch = () => {
this.setState({ pagination: {...defaultPagination} }, () => {
this.getYearData();
})
}
getYearData = () => {
let { majorId, keyword, pagination } = this.state;
axios.get(`/ec_major_schools/${majorId}/ec_years.json`, {
params: {
search: keyword,
page: pagination.current,
per_page: pagination.pageSize
}
}).then(res => {
if(res.status === 200){
let pagination = { ...this.state.pagination };
pagination.total = res.data.count;
this.setState({
loading: false,
yearData: res.data.ec_years,
pagination
})
}
}).catch((e) => {
console.log(e);
this.setState({ loading: false });
})
}
onPaginationChange = (page, pageSize) => {
this.setState({ pagination: { current: page, pageSize: pageSize } }, () => {
this.getYearData()
});
}
showDeleteYearConfirm = (yearId) => {
confirm({
title: '确认删除该届别?',
okText: '确认',
cancelText: '取消',
onOk: () => {
this.deleteYear(yearId);
},
onCancel() {},
});
}
deleteYear = (yearId) => {
let { majorId } = this.state;
axios.delete(`/ec_major_schools/${majorId}/ec_years/${yearId}.json`).then(res => {
if(res.status === 200){
message.success('操作成功');
this.getYearData();
}
}).catch(e => console.log(e))
}
HideAddYearModal = (added) => {
this.setState({ AddYearModalVisible: false });
if(added){
this.setState({ keyword: '', pagination: { ...defaultPagination } }, this.getYearData);
}
}
render() {
let { majorId, spin, keyword, loading, pagination, major, yearData } = this.state;
const linkRender = (num, url) => {
return <Link to={url}>{ num === 0 ? "立即配置" : num }</Link>;
}
const contrastRender = (num, other) => {
let color = other !== 0 && num === other ? 'color-green' : 'color-orange';
return other === 0 ? (
<div className={color}><span>--</span> / <span>--</span></div>
) : (
<div className={color}><span>{num}</span> / <span>{other}</span></div>
)
}
const statusRender = (text, record) => {
let zero = record.graduation_subitem_count === 0;
return zero ? (
<span className="color-orange">--</span>
) : (
<span className={text === 'achieved' ? 'color-green' : 'color-orange'}>
{ text === 'achieved' ? '已达成' : '未达成' }
</span>
)
}
const operationRender = (_, record) => {
return (
<div className="operation-box">
<Link to={`/ecs/major_schools/${majorId}/academic_years/${record.id}/training_objectives`} className="link">立即配置</Link>
<a className="link" onClick={() => this.showDeleteYearConfirm(record.id)}>删除</a>
</div>
)
}
const tableColumns = [
{ title: '届别', dataIndex: 'year', render: text => `${text}` },
{ title: '培养目标', dataIndex: 'training_subitem_count', render: (text, record) => linkRender(text, `/ecs/major_schools/${this.state.majorId}/academic_years/${record.id}/training_objectives`), },
{ title: '毕业要求', dataIndex: 'graduation_requirement_count', render: (text, record) => linkRender(text, `/ecs/major_schools/${this.state.majorId}/academic_years/${record.id}/graduation_requirement`), },
{ title: '课程体系', dataIndex: 'course_count', render: (text, record) => linkRender(text, `/ecs/major_schools/${this.state.majorId}/academic_years/${record.id}/ec_course_setting`), },
{ title: '课程目标(达成情况)', key: 'courseTarget', render: (_, record) => { return contrastRender(record.achieved_graduation_course_count, record.course_target_count) } },
{ title: '毕业要求指标点(达成情况)', key: 'graduation', render: (_, record) => { return contrastRender(record.achieved_graduation_objective_count, record.graduation_subitem_count) } },
{ title: '评价结果', dataIndex: 'status', render: statusRender },
{ title: '操作', key: 'operation', render: operationRender }
];
return (
<div className="newMain clearfix">
<Spin size="large" spinning={spin} style={{marginTop: '15%'}}>
<div className="educontent ec-year-list-page">
<div className="year-list-head">
<div className="year-list-head-left">
<div className="year-list-head-label">{ major.name }</div>
<div className="year-list-head-tip">
<span>请选择添加参与认证的学生界别多个界别分次添加</span>
<Link to="/forums/3528" target="_blank" className="link ml10">查看详情</Link>
</div>
</div>
<Button type="primary" onClick={() => this.setState({ AddYearModalVisible: true })}>添加届别</Button>
</div>
<Divider/>
<div className="year-list-body">
<div className="year-list-search">
<Search
placeholder="届别检索"
onInput={e => this.setState({keyword: e.target.value})}
onSearch={this.onSearch}
value={keyword}
style={{ width: 200 }}/>
</div>
<div className="year-list-table">
<Table rowKey="id"
loading={loading}
columns={tableColumns}
dataSource={yearData}
pagination={{...pagination, onChange: this.onPaginationChange}}/>
</div>
</div>
</div>
<AddYearModal schoolId={major && major.school_id}
majorId={majorId}
visible={this.state.AddYearModalVisible}
onHide={this.HideAddYearModal}/>
</Spin>
</div>
)
}
}
export default EcYear;

@ -0,0 +1,56 @@
.ec-year-list-page {
background: #fff;
.year-list {
&-head {
margin-top: 30px;
margin-bottom: -24px;
padding: 20px 30px;
display: flex;
align-items: flex-end;
justify-content: space-between;
&-left {
flex: 1;
}
&-label {
font-size: 18px;
}
&-tip {
font-size: 14px;
color: #999999;
}
}
&-body {
margin-top: -24px;
}
&-search {
padding: 20px 30px;
display: flex;
flex-direction: row-reverse;
}
&-table {
min-height: 400px;
th, td {
text-align: center;
}
}
}
.link {
color: #007bff;
}
.operation-box {
.link {
margin: 0 5px;
}
}
}

@ -0,0 +1,25 @@
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { SnackbarHOC } from 'educoder';
import CustomLoadable from "../../CustomLoadable";
import {TPMIndexHOC} from "../tpm/TPMIndexHOC";
const Home = CustomLoadable(() => import('./Home/index'));
const EcYear = CustomLoadable(() => import('./EcYear/index'));
class Ecs extends React.Component {
render() {
return (
<div className="newMain clearfix">
<Switch>
<Route extra path='/ecs/department' component={Home}></Route>
<Route extra path='/ecs/major_schools/:majorId' component={EcYear}></Route>
</Switch>
</div>
)
}
}
export default SnackbarHOC() (TPMIndexHOC ( Ecs ));

@ -43,6 +43,12 @@ class AddMajorModal extends React.Component {
}
}
onSearch = () => {
this.setState({ pagination: {...defaultPagination} }, () => {
this.getMajors();
})
}
getMajors(){
let { schoolId, keyword, pagination } = this.state;
@ -55,7 +61,7 @@ class AddMajorModal extends React.Component {
}
}).then(res => {
if(res.status === 200){
const pagination = { ...this.state.pagination };
let pagination = { ...this.state.pagination };
pagination.total = res.data.count;
this.setState({
@ -143,7 +149,7 @@ class AddMajorModal extends React.Component {
<Search
placeholder="专业代码/专业名称检索"
onInput={e => this.setState({keyword: e.target.value})}
onSearch={this.getMajors}
onSearch={this.onSearch}
value={keyword}/>
</div>

@ -22,6 +22,7 @@
}
}
.ant-modal-footer {
padding-bottom: 20px;
text-align: center;
border-top: unset;
}

@ -43,6 +43,12 @@ class AddManagerModal extends React.Component {
this.onPaginationChange = this.onPaginationChange.bind(this);
}
onSearch = () => {
this.setState({ pagination: {...defaultPagination} }, () => {
this.getUsers();
})
}
getUsers(){
let { majorId } = this.props;
let { name, school, identity, pagination } = this.state;
@ -61,7 +67,7 @@ class AddManagerModal extends React.Component {
}
}).then(res => {
if(res.status === 200){
const pagination = { ...this.state.pagination };
let pagination = { ...this.state.pagination };
pagination.total = res.data.count;
this.setState({
@ -186,7 +192,7 @@ class AddManagerModal extends React.Component {
</Form.Item>
</Col>
<Col span={4} offset={2}>
<Button type="primary" className="mt5" onClick={this.getUsers}>搜索</Button>
<Button type="primary" className="mt5" onClick={this.onSearch}>搜索</Button>
</Col>
</Row>
</Form>

@ -28,6 +28,7 @@
}
}
.ant-modal-footer {
padding-bottom: 20px;
text-align: center;
border-top: unset;
}

@ -1,13 +1,12 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Spin, Avatar, Tooltip, Button, Divider, Input, Row, Col, Icon, Modal } from "antd";
import { SnackbarHOC, getImageUrl } from 'educoder';
import { getImageUrl } from 'educoder';
import axios from 'axios';
import './index.scss';
import bgImage from '../../../images/ecs/bg.jpg';
import {TPMIndexHOC} from "../../tpm/TPMIndexHOC";
import MajorManager from "./MajorManager";
import AddMajorModal from "./AddMajorModal";
import AddManagerModal from "./AddManagerModal";
@ -52,13 +51,13 @@ class Home extends React.Component {
}
componentDidMount() {
window.document.title = "专业列表";
this.getSchoolDetail();
}
getSchoolDetail() {
axios.get(`/schools/${this.state.schoolId}/detail.json`).then(result => {
if(result.status === 200){
window.document.title = result.data.school.name;
this.setState({
school: result.data.school,
currentUser: result.data.current_user,
@ -100,15 +99,13 @@ class Home extends React.Component {
HideAddMajorModal(added){
this.setState({ AddMajorVisible: false });
if(added){
this.state.searchKeyword = '';
this.getSchoolMajors();
this.setState({ searchKeyword: '' }, this.getSchoolMajors)
}
}
HideAddManagerModal(added){
this.setState({ AddManagerVisible: false });
if(added){
this.state.searchKeyword = '';
this.getSchoolMajors();
this.setState({ searchKeyword: '' }, this.getSchoolMajors)
}
}
@ -219,7 +216,7 @@ class Home extends React.Component {
</Col>
<Col span={8}></Col>
<Col span={4} className="textcenter">
<a className="link">{ configBtnText }</a>
<Link to={`/ecs/major_schools/${templateMajor.id}`} className="link">{ configBtnText }</Link>
</Col>
</Row>
)
@ -244,7 +241,7 @@ class Home extends React.Component {
</div>
</Col>
<Col span={4} className="textcenter operate-box">
<a className="link">{ configBtnText }</a>
<Link to={`/ecs/major_schools/${major.id}`} className="link">{ configBtnText }</Link>
{ manageSchool && ( <a className="link" onClick={() => this.showDeleteMajorConfirm(major.id)}>删除</a> ) }
</Col>
</Row>
@ -265,4 +262,4 @@ class Home extends React.Component {
}
}
export default SnackbarHOC() (TPMIndexHOC ( Home ));
export default Home;
Loading…
Cancel
Save