ecs: training_objectives page

dev_cs
p31729568 6 years ago
parent ac67cdafad
commit 0ed5b1d79f

@ -2,7 +2,7 @@ class Ecs::EcTrainingObjectivesController < Ecs::BaseController
before_action :check_major_manager_permission!, only: [:create]
def show
@training_objective = current_year.ec_training_objective
@training_objective = current_year.ec_training_objective || current_year.build_ec_training_objective
respond_to do |format|
format.json

@ -27,6 +27,10 @@ class Ecs::EcYearsController < Ecs::BaseController
.where(ec_graduation_requirements: { ec_year_id: year_ids }).group('ec_year_id').count
end
def show
@year = current_year
end
def create
if current_major_school.ec_years.exists?(year: params[:year].to_i)
render_error('届别已存在')

@ -1,4 +1,6 @@
class EcTrainingSubitem < ApplicationRecord
default_scope { order(position: :asc) }
belongs_to :ec_training_objective
has_many :ec_requirement_vs_objectives, foreign_key: :ec_training_objective_id, dependent: :destroy

@ -11,13 +11,16 @@ class Ecs::CreateTrainingObjectiveService < ApplicationService
def call
training_objective.content = params[:content].to_s.strip
attributes = build_accepts_nested_attributes(
training_objective,
training_objective.ec_training_subitems,
params[:training_subitems],
&method(:training_subitem_param_handler)
)
training_objective.assign_attributes(ec_training_subitems_attributes: attributes)
if params.key?(:training_subitems)
attributes = build_accepts_nested_attributes(
training_objective,
training_objective.ec_training_subitems,
params[:training_subitems],
&method(:training_subitem_param_handler)
)
attributes.each_with_index { |attr, index| attr[:position] = index + 1 }
training_objective.assign_attributes(ec_training_subitems_attributes: attributes)
end
training_objective.save!
training_objective

@ -1,3 +1,3 @@
json.extract! ec_training_objective, :id, :content
json.ec_training_items ec_training_objective.ec_training_subitems, partial: 'ec_training_subitem', as: :ec_training_subitem
json.ec_training_items ec_training_objective.ec_training_subitems, partial: '/ecs/ec_training_objectives/shared/ec_training_subitem', as: :ec_training_subitem

@ -1 +1 @@
json.partial! 'shared/ec_training_objective', ec_training_objective: @training_objective
json.partial! '/ecs/ec_training_objectives/shared/ec_training_objective', ec_training_objective: @training_objective

@ -6,11 +6,11 @@ wb = xlsx_package.workbook
wb.styles do |style|
title_style = style.add_style(sz: 16, height: 20, b: true)
ec_year_style = style.add_style(sz: 10, height: 14)
label_style = style.add_style(sz: 11, b: true, bg_color: '90EE90', alignment: { horizontal: :center })
label_style = style.add_style(sz: 11, b: true, bg_color: '90EE90', border: { style: :thin, color: '000000' }, alignment: { horizontal: :center, vertical: :center })
content_style = style.add_style(sz: 11, height: 16, border: { style: :thin, color: '000000' })
wb.add_worksheet(:name => '培养目标及目标分解') do |sheet|
sheet.add_row '培养目标及目标分解', style: title_style
sheet.add_row ['培养目标及目标分解'], style: title_style
sheet.add_row []
sheet.add_row []
@ -27,6 +27,6 @@ wb.styles do |style|
end
items_size = training_objective.ec_training_subitems.size
sheet.merge_cells("A9:A#{9 + items_size}")
sheet.merge_cells("A9:A#{9 + items_size - 1}")
end
end

@ -0,0 +1,13 @@
json.extract! @year, :id, :year
major = @year.ec_major_school
json.major_id major.id
json.major_name major.name
json.major_code major.code
school = major.school
json.school_id school.id
json.school_name school.name
can_manager = major.manager?(current_user) || school.manager?(current_user) || current_user.admin_or_business?
json.can_manager can_manager

@ -710,7 +710,7 @@ Rails.application.routes.draw 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]
resources :ec_years, only: [:index, :show, :create, :destroy]
end
resources :ec_years, only: [] do

