diff --git a/app/controllers/ecs/ec_major_schools_controller.rb b/app/controllers/ecs/ec_major_schools_controller.rb index b7af447e2..c5f187af0 100644 --- a/app/controllers/ecs/ec_major_schools_controller.rb +++ b/app/controllers/ecs/ec_major_schools_controller.rb @@ -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| diff --git a/app/controllers/ecs/ec_years_controller.rb b/app/controllers/ecs/ec_years_controller.rb index 2257911a7..896aaed8a 100644 --- a/app/controllers/ecs/ec_years_controller.rb +++ b/app/controllers/ecs/ec_years_controller.rb @@ -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 diff --git a/app/helpers/ecs/ec_years_helper.rb b/app/helpers/ecs/ec_years_helper.rb new file mode 100644 index 000000000..108abb0e7 --- /dev/null +++ b/app/helpers/ecs/ec_years_helper.rb @@ -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 \ No newline at end of file diff --git a/app/models/ec_course_support.rb b/app/models/ec_course_support.rb index a6ca96ea9..c7865f73c 100644 --- a/app/models/ec_course_support.rb +++ b/app/models/ec_course_support.rb @@ -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 diff --git a/app/models/ec_major_school.rb b/app/models/ec_major_school.rb index 41a835f63..5cfc4df9e 100644 --- a/app/models/ec_major_school.rb +++ b/app/models/ec_major_school.rb @@ -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) diff --git a/app/models/ec_year.rb b/app/models/ec_year.rb index 153edcf16..6a3d97340 100644 --- a/app/models/ec_year.rb +++ b/app/models/ec_year.rb @@ -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 diff --git a/app/services/ecs/copy_ec_year_service.rb b/app/services/ecs/copy_ec_year_service.rb index 87cbe0845..462681eba 100644 --- a/app/services/ecs/copy_ec_year_service.rb +++ b/app/services/ecs/copy_ec_year_service.rb @@ -1,4 +1,4 @@ -class CopyEcYearService < ApplicationService +class Ecs::CopyEcYearService < ApplicationService attr_reader :major_school, :to_year def initialize(major_school, year) diff --git a/app/views/ecs/ec_major_schools/show.json.jbuilder b/app/views/ecs/ec_major_schools/show.json.jbuilder new file mode 100644 index 000000000..7c911d824 --- /dev/null +++ b/app/views/ecs/ec_major_schools/show.json.jbuilder @@ -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 \ No newline at end of file diff --git a/app/views/ecs/ec_years/index.json.jbuilder b/app/views/ecs/ec_years/index.json.jbuilder index c5c89cd06..b0a8985b2 100644 --- a/app/views/ecs/ec_years/index.json.jbuilder +++ b/app/views/ecs/ec_years/index.json.jbuilder @@ -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 \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index d043eff96..e1e139c9d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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] diff --git a/db/migrate/20190911080150_change_ec_course_supports.rb b/db/migrate/20190911080150_change_ec_course_supports.rb new file mode 100644 index 000000000..07c3a536d --- /dev/null +++ b/db/migrate/20190911080150_change_ec_course_supports.rb @@ -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 diff --git a/public/react/src/App.js b/public/react/src/App.js index 6b7a74cd4..ddb8de809 100644 --- a/public/react/src/App.js +++ b/public/react/src/App.js @@ -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)=>() }/> - () + (props)=>() }/> diff --git a/public/react/src/AppConfig.js b/public/react/src/AppConfig.js index 099b9ddc0..5d2373e84 100644 --- a/public/react/src/AppConfig.js +++ b/public/react/src/AppConfig.js @@ -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 = {}; diff --git a/public/react/src/modules/ecs/EcYear/AddYearModal.js b/public/react/src/modules/ecs/EcYear/AddYearModal.js new file mode 100644 index 000000000..9e91e34ec --- /dev/null +++ b/public/react/src/modules/ecs/EcYear/AddYearModal.js @@ -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 ( +
+ + +
+
+ 基础数据:除学生列表与成绩录入以外的所有基础数据
+ 将自动复制上届别的数据;数据均可再编辑 +
+ +
+
选择届别:
+
+ +
+
+
+
+
+ ) + } +} + +AddYearModal.propTypes = { + schoolId: PropTypes.number, + majorId: PropTypes.number, + visible: PropTypes.bool, + onHide: PropTypes.func +} + +export default AddYearModal \ No newline at end of file diff --git a/public/react/src/modules/ecs/EcYear/AddYearModal.scss b/public/react/src/modules/ecs/EcYear/AddYearModal.scss new file mode 100644 index 000000000..75218677d --- /dev/null +++ b/public/react/src/modules/ecs/EcYear/AddYearModal.scss @@ -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; + } +} \ No newline at end of file diff --git a/public/react/src/modules/ecs/EcYear/index.js b/public/react/src/modules/ecs/EcYear/index.js new file mode 100644 index 000000000..bb3b3bfe0 --- /dev/null +++ b/public/react/src/modules/ecs/EcYear/index.js @@ -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 { num === 0 ? "立即配置" : num }; + } + const contrastRender = (num, other) => { + let color = other !== 0 && num === other ? 'color-green' : 'color-orange'; + + return other === 0 ? ( +
-- / --
+ ) : ( +
{num} / {other}
+ ) + } + const statusRender = (text, record) => { + let zero = record.graduation_subitem_count === 0; + + return zero ? ( + -- + ) : ( + + { text === 'achieved' ? '已达成' : '未达成' } + + ) + } + const operationRender = (_, record) => { + return ( +
+ 立即配置 + this.showDeleteYearConfirm(record.id)}>删除 +
+ ) + } + 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 ( +
+ +
+
+
+
{ major.name }
+
+ 请选择添加参与认证的学生界别,多个界别分次添加 + 查看详情 +
+
+ +
+ + + +
+
+ this.setState({keyword: e.target.value})} + onSearch={this.onSearch} + value={keyword} + style={{ width: 200 }}/> +
+ +
+ + + + + + + + + ) + } +} + +export default EcYear; \ No newline at end of file diff --git a/public/react/src/modules/ecs/EcYear/index.scss b/public/react/src/modules/ecs/EcYear/index.scss new file mode 100644 index 000000000..a0b96541d --- /dev/null +++ b/public/react/src/modules/ecs/EcYear/index.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/public/react/src/modules/ecs/Ecs.js b/public/react/src/modules/ecs/Ecs.js new file mode 100644 index 000000000..307754e61 --- /dev/null +++ b/public/react/src/modules/ecs/Ecs.js @@ -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 ( +
+ + + + +
+ ) + } +} + +export default SnackbarHOC() (TPMIndexHOC ( Ecs )); \ No newline at end of file diff --git a/public/react/src/modules/ecs/Home/AddMajorModal.js b/public/react/src/modules/ecs/Home/AddMajorModal.js index f1282e244..9b58ed7e4 100644 --- a/public/react/src/modules/ecs/Home/AddMajorModal.js +++ b/public/react/src/modules/ecs/Home/AddMajorModal.js @@ -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 { this.setState({keyword: e.target.value})} - onSearch={this.getMajors} + onSearch={this.onSearch} value={keyword}/> diff --git a/public/react/src/modules/ecs/Home/AddMajorModal.scss b/public/react/src/modules/ecs/Home/AddMajorModal.scss index f79576eab..bde678bcd 100644 --- a/public/react/src/modules/ecs/Home/AddMajorModal.scss +++ b/public/react/src/modules/ecs/Home/AddMajorModal.scss @@ -22,6 +22,7 @@ } } .ant-modal-footer { + padding-bottom: 20px; text-align: center; border-top: unset; } diff --git a/public/react/src/modules/ecs/Home/AddManagerModal.js b/public/react/src/modules/ecs/Home/AddManagerModal.js index 99dc6cd8e..7e341ea11 100644 --- a/public/react/src/modules/ecs/Home/AddManagerModal.js +++ b/public/react/src/modules/ecs/Home/AddManagerModal.js @@ -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 {
- + diff --git a/public/react/src/modules/ecs/Home/AddManagerModal.scss b/public/react/src/modules/ecs/Home/AddManagerModal.scss index b3c39f1f4..2b30690fc 100644 --- a/public/react/src/modules/ecs/Home/AddManagerModal.scss +++ b/public/react/src/modules/ecs/Home/AddManagerModal.scss @@ -28,6 +28,7 @@ } } .ant-modal-footer { + padding-bottom: 20px; text-align: center; border-top: unset; } diff --git a/public/react/src/modules/ecs/Home/index.js b/public/react/src/modules/ecs/Home/index.js index 30fbf64f5..8f66b6572 100644 --- a/public/react/src/modules/ecs/Home/index.js +++ b/public/react/src/modules/ecs/Home/index.js @@ -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 { - { configBtnText } + { configBtnText } ) @@ -244,7 +241,7 @@ class Home extends React.Component { - { configBtnText } + { configBtnText } { manageSchool && ( this.showDeleteMajorConfirm(major.id)}>删除 ) } @@ -265,4 +262,4 @@ class Home extends React.Component { } } -export default SnackbarHOC() (TPMIndexHOC ( Home )); \ No newline at end of file +export default Home; \ No newline at end of file