@ -0,0 +1,72 @@
import React from 'react';
import PropTypes from "prop-types";
import { Link } from 'react-router-dom';
import { Spin, Button, Input, Divider, Icon, Tooltip, Form, message } from 'antd';
import axios from 'axios';
import './index.scss';
class GraduationRequirement extends React.Component {
constructor (props) {
super(props);
this.state = {
loading: true,
graduationRequirements: []
}
}
componentDidMount() {
this.getData();
}
getData = () => {
let { yearId } = this.props;
axios.get(`/ec_years/${yearId}/ec_graduation_requirements.json`).then(res => {
if(res.status === 200){
this.setState({
graduationRequirements: res.data.ec_graduation_requirements,
loading: false
})
}
}).catch(e => console.log(e))
}
render() {
let { can_manager } = this.props.year;
let { loading } = this.state;
return (
<div>
<Spin spinning={loading} size='large' style={{ marginTop: '15%' }}>
<div className="educontent ec-graduation-requirement-page">
<div className="ec-head">
<div className="ec-head-left">
<div className="ec-head-label">毕业要求(及其指标点)</div>
<div className="ec-head-tip">
<span>请结合本专业特色修改毕业要求文字描述及指标点需完全覆盖12项通用标准</span>
<Link to="/forums/3530" target="_blank" className="link ml10">查看详情</Link>
</div>
</div>
<a href={`/api/ec_years/${this.props.yearId}/ec_graduation_requirements.xlsx`} target="_blank" className="ant-btn ant-btn-primary color-white">导出毕业要求</a>
</div>
<Divider/>
<div className="graduation-requirement-body">
</div>
</div>
</Spin>
</div>
)
}
}
GraduationRequirement.propTypes = {
schoolId: PropTypes.string,
majorId: PropTypes.string,
yearId: PropTypes.string,
}
export default GraduationRequirement

@ -0,0 +1,264 @@
import React from 'react';
import PropTypes from "prop-types";
import { Link } from 'react-router-dom';
import { Spin, Button, Input, Divider, Icon, Tooltip, Form, message } from 'antd';
import axios from 'axios';
import './index.scss';
class TrainingObjective extends React.Component {
constructor (props) {
super(props);
this.state = {
loading: true,
contentEditState: false,
itemsEditState: false,
submitState: false,
validateState: false,
itemSubmitState: false,
itemValidateState: false,
objective: {},
editContent: '',
trainingSubitems: []
}
}
componentDidMount() {
this.getData();
}
getData = () => {
let { yearId } = this.props;
axios.get(`/ec_years/${yearId}/ec_training_objectives.json`).then(res => {
if(res.status === 200){
this.setState({
objective: res.data,
editContent: res.data.content,
trainingSubitems: res.data.ec_training_items,
loading: false
})
}
}).catch(e => console.log(e))
}
saveContentEdit = () => {
let { editContent } = this.state;
this.setState({ validateState: editContent.length === 0 });
if(editContent.length === 0){ return; }
this.setState(
{ submitState: true },
() => {
this.updateTrainingObjective(
{ content: editContent },
() => {
this.setState({ submitState: false, contentEditState: false });
this.getData();
},
_e => {
this.setState({ submitState: false })
}
)
}
);
}
cancelContentEdit = () => {
this.setState({ editContent: this.state.objective.content, contentEditState: false });
}
editItemsContent = () => {
let { trainingSubitems } = this.state;
if(!trainingSubitems || trainingSubitems.length === 0){
trainingSubitems = [{ id: null, content: null }]
}
this.setState({ trainingSubitems: trainingSubitems, itemsEditState: true });
}
addItemColumn = (index) => {
let { trainingSubitems } = this.state;
trainingSubitems.splice(index, 0, { id: null, content: null });
this.setState({ trainingSubitems })
}
removeItemColumn = (index) => {
let { trainingSubitems } = this.state;
trainingSubitems.splice(index, 1);
this.setState({ trainingSubitems })
}
onItemContentChange = (e, index) => {
let { trainingSubitems } = this.state;
trainingSubitems[index].content = e.target.value;
this.setState({ trainingSubitems: trainingSubitems });
}
saveItemsContentEdit = () => {
let { objective, trainingSubitems } = this.state;
let errorItem = trainingSubitems.find(item => !item.content || item.content.length === 0);
this.setState({ itemValidateState: !!errorItem });
if(errorItem){ return }
this.setState(
{ itemSubmitState: true },
() => {
this.updateTrainingObjective(
{ content: objective.content, training_subitems: trainingSubitems },
() => {
this.setState({ itemSubmitState: false, itemsEditState: false });
this.getData();
},
_e => {
this.setState({ itemSubmitState: false })
}
)
}
);
}
cancelItemsContentEdit = () => {
this.setState({ trainingSubitems: this.state.objective.ec_training_items, itemsEditState: false, itemValidateState: false });
}
updateTrainingObjective = (data, success, fail) => {
let { yearId } = this.props;
let url = `/ec_years/${yearId}/ec_training_objectives.json`;
axios.post(url, data).then(res => {
if(res){
message.success('操作成功');
success();
}
}).catch(e => {
console.log(e);
fail(e);
})
}
render() {
let { can_manager } = this.props.year;
let { loading, contentEditState, itemsEditState, objective, editContent, trainingSubitems, validateState, itemValidateState, itemSubmitState, submitState } = this.state;
return (
<div>
<Spin spinning={loading} size='large' style={{ marginTop: '15%' }}>
<div className="educontent ec-training-objective-page">
<div className="ec-head">
<div className="ec-head-left">
<div className="ec-head-label">培养目标</div>
<div className="ec-head-tip">
<span>请结合本专业特色修改培养目标文字描述及目标分解查看详情</span>
<Link to="/forums/3529" target="_blank" className="link ml10">查看详情</Link>
</div>
</div>
<a href={`/api/ec_years/${this.props.yearId}/ec_training_objectives.xlsx`} target="_blank" className="ant-btn ant-btn-primary color-white">导出培养目标</a>
</div>
<Divider/>
<div className="training-objective-body">
{
can_manager && contentEditState ? (
<div className="training-objective-content block">
<div>
<Form.Item label={false} validateStatus={validateState && (!editContent || editContent.length === 0) ? 'error' : ''}>
<Input.TextArea rows={6} value={editContent} onChange={e => this.setState({ editContent: e.target.value })} />
</Form.Item>
</div>
<div className="training-objective-content-form">
<Button type="primary" loading={submitState} onClick={this.saveContentEdit}>保存</Button>
<Button loading={submitState} onClick={this.cancelContentEdit}>取消</Button>
</div>
</div>
) : (
<div className="training-objective-content">
<div className="training-objective-content-text">{ objective.content }</div>
{
can_manager && (
<div className="training-objective-content-edit">
<Tooltip title="编辑">
<Icon type="edit" theme="filled" className="edit-action" onClick={() => this.setState({ contentEditState: true })} />
</Tooltip>
</div>
)
}
</div>
)
}
<div className="training-objective-items">
<div className="training-objective-items-head">
<div className="no-column">分项</div>
<div className="item-content-column">目标分解详情</div>
<div className="operation-column">
{
itemsEditState || (
<Tooltip title="编辑">
<Icon type="edit" theme="filled" className="edit-action" onClick={this.editItemsContent} />
</Tooltip>
)
}
</div>
</div>
<div className="training-objective-items-body">
{
can_manager && itemsEditState ? (
<div>
{
trainingSubitems && trainingSubitems.map((item, index) => {
return (
<div className="training-objective-items-body-item" key={index}>
<div className="no-column">{index + 1}</div>
<div className="item-content-column">
<Form.Item label={false} validateStatus={itemValidateState && (!item.content || item.content.length === 0) ? 'error' : ''}>
<Input.TextArea rows={2} value={item.content} onChange={e => this.onItemContentChange(e, index)} />
</Form.Item>
<div className="item-column-operation">
{ index !== 0 && <Icon type="delete" onClick={() => this.removeItemColumn(index)} /> }
<Icon type="plus-circle" onClick={() => this.addItemColumn(index + 1)} style={{ color: '#29BD8B' }} />
</div>
</div>
</div>
)
})
}
<div className="training-objective-content-form">
<Button type="primary" loading={itemSubmitState} onClick={this.saveItemsContentEdit}>保存</Button>
<Button disabled={itemSubmitState} onClick={this.cancelItemsContentEdit}>取消</Button>
</div>
</div>
) : (
objective.ec_training_items && objective.ec_training_items.map((item, index) => {
return (
<div className="training-objective-items-body-item" key={index}>
<div className="no-column">{ index + 1 }</div>
<div className="item-content-column">{ item.content }</div>
</div>
)
})
)
}
</div>
</div>
</div>
</div>
</Spin>
</div>
)
}
}
TrainingObjective.propTypes = {
schoolId: PropTypes.string,
majorId: PropTypes.string,
yearId: PropTypes.string,
}
export default TrainingObjective

@ -0,0 +1,99 @@
.ec-training-objective-page {
background: #ffffff;
.training-objective {
&-body {
margin-top: -24px;
min-height: 600px;
}
&-content {
display: flex;
padding: 20px 30px;
line-height: 2.0;
&.block {
display: block;
}
&-text {
flex: 1;
}
&-edit {
margin-left: 20px;
& > i {
color: #29BD8B;
cursor: pointer;
font-size: 18px;
}
}
&-form {
margin-top: 10px;
text-align: right;
button {
margin-left: 10px;
}
}
}
&-items {
&-head {
padding: 15px 30px;
display: flex;
background: #F5F5F5;
}
&-body {
margin: 0 30px;
&-item {
display: flex;
min-height: 48px;
padding: 10px 0px;
border-bottom: 1px solid #eaeaea;
&:last-child {
border-bottom: unset;
}
}
}
.no-column {
width: 40px;
text-align: center;
}
.item-content-column {
flex: 1;
padding-left: 10px;
display: flex;
.ant-form-item {
flex: 1;
margin-bottom: 0;
}
.item-column-operation {
display: flex;
justify-content: flex-end;
width: 80px;
& > i {
margin: 15px 10px;
font-size: 18px;
}
}
}
}
}
i.edit-action {
color: #29BD8B;
cursor: pointer;
font-size: 18px;
}
}

@ -0,0 +1,110 @@
import React from 'react';
import { Switch, Route, Link } from 'react-router-dom';
import { Steps, Breadcrumb } from 'antd';
import axios from 'axios';
import './index.scss';
import CustomLoadable from "../../../CustomLoadable";
const { Step } = Steps;
const TrainingObjective = CustomLoadable(() => import('./TrainingObjective/index'))
const steps = ["培养目标", "毕业要求", "培养目标VS毕业要求", "毕业要求VS通用标准", "学生", "课程体系", "课程体系VS毕业要求", "达成度评价结果"];
const stepTypes = ["training_objectives", "graduation_requirement", "requirement_vs_objective", "requirement_vs_standard", "students", "courses", "requirement_vs_courses", "reach_calculation_info"];
class EcSetting extends React.Component {
constructor (props) {
super(props);
this.state = {
schoolId: null,
majorId: props.match.params.majorId,
yearId: props.match.params.yearId,
year: null,
stepIndex: 0,
}
}
componentDidMount() {
this.setupStep();
this.getYearDetail();
}
getYearDetail = () => {
let { majorId, yearId } = this.state;
axios.get(`/ec_major_schools/${majorId}/ec_years/${yearId}.json`).then(res => {
if(res){
this.setState({ year: res.data, schoolId: res.data.school_id })
}
}).catch(e => console.log(e))
}
onStepChange = (stepIndex) => {
let { majorId, yearId } = this.state;
let type = stepTypes[stepIndex];
this.setState({ stepIndex: stepIndex });
this.props.history.push(`/ecs/major_schools/${majorId}/years/${yearId}/${type}`);
}
setupStep = () => {
let type = this.props.match.params.type;
let stepIndex = stepTypes.indexOf(type);
this.setState({ stepIndex: stepIndex });
}
render() {
let { year, schoolId, majorId, yearId } = this.state;
let { stepIndex } = this.state;
return (
<div>
<div className="ec-page">
<div className="educontent ec-breadcrumb">
<Breadcrumb separator=">">
<Breadcrumb.Item key="department">
<Link to={`/ecs/department?school_id=${schoolId}`}>{ year && year.school_name }</Link>
</Breadcrumb.Item>
<Breadcrumb.Item key="major-school">
<Link to={`/ecs/major_schools/${majorId}`}>{ year && year.major_name }</Link>
</Breadcrumb.Item>
<Breadcrumb.Item key="year">{year && year.year}</Breadcrumb.Item>
</Breadcrumb>
</div>
<div className="educontent ec-steps">
<Steps type="navigation"
size='small'
className="ec-steps-box"
current={stepIndex}
onChange={this.onStepChange}>
{
steps.map((title, index) => {
return (
<Step subTitle={title} status={index === stepIndex ? 'process' : 'wait'} key={index}/>
)
})
}
</Steps>
</div>
{
year && (
<Switch>
<Route extra path='/ecs/major_schools/:majorId/years/:yearId/training_objectives'
render={ (props) => (<TrainingObjective {...this.props} {...props} {...this.state} />) }></Route>
</Switch>
)
}
</div>
</div>
)
}
}
export default EcSetting

@ -0,0 +1,62 @@
.ec-page {
margin-bottom: 50px;
.ec-breadcrumb {
margin-top: 10px;
margin-bottom: 10px;
}
.ec-steps {
&-box {
margin-bottom: 10px;
padding-left: 20px;
padding-right: 20px;
justify-content: space-between;
background: #fff;
.ant-steps-item {
flex: unset;
&-container {
margin-left: 0;
}
&.ant-steps-item-active {
.ant-steps-item-subtitle {
color: #1890ff;
}
}
&-subtitle {
margin-left: 0;
margin-right: 8px;
}
}
}
}
.ec-head {
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;
}
}
.link {
color: #007bff;
}
}

@ -70,7 +70,7 @@ class AddYearModal extends React.Component {
onOk={this.handleOk}
onCancel={this.handleCancel}>
<div class="add-year-container">
<div className="add-year-container">
<div className="add-year-tip">
基础数据除学生列表与成绩录入以外的所有基础数据<br/>
将自动复制上届别的数据数据均可再编辑

@ -1,7 +1,7 @@
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 { Spin, Button, Table, Input, Divider, Modal, message, Breadcrumb } from 'antd';
import './index.scss';
import AddYearModal from "./AddYearModal";
@ -141,16 +141,16 @@ class EcYear extends React.Component {
const operationRender = (_, record) => {
return (
<div className="operation-box">
<Link to={`/ecs/major_schools/${majorId}/academic_years/${record.id}/training_objectives`} className="link">立即配置</Link>
<Link to={`/ecs/major_schools/${majorId}/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: '培养目标', dataIndex: 'training_subitem_count', render: (text, record) => linkRender(text, `/ecs/major_schools/${this.state.majorId}/years/${record.id}/training_objectives`), },
{ title: '毕业要求', dataIndex: 'graduation_requirement_count', render: (text, record) => linkRender(text, `/ecs/major_schools/${this.state.majorId}/years/${record.id}/graduation_requirement`), },
{ title: '课程体系', dataIndex: 'course_count', render: (text, record) => linkRender(text, `/ecs/major_schools/${this.state.majorId}/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 },
@ -161,35 +161,46 @@ class EcYear extends React.Component {
<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 className="educontent ec-breadcrumb">
<Breadcrumb separator=">">
<Breadcrumb.Item key="department">
<Link to={`/ecs/department?school_id=${major && major.school_id}`}>{ major && major.school_name }</Link>
</Breadcrumb.Item>
<Breadcrumb.Item key="major-school">{ major && major.name }</Breadcrumb.Item>
</Breadcrumb>
</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 className="ec-year-list-container">
<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>
<div className="year-list-table">
<Table rowKey="id"
loading={loading}
columns={tableColumns}
dataSource={yearData}
pagination={{...pagination, onChange: this.onPaginationChange}}/>
<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>
</div>

@ -1,9 +1,15 @@
.ec-year-list-page {
background: #fff;
.ec-breadcrumb {
margin-top: 10px;
margin-bottom: 10px;
}
.ec-year-list-container {
background: #fff;
}
.year-list {
&-head {
margin-top: 30px;
margin-bottom: -24px;
padding: 20px 30px;
display: flex;

@ -7,6 +7,7 @@ import {TPMIndexHOC} from "../tpm/TPMIndexHOC";
const Home = CustomLoadable(() => import('./Home/index'));
const EcYear = CustomLoadable(() => import('./EcYear/index'));
const EcSetting = CustomLoadable(() => import('./EcSetting/index'));
class Ecs extends React.Component {
@ -15,6 +16,7 @@ class Ecs extends React.Component {
<div className="newMain clearfix">
<Switch>
<Route extra path='/ecs/department' component={Home}></Route>
<Route path='/ecs/major_schools/:majorId/years/:yearId/:type' component={EcSetting}></Route>
<Route extra path='/ecs/major_schools/:majorId' component={EcYear}></Route>
</Switch>
</div>

Loading…
Cancel
Save