Show username on student-level outcomes reports

This helps disambiguate when the user's broad name is common or not
entered correctly.

<script>
import { mapGetters, mapActions, mapState } from 'vuex'
import utils from 'core/utils'

export default Vue.extend({
  name: 'outcomes-report-result-component',
  props: {
    org: {
      type: Object,
      required: true
    },
    isSubOrg: {
      type: Boolean,
      default: false
    },
    editing: {
      type: Boolean,
      default: false
    },
    parentOrgKind: {
      type: String,
      default: null
    },
  },

  data () {
    return {
      // TODO: some bug where printing gets large horizontal margins after we customize included schools and finish editing
      included: !this.isSubOrg || this.org.initiallyIncluded
    }
  },

  computed: {
    ...mapState('courses', {
      coursesLoaded: 'loaded',
      sortedCourses: (state) => utils.sortCourses(_.values(state.byId))
    }),

    kindString () {
      return utils.orgKindString(this.org.kind, this.org)
    },

    codeLanguageString () {
      return utils.capitalLanguages[this.org.codeLanguage] || ''
    },

    codeLanguageStats () {
      if (!this.org.progress) return
      let languageStats = {}
      let totalPrograms = 0
      for (let language of ['python', 'javascript', 'cpp']) {
        let programs = this.org.progress.programsByLanguage[language]
        totalPrograms += programs
        languageStats[language] = { programs }
      }
      for (let [language, stats] of Object.entries(languageStats)) {
        stats.percentage = Math.round(100 * stats.programs / totalPrograms)
        stats.name = utils.capitalLanguages[language]
        stats.language = language
      }
      languageStats = _.omit(languageStats, (s) => !s.programs || s.programs < totalPrograms * 0.005)  // Skip low usage languages
      languageStats = Object.values(languageStats).sort((a, b) => b.programs - a.programs)  // Sorted array
      let otherLanguagesCumulativePercentage = 0
      for (let stats of languageStats) {
        // Keep track of how much all languages have contributed so that we can draw our pie chart
        stats.otherLanguagesCumulativePercentage = otherLanguagesCumulativePercentage
        otherLanguagesCumulativePercentage += stats.percentage
      }
      return languageStats
    },

    coursesWithProgress () {
      if (!this.org.progress) return []
      let courses = _.cloneDeep(this.sortedCourses)
      let alreadyCoveredConcepts = []
      for (let course of courses) {
        course.name = utils.i18n(course, 'name')
        course.acronym = utils.courseAcronyms[course._id]
        course.studentsStarting = (this.org.progress.studentsStartingCourse || {})[course._id] || 0
        course.studentsCompleting = (this.org.progress.studentsCompletingCourse || {})[course._id] || 0
        course.completion = course.studentsStarting ? Math.min(1, course.studentsCompleting / course.studentsStarting) : 0
        course.newConcepts = _.difference(course.concepts, alreadyCoveredConcepts)
        alreadyCoveredConcepts = _.union(course.concepts, alreadyCoveredConcepts)
      }
      courses = _.filter(courses, (course) => (course.studentsStarting + course.studentsCompleting) >= Math.min(100, Math.max(2, Math.ceil(0.02 * this.org.progress.studentsWithCode))))
      return courses
    },

    mapUrl() {
      if (!this.org.address) return null
      let orgName = this.org.displayName || this.org.name
      if (this.org.kind === 'school-district' && !/school.*district|isd|usd|unified/.test(orgName))
        orgName += ' School District'
      const addresses = [orgName + ', ' + this.org.address]
      for (let subOrg of this.org.subOrgs || []) {
        if (subOrg.address) {
          let subOrgName = subOrg.displayName || subOrg.name
          if (subOrg.kind === 'school-district' && !/school.*district|isd|usd|unified/.test(subOrgName))
            subOrgName += ' School District'
          addresses.push(subOrgName + ', ' + subOrg.address)
        }
      }
      // One address, do a search-type link
      let url = `https://www.google.com/maps/search/?api=1&query=${addresses.map(a => encodeURIComponent(a))}`
      if (addresses.length > 1) {
        // Multiple addresses, abuse direction-type link to show them all
        // Could add &destination=${encodeURIComponent(addresses[0])} to activate the driving directions, but probably better without
        let urlWithSubOrgs = `https://www.google.com/maps/dir/?api=1&origin=${encodeURIComponent(addresses[0])}&waypoints=${addresses.slice(1).map(a => encodeURIComponent(a)).join('|')}`
        if (urlWithSubOrgs.length < 8192)
          url = urlWithSubOrgs
      }
      return url
    },

    isAdmin() {
      return me.isAdmin()
    },

    isSchoolAdmin() {
      return me.isSchoolAdmin()
    },
  },

  created () {
    this.fetchCourses()
  },

  // TODO: figure out how to sync included status back to the parent
  // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier

  mounted () {
  },

  methods: {
    ...mapActions({
      fetchCourses: 'courses/fetch',
    }),

    phoneString (phone) {
      if (!/[0-9]{10}/.test(phone)) return phone
      return `(${phone.slice(0, 3)}) ${phone.slice(3, 6)}-${phone.slice(6, 10)}`
    },

    formatNumber (num) {
      if (num <         10000) return num.toLocaleString()
      if (num <        999500) return Math.round(num / 1000) + 'K'
      if (num <      10000000) return (num / 1000000).toFixed(1) + 'M'
      if (num <     999500000) return Math.round(num / 1000000) + 'M'
      if (num <   10000000000) return (num / 1000000000).toFixed(1) + 'B'
      if (num <  999500000000) return Math.round(num / 1000000000) + 'B'
      return Math.round(num / 1000000000000) + 'T'
    }
  }
});
</script>

<template lang="pug">
.outcomes-report-result-component(v-if="included || editing")
  .page-break(v-if="isSubOrg && included")
    hr

  .address
    p
      b
        if org.archived
          em Archived:
        else
          span #{kindString}:
      span= " "
      if isSubOrg
        a(:href="'/outcomes-report/' + org.kind + '/' + org._id" target="_blank")
          b= org.displayName || org.name
      else
        b= org.displayName || org.name
        if included && isAdmin && editing && org.kind == 'school-district' && org['administrative-region']
          span ,
          a(:href="'/outcomes-report/administrative-region/' + org['administrative-region'].region.toLowerCase()" target="_blank")= org['administrative-region'].region
      if org.email
        span  (#{org.email})
      if org.kind == 'student' && org.displayName && org.name && (org.name.replace(/\W/g, '').toLowerCase() != org.displayName.replace(/\W/g, '').toLowerCase())
        span  (#{org.name})
      if codeLanguageString
        span  (#{codeLanguageString})
      if !included && org.progress && org.progress.studentsWithCode && org.kind != 'student'
        span  - #{org.progress.studentsWithCode} students
      label.edit-label.editing-only(:for="'includeOrg-' + org._id" v-if="editing && isSubOrg")
        span
        input(:id="'includeOrg-' + org._id" name="'includeOrg-' + org._id" type="checkbox" v-model="included")
        span  include
      if included && isAdmin && editing
        span(v-if="org.address")
          br
          span= org.address
          span
            span= ' '
            a(:href="mapUrl" target="_blank" rel="nofollow") (map)
        //if org.geo && org.geo.county
        //  br
        //  span County: #{org.geo.county}
        if org.phone || org.website
          br
        if org.phone
          span= 'Phone: '
          a(:href="'tel:' + org.phone")= phoneString(org.phone)
          span
        if org.website
          span= ' Website: '
          a(:href="org.website" rel="nofollow" target="_blank")= org.website
      if included && org.students && org.kind == 'course' && parentOrgKind != 'student'
        for student in org.students
          br
          span= 'Student: '
          a(:href="'/outcomes-report/student/' + student._id" target="_blank")
            b= student.displayName
      if included && org.classrooms && org.kind == 'student' && parentOrgKind != 'classroom'
        for classroom in org.classrooms
          br
          span= 'Classroom: '
          a(:href="'/outcomes-report/classroom/' + classroom._id" target="_blank")
            b= classroom.name
          span  #{classroom.codeLanguage} - #{formatNumber(classroom.studentCount)} students
      if included && org.teachers && ['classroom', 'student'].indexOf(org.kind) != -1 && ['teacher', 'classroom'].indexOf(parentOrgKind) == -1
        for teacher in org.teachers
          br
          span= 'Teacher: '
          a(:href="'/outcomes-report/teacher/' + teacher._id" target="_blank")
            b= teacher.displayName
          if teacher.email
            span  (#{teacher.email})
      if included && org.schools && org.kind == 'teacher' && parentOrgKind != 'school'
        for school in org.schools
          br
          span= 'School: '
          a(:href="'/outcomes-report/school/' + school._id" target="_blank")
            b= school.name
      if included && org['school-district'] && ['school', 'teacher', 'school-admin'].indexOf(org.kind) != -1 && ['school-district', 'school'].indexOf(parentOrgKind) == -1 && (isSchoolAdmin || (isAdmin && (editing || org.kind != 'teacher')))
        br
        span= 'District: '
        a(:href="'/outcomes-report/school-district/' + org['school-district']._id" target="_blank")
          b= org['school-district'].name
      if included && org['school-admin'] && ['school', 'teacher'].indexOf(org.kind) != -1 && parentOrgKind != 'school-admin' && org['school-admin'].displayName != 'Anonymous'
        br
        span= 'Admin: '
        a(:href="'/outcomes-report/school-admin/' + org['school-admin']._id" target="_blank")
          b= org['school-admin'].displayName

  .block(v-if="included && coursesLoaded && coursesWithProgress[0] && coursesWithProgress[0].studentsStarting > 1" :class="isSubOrg && coursesWithProgress.length > 1 ? 'dont-break' : ''")
    h1 Course Progress
    for course in coursesWithProgress
      if !isSubOrg || coursesWithProgress.length == 1
        .course.dont-break.full-row
          h3= course.name
          .bar
            .el
              svg(width=100, height=100)
                - var radius = 50
                circle.bottom(r=radius,cx=radius, cy=radius)
                circle.top(r=radius / 2, cx=radius, cy=radius, :style="'stroke-dasharray: ' + 3.1415926 * 50 * course.completion + 'px ' + 3.1415926 * 50 + 'px'")
            .el
              .big #{Math.round(100 * course.completion)}%
              .under complete
            .el(v-if="org.kind != 'student'")
              .big #{formatNumber(course.studentsStarting)}
              .under= course.studentsStarting === 1 ? "student" : "students"
            .el.concepts-list
              b Key Concepts:
              ul
                li(v-for="concept in course.newConcepts")
                  span {{$t('concepts.' + concept)}}
      else
        .course.inline
          .el
            svg(width=100, height=100)
              - var radius = 50
              circle.bottom(r=radius,cx=radius,cy=radius)
              circle.top(r=radius / 2, cx=radius, cy=radius, :style="'stroke-dasharray: ' + 3.1415926 * 50 * course.completion + 'px ' + 3.1415926 * 50 + 'px'")
            if org.kind != 'student'
              .overlay-text.top-text #{formatNumber(course.studentsStarting)} #{course.studentsStarting === 1 ? "student" : "students"}
            .overlay-text.mid-text= course.acronym
            .overlay-text.bot-text #{Math.round(100 * course.completion)}% complete

  .block(v-if="org.progress && org.progress.programs > 1 && included && codeLanguageStats.length > 1 && !codeLanguageString")
    // TODO: somehow note the code language if we are skipping this section (like 100% Python at school level)
    .course.dont-break.full-row
      h3 Code Languages
      .bar
        .el
          svg(width=100, height=100)
            - var radius = 50
            circle.bottom(r=radius,cx=radius, cy=radius)
            for stats, index in codeLanguageStats
              circle(r=radius / 2, cx=radius, cy=radius, :class="'top top' + index", :style="'stroke-dasharray: ' + 3.1415926 * 50 * stats.percentage / 100 + 'px ' + 3.1415926 * 50 + 'px; stroke-dashoffset: ' + 3.1415926 * 50 * -stats.otherLanguagesCumulativePercentage / 100 + 'px;'")

        for stats, index in codeLanguageStats
          .el.code-language-stat
            .big #{stats.percentage}%
            .under
              img.code-language-icon(alt="" :src="'/images/common/code_languages/' + stats.language + '_small.png'")
              span= stats.name

  .dont-break.block.summary(v-if="org.progress && org.progress.programs > 1 && included")
    h1 Summary
    if org.kind != 'student'
      h4 Using CodeCombat's personalized learning engine...
      .fakebar
        div
          | #{formatNumber(org.progress.studentsWithCode)}
          = " "
          small= org.progress.studentsWithCode == 1 ? 'student' : 'students'
    if org.kind === 'student'
      h4 #{org.displayName || org.name} wrote...
    else
      h4 wrote...
    .fakebar
      div
        | #{formatNumber(org.progress.programs)}
        = " "
        small= org.progress.programs == 1 ? 'computer program' : 'computer programs'
    h4 across an estimated...
    .fakebar
      div
        | #{formatNumber(org.progress.linesOfCode)}
        = " "
        small= org.progress.linesOfCode == 1 ? 'line of code' : 'lines of code'
    if org.progress && (org.progress.playtime >= 1.5 * 3600 || org.kind == 'student')
      h4 in...
      .fakebar
        div
          if org.progress.playtime > 1.5 * 3600
            span= formatNumber(Math.round(org.progress.playtime / 3600))
          else
            span= (org.progress.playtime / 3600).toFixed(1)
          = " "
          small coding hours
    if org.progress.projects >= 1 + Math.min(100, Math.floor(0.02 * org.progress.studentsWithCode))
      h4 and expressed creativity by building
      .fakebar
        div
          | #{formatNumber(org.progress.projects)}
          = " "
          small= 'standalone game and web ' + (org.progress.projects == 1 ? 'project' : 'projects')
    if org.progress && org.progress.sampleSize < org.progress.populationSize
      em * Progress stats based on sampling #{formatNumber(org.progress.sampleSize)} of #{formatNumber(org.progress.populationSize)} students.

  .block(v-if="included && false")
    h1 Uncategorized Info
    ol
      if org.clan
        li
          a(:href="'/league/' + org.clan") View AI League Team
        li
          span (summary AI League stats about CodePoints, # participants, top players, etc.)
      if org.nces && org.nces.schools && !isNaN(org.nces.schools)
        li
          span Total schools in #{org.kind}: #{formatNumber(org.nces.schools)}
        li TODO: show info for all active schools we have for this #{org.kind}, not just the number of schools that exist
      else if org.schools && org.schools.length > 1
        li
          span # of schools: #{formatNumber(org.schools.length)}
      if org.nces && org.nces.teachers && !isNaN(org.nces.teachers)
        li
          span Total teachers in #{org.kind}: #{formatNumber(org.nces.teachers)}
        li TODO: show info for all registered teachers we have for this #{org.kind}, not just the number of teachers that exist
      else if org.teachers && org.teachers.length > 1
        li
          span # of teachers: #{formatNumber(org.teachers.length)}
      if org.classrooms && !isNaN(org.classrooms)
        li
          span Total classrooms in #{org.kind}: #{formatNumber(org.classrooms)}
      else if org.classrooms && org.classrooms.length > 1
        li
          span # of classrooms: #{formatNumber(org.classrooms.length)}
      if org.nces && org.nces.students && !isNaN(org.nces.students)
        li
          span Total students in #{org.kind}: #{formatNumber(org.nces.students)}
        li TODO: show info for all registered students we have for this #{org.kind}, not just the number of students that exist
      else if org.students && org.students.length > 1
        li
          span # of students: #{formatNumber(org.students.length)}
      if org.licenses && ((org.licenses.usage && org.licenses.total) || org.licenses.hasAccessToShared)
        li
          span= licenses

</template>

<style lang="scss">
#page-outcomes-report .outcomes-report-result-component {
  .address {
    margin-top: 0.25in;
    padding-left: 0.5in;
    padding-right: 0.5in;
    text-align: left;
    p {
      line-height: 18pt;
      font-size: 15pt;
      margin-bottom: 0.1in;
    }
  }
  label.edit-label {
    float: right;
  }
  .course {
    // TODO: tighten up styles so that most common case (1 course, multiple code languages) can fit on one page
    &.full-row {
      margin-bottom: 0.15in;
    }
    &.inline {
      width: 1.25in;
      height: 1.25in;
      display: inline-block;

      svg {
        filter: contrast(0.8);
      }
    }
    .bar {
      margin-bottom: 0.0in;
      break-inside: avoid;
      //border: 1px solid orange
      .concepts-list {
        max-width: 30%;
      }
    }
    .bar::after {
      clear: both;
      display: table;
      content: " ";
    }
    .el {
      margin-right: 0.25in;
      position: relative;
      svg {
        transform: rotate(-90deg);
      }
      circle.top {
        fill: transparent;
        stroke-width: 50;
        stroke: rgb(14, 75, 96);

        // TODO: better colors
        &.top1 {
          stroke: rgb(96, 14, 75);
        }

        &.top2 {
          stroke: rgb(75, 96, 14);
        }
      }
      circle.bottom {
        fill: rgb(242, 190, 24);
      }

      &.code-language-stat {
        .big {
          width: 1.7in;
        }

        .under {
          width: 1.7in;
        }
      }

      .big {
        display: block;
        font-size: 41pt;
        line-height: 41pt;
        font-family: 'Open Sans', sans-serif;
        font-weight: 600;
        text-align: center;
        width: 1.5in;
      }
      .under {
        display: block;
        text-align: center;
        width: 1.5in;

        img.code-language-icon {
          width: 0.49in;
          margin: 0 0.05in 0 -0.09in;
        }
      }
      @media print {
        .overlay-text {
          color: white !important;
          text-shadow: -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 1px 0 black !important;
        }
      }
      .overlay-text {
        color: white;
        position: absolute;
        width: 100%;
        left: 0;
        right: 0;
        text-align: center;
        font-family: 'Open Sans', sans-serif;
        text-shadow: -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 1px 0 black;
        $circleSize: 75pt;

        &.top-text, &.bot-text {
          font-size: 9pt;
          line-height: 9pt;
        }
        &.mid-text {
          font-size: 16pt;
          line-height: 16pt;
          /*top: calc($circleSize / 2 - 16pt / 2);*/
          top: 29.5pt;
        }
        &.top-text {
          /*top: calc($circleSize / 2 - 16pt / 2 - 9pt - 2pt);*/
          top: 18pt;
        }
        &.bot-text {
          /*bottom: calc($circleSize / 2 - 16pt / 2 - 9pt - 2pt);*/
          bottom: 18pt;
        }
      }

      li {
        line-height: 14pt;
        font-size: 12pt;
      }
      height: 75pt;
      float: left;
    }
    h3 {
      font-family: 'Open Sans', sans-serif;
      font-weight: 600;
      font-size: 18pt;
    }
  }
  .block {
    margin-top: 0;
    margin-left: 0.5in;
    margin-right: 0.5in;

    h1, h2 {
      font-family: 'Open Sans', sans-serif;
      font-size: 22pt;
      font-weight: 100;
      margin-top: 0.25in;
      border-bottom: 1px solid #ccc;
      border-radius: 0px;
      margin-bottom: 0.1in;
    }
    h3, h4 {
      font-family: 'Open Sans', sans-serif;
      font-weight: 600;
    }
    h1 {
      font-size: 18pt;
      line-height: 18pt;
    }
    h2 {
      font-size: 14pt;
      line-height: 15pt;
    }
    h4 {
      font-size: 18pt;
      line-height: 18pt;
      font-weight: 600;
    }
    .fakebar {
      margin-top: 0.05in;
      margin-bottom: 0.15in;

      background-color: rgb(31, 87, 43);
      -webkit-print-color-adjust: exact !important;
      background: linear-gradient(rgb(31, 87, 43), rgb(31, 87, 43)) !important;

      height: 0.4in;
      line-height: 0.4in;
      > div {
        background-color: white;
        -webkit-print-color-adjust: exact !important;
        background: linear-gradient(white, white) !important;
        height: 100%;
        padding-left: 0.1in;
        float: right;
        font-size: 32pt;
        font-weight: 600;
        small {
          font-size: 18pt;
          font-weight: 600;
        }
      }
      > div::after {
        clear: both;
        content: ' ';
        display: table;
      }
    }
    p {
      font-size: 14pt;
      line-height: 18pt;
    }
    .left-col {
      break-inside: avoid;
      float: left;
      width: 4in;
      margin-right: 1in;
    }
    .right-col {
      break-inside: avoid;
      float: left;
      width: 2.5in;
      font-size: 14pt;
      line-height: 18pt;
      ul {
        padding-left: 2ex;
      }
    }
  }
  .dont-break {
    break-inside: avoid;
  }

  .dont-break::after {
    clear: both;
    content: ' ';
    display: table;
  }

  .dont-break.block.summary {
    padding-top: 0.25in;

    h1 {
      margin-top: 0.1in;
    }
  }

  .page-break {
    display: flex;
    align-items: center;
    page-break-before: always;

    hr {
      width: 100%;
    }

    @media screen {
      height: 1in;
    }

    @media print {
      height: 0.5in;

      hr {
        display: none
      }
    }
  }

  @media print {
    * {
      /* print-color-adjust: exact is needed for white text when overlaid on pie charts */
      -webkit-print-color-adjust: exact;
      print-color-adjust: exact;
      transition: none !important;
    }
  }
}
</style>

<script>
import { mapGetters, mapActions, mapState } from 'vuex'
import utils from 'core/utils'

export default Vue.extend({
  name: 'outcomes-report-result-component',
  props: {
    org: {
      type: Object,
      required: true
    },
    isSubOrg: {
      type: Boolean,
      default: false
    },
    editing: {
      type: Boolean,
      default: false
    },
    parentOrgKind: {
      type: String,
      default: null
    },
  },

  data () {
    return {
      // TODO: some bug where printing gets large horizontal margins after we customize included schools and finish editing
      included: !this.isSubOrg || this.org.initiallyIncluded
    }
  },

  computed: {
    ...mapState('courses', {
      coursesLoaded: 'loaded',
      sortedCourses: (state) => utils.sortCourses(_.values(state.byId))
    }),

    kindString () {
      return utils.orgKindString(this.org.kind, this.org)
    },

    codeLanguageString () {
      return utils.capitalLanguages[this.org.codeLanguage] || ''
    },

    codeLanguageStats () {
      if (!this.org.progress) return
      let languageStats = {}
      let totalPrograms = 0
      for (let language of ['python', 'javascript', 'cpp']) {
        let programs = this.org.progress.programsByLanguage[language]
        totalPrograms += programs
        languageStats[language] = { programs }
      }
      for (let [language, stats] of Object.entries(languageStats)) {
        stats.percentage = Math.round(100 * stats.programs / totalPrograms)
        stats.name = utils.capitalLanguages[language]
        stats.language = language
      }
      languageStats = _.omit(languageStats, (s) => !s.programs || s.programs < totalPrograms * 0.005)  // Skip low usage languages
      languageStats = Object.values(languageStats).sort((a, b) => b.programs - a.programs)  // Sorted array
      let otherLanguagesCumulativePercentage = 0
      for (let stats of languageStats) {
        // Keep track of how much all languages have contributed so that we can draw our pie chart
        stats.otherLanguagesCumulativePercentage = otherLanguagesCumulativePercentage
        otherLanguagesCumulativePercentage += stats.percentage
      }
      return languageStats
    },

    coursesWithProgress () {
      if (!this.org.progress) return []
      let courses = _.cloneDeep(this.sortedCourses)
      let alreadyCoveredConcepts = []
      for (let course of courses) {
        course.name = utils.i18n(course, 'name')
        course.acronym = utils.courseAcronyms[course._id]
        course.studentsStarting = (this.org.progress.studentsStartingCourse || {})[course._id] || 0
        course.studentsCompleting = (this.org.progress.studentsCompletingCourse || {})[course._id] || 0
        course.completion = course.studentsStarting ? Math.min(1, course.studentsCompleting / course.studentsStarting) : 0
        course.newConcepts = _.difference(course.concepts, alreadyCoveredConcepts)
        alreadyCoveredConcepts = _.union(course.concepts, alreadyCoveredConcepts)
      }
      courses = _.filter(courses, (course) => (course.studentsStarting + course.studentsCompleting) >= Math.min(100, Math.max(2, Math.ceil(0.02 * this.org.progress.studentsWithCode))))
      return courses
    },

    mapUrl() {
      if (!this.org.address) return null
      let orgName = this.org.displayName || this.org.name
      if (this.org.kind === 'school-district' && !/school.*district|isd|usd|unified/.test(orgName))
        orgName += ' School District'
      const addresses = [orgName + ', ' + this.org.address]
      for (let subOrg of this.org.subOrgs || []) {
        if (subOrg.address) {
          let subOrgName = subOrg.displayName || subOrg.name
          if (subOrg.kind === 'school-district' && !/school.*district|isd|usd|unified/.test(subOrgName))
            subOrgName += ' School District'
          addresses.push(subOrgName + ', ' + subOrg.address)
        }
      }
      // One address, do a search-type link
      let url = `https://www.google.com/maps/search/?api=1&query=${addresses.map(a => encodeURIComponent(a))}`
      if (addresses.length > 1) {
        // Multiple addresses, abuse direction-type link to show them all
        // Could add &destination=${encodeURIComponent(addresses[0])} to activate the driving directions, but probably better without
        let urlWithSubOrgs = `https://www.google.com/maps/dir/?api=1&origin=${encodeURIComponent(addresses[0])}&waypoints=${addresses.slice(1).map(a => encodeURIComponent(a)).join('|')}`
        if (urlWithSubOrgs.length < 8192)
          url = urlWithSubOrgs
      }
      return url
    },

    isAdmin() {
      return me.isAdmin()
    },

    isSchoolAdmin() {
      return me.isSchoolAdmin()
    },
  },

  created () {
    this.fetchCourses()
  },

  // TODO: figure out how to sync included status back to the parent
  // https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier

  mounted () {
  },

  methods: {
    ...mapActions({
      fetchCourses: 'courses/fetch',
    }),

    phoneString (phone) {
      if (!/[0-9]{10}/.test(phone)) return phone
      return `(${phone.slice(0, 3)}) ${phone.slice(3, 6)}-${phone.slice(6, 10)}`
    },

    formatNumber (num) {
      if (num <         10000) return num.toLocaleString()
      if (num <        999500) return Math.round(num / 1000) + 'K'
      if (num <      10000000) return (num / 1000000).toFixed(1) + 'M'
      if (num <     999500000) return Math.round(num / 1000000) + 'M'
      if (num <   10000000000) return (num / 1000000000).toFixed(1) + 'B'
      if (num <  999500000000) return Math.round(num / 1000000000) + 'B'
      return Math.round(num / 1000000000000) + 'T'
    }
  }
});
</script>

<template lang="pug">
.outcomes-report-result-component(v-if="included || editing")
  .page-break(v-if="isSubOrg && included")
    hr

  .address
    p
      b
        if org.archived
          em Archived:
        else
          span #{kindString}:
      span= " "
      if isSubOrg
        a(:href="'/outcomes-report/' + org.kind + '/' + org._id" target="_blank")
          b= org.displayName || org.name
      else
        b= org.displayName || org.name
        if included && isAdmin && editing && org.kind == 'school-district' && org['administrative-region']
          span ,
          a(:href="'/outcomes-report/administrative-region/' + org['administrative-region'].region.toLowerCase()" target="_blank")= org['administrative-region'].region
      if org.email
        span  (#{org.email})
      if codeLanguageString
        span  (#{codeLanguageString})
      if !included && org.progress && org.progress.studentsWithCode && org.kind != 'student'
        span  - #{org.progress.studentsWithCode} students
      label.edit-label.editing-only(:for="'includeOrg-' + org._id" v-if="editing && isSubOrg")
        span
        input(:id="'includeOrg-' + org._id" name="'includeOrg-' + org._id" type="checkbox" v-model="included")
        span  include
      if included && isAdmin && editing
        span(v-if="org.address")
          br
          span= org.address
          span
            span= ' '
            a(:href="mapUrl" target="_blank" rel="nofollow") (map)
        //if org.geo && org.geo.county
        //  br
        //  span County: #{org.geo.county}
        if org.phone || org.website
          br
        if org.phone
          span= 'Phone: '
          a(:href="'tel:' + org.phone")= phoneString(org.phone)
          span
        if org.website
          span= ' Website: '
          a(:href="org.website" rel="nofollow" target="_blank")= org.website
      if included && org.students && org.kind == 'course' && parentOrgKind != 'student'
        for student in org.students
          br
          span= 'Student: '
          a(:href="'/outcomes-report/student/' + student._id" target="_blank")
            b= student.displayName
      if included && org.classrooms && org.kind == 'student' && parentOrgKind != 'classroom'
        for classroom in org.classrooms
          br
          span= 'Classroom: '
          a(:href="'/outcomes-report/classroom/' + classroom._id" target="_blank")
            b= classroom.name
          span  #{classroom.codeLanguage} - #{formatNumber(classroom.studentCount)} students
      if included && org.teachers && ['classroom', 'student'].indexOf(org.kind) != -1 && ['teacher', 'classroom'].indexOf(parentOrgKind) == -1
        for teacher in org.teachers
          br
          span= 'Teacher: '
          a(:href="'/outcomes-report/teacher/' + teacher._id" target="_blank")
            b= teacher.displayName
          if teacher.email
            span  (#{teacher.email})
      if included && org.schools && org.kind == 'teacher' && parentOrgKind != 'school'
        for school in org.schools
          br
          span= 'School: '
          a(:href="'/outcomes-report/school/' + school._id" target="_blank")
            b= school.name
      if included && org['school-district'] && ['school', 'teacher', 'school-admin'].indexOf(org.kind) != -1 && ['school-district', 'school'].indexOf(parentOrgKind) == -1 && (isSchoolAdmin || (isAdmin && (editing || org.kind != 'teacher')))
        br
        span= 'District: '
        a(:href="'/outcomes-report/school-district/' + org['school-district']._id" target="_blank")
          b= org['school-district'].name
      if included && org['school-admin'] && ['school', 'teacher'].indexOf(org.kind) != -1 && parentOrgKind != 'school-admin' && org['school-admin'].displayName != 'Anonymous'
        br
        span= 'Admin: '
        a(:href="'/outcomes-report/school-admin/' + org['school-admin']._id" target="_blank")
          b= org['school-admin'].displayName

  .block(v-if="included && coursesLoaded && coursesWithProgress[0] && coursesWithProgress[0].studentsStarting > 1" :class="isSubOrg && coursesWithProgress.length > 1 ? 'dont-break' : ''")
    h1 Course Progress
    for course in coursesWithProgress
      if !isSubOrg || coursesWithProgress.length == 1
        .course.dont-break.full-row
          h3= course.name
          .bar
            .el
              svg(width=100, height=100)
                - var radius = 50
                circle.bottom(r=radius,cx=radius, cy=radius)
                circle.top(r=radius / 2, cx=radius, cy=radius, :style="'stroke-dasharray: ' + 3.1415926 * 50 * course.completion + 'px ' + 3.1415926 * 50 + 'px'")
            .el
              .big #{Math.round(100 * course.completion)}%
              .under complete
            .el(v-if="org.kind != 'student'")
              .big #{formatNumber(course.studentsStarting)}
              .under= course.studentsStarting === 1 ? "student" : "students"
            .el.concepts-list
              b Key Concepts:
              ul
                li(v-for="concept in course.newConcepts")
                  span {{$t('concepts.' + concept)}}
      else
        .course.inline
          .el
            svg(width=100, height=100)
              - var radius = 50
              circle.bottom(r=radius,cx=radius,cy=radius)
              circle.top(r=radius / 2, cx=radius, cy=radius, :style="'stroke-dasharray: ' + 3.1415926 * 50 * course.completion + 'px ' + 3.1415926 * 50 + 'px'")
            if org.kind != 'student'
              .overlay-text.top-text #{formatNumber(course.studentsStarting)} #{course.studentsStarting === 1 ? "student" : "students"}
            .overlay-text.mid-text= course.acronym
            .overlay-text.bot-text #{Math.round(100 * course.completion)}% complete

  .block(v-if="org.progress && org.progress.programs > 1 && included && codeLanguageStats.length > 1 && !codeLanguageString")
    // TODO: somehow note the code language if we are skipping this section (like 100% Python at school level)
    .course.dont-break.full-row
      h3 Code Languages
      .bar
        .el
          svg(width=100, height=100)
            - var radius = 50
            circle.bottom(r=radius,cx=radius, cy=radius)
            for stats, index in codeLanguageStats
              circle(r=radius / 2, cx=radius, cy=radius, :class="'top top' + index", :style="'stroke-dasharray: ' + 3.1415926 * 50 * stats.percentage / 100 + 'px ' + 3.1415926 * 50 + 'px; stroke-dashoffset: ' + 3.1415926 * 50 * -stats.otherLanguagesCumulativePercentage / 100 + 'px;'")

        for stats, index in codeLanguageStats
          .el.code-language-stat
            .big #{stats.percentage}%
            .under
              img.code-language-icon(alt="" :src="'/images/common/code_languages/' + stats.language + '_small.png'")
              span= stats.name

  .dont-break.block.summary(v-if="org.progress && org.progress.programs > 1 && included")
    h1 Summary
    if org.kind != 'student'
      h4 Using CodeCombat's personalized learning engine...
      .fakebar
        div
          | #{formatNumber(org.progress.studentsWithCode)}
          = " "
          small= org.progress.studentsWithCode == 1 ? 'student' : 'students'
    if org.kind === 'student'
      h4 #{org.displayName || org.name} wrote...
    else
      h4 wrote...
    .fakebar
      div
        | #{formatNumber(org.progress.programs)}
        = " "
        small= org.progress.programs == 1 ? 'computer program' : 'computer programs'
    h4 across an estimated...
    .fakebar
      div
        | #{formatNumber(org.progress.linesOfCode)}
        = " "
        small= org.progress.linesOfCode == 1 ? 'line of code' : 'lines of code'
    if org.progress && (org.progress.playtime >= 1.5 * 3600 || org.kind == 'student')
      h4 in...
      .fakebar
        div
          if org.progress.playtime > 1.5 * 3600
            span= formatNumber(Math.round(org.progress.playtime / 3600))
          else
            span= (org.progress.playtime / 3600).toFixed(1)
          = " "
          small coding hours
    if org.progress.projects >= 1 + Math.min(100, Math.floor(0.02 * org.progress.studentsWithCode))
      h4 and expressed creativity by building
      .fakebar
        div
          | #{formatNumber(org.progress.projects)}
          = " "
          small= 'standalone game and web ' + (org.progress.projects == 1 ? 'project' : 'projects')
    if org.progress && org.progress.sampleSize < org.progress.populationSize
      em * Progress stats based on sampling #{formatNumber(org.progress.sampleSize)} of #{formatNumber(org.progress.populationSize)} students.

  .block(v-if="included && false")
    h1 Uncategorized Info
    ol
      if org.clan
        li
          a(:href="'/league/' + org.clan") View AI League Team
        li
          span (summary AI League stats about CodePoints, # participants, top players, etc.)
      if org.nces && org.nces.schools && !isNaN(org.nces.schools)
        li
          span Total schools in #{org.kind}: #{formatNumber(org.nces.schools)}
        li TODO: show info for all active schools we have for this #{org.kind}, not just the number of schools that exist
      else if org.schools && org.schools.length > 1
        li
          span # of schools: #{formatNumber(org.schools.length)}
      if org.nces && org.nces.teachers && !isNaN(org.nces.teachers)
        li
          span Total teachers in #{org.kind}: #{formatNumber(org.nces.teachers)}
        li TODO: show info for all registered teachers we have for this #{org.kind}, not just the number of teachers that exist
      else if org.teachers && org.teachers.length > 1
        li
          span # of teachers: #{formatNumber(org.teachers.length)}
      if org.classrooms && !isNaN(org.classrooms)
        li
          span Total classrooms in #{org.kind}: #{formatNumber(org.classrooms)}
      else if org.classrooms && org.classrooms.length > 1
        li
          span # of classrooms: #{formatNumber(org.classrooms.length)}
      if org.nces && org.nces.students && !isNaN(org.nces.students)
        li
          span Total students in #{org.kind}: #{formatNumber(org.nces.students)}
        li TODO: show info for all registered students we have for this #{org.kind}, not just the number of students that exist
      else if org.students && org.students.length > 1
        li
          span # of students: #{formatNumber(org.students.length)}
      if org.licenses && ((org.licenses.usage && org.licenses.total) || org.licenses.hasAccessToShared)
        li
          span= licenses

</template>

<style lang="scss">
#page-outcomes-report .outcomes-report-result-component {
  .address {
    margin-top: 0.25in;
    padding-left: 0.5in;
    padding-right: 0.5in;
    text-align: left;
    p {
      line-height: 18pt;
      font-size: 15pt;
      margin-bottom: 0.1in;
    }
  }
  label.edit-label {
    float: right;
  }
  .course {
    // TODO: tighten up styles so that most common case (1 course, multiple code languages) can fit on one page
    &.full-row {
      margin-bottom: 0.15in;
    }
    &.inline {
      width: 1.25in;
      height: 1.25in;
      display: inline-block;

      svg {
        filter: contrast(0.8);
      }
    }
    .bar {
      margin-bottom: 0.0in;
      break-inside: avoid;
      //border: 1px solid orange
      .concepts-list {
        max-width: 30%;
      }
    }
    .bar::after {
      clear: both;
      display: table;
      content: " ";
    }
    .el {
      margin-right: 0.25in;
      position: relative;
      svg {
        transform: rotate(-90deg);
      }
      circle.top {
        fill: transparent;
        stroke-width: 50;
        stroke: rgb(14, 75, 96);

        // TODO: better colors
        &.top1 {
          stroke: rgb(96, 14, 75);
        }

        &.top2 {
          stroke: rgb(75, 96, 14);
        }
      }
      circle.bottom {
        fill: rgb(242, 190, 24);
      }

      &.code-language-stat {
        .big {
          width: 1.7in;
        }

        .under {
          width: 1.7in;
        }
      }

      .big {
        display: block;
        font-size: 41pt;
        line-height: 41pt;
        font-family: 'Open Sans', sans-serif;
        font-weight: 600;
        text-align: center;
        width: 1.5in;
      }
      .under {
        display: block;
        text-align: center;
        width: 1.5in;

        img.code-language-icon {
          width: 0.49in;
          margin: 0 0.05in 0 -0.09in;
        }
      }
      @media print {
        .overlay-text {
          color: white !important;
          text-shadow: -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 1px 0 black !important;
        }
      }
      .overlay-text {
        color: white;
        position: absolute;
        width: 100%;
        left: 0;
        right: 0;
        text-align: center;
        font-family: 'Open Sans', sans-serif;
        text-shadow: -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 1px 0 black;
        $circleSize: 75pt;

        &.top-text, &.bot-text {
          font-size: 9pt;
          line-height: 9pt;
        }
        &.mid-text {
          font-size: 16pt;
          line-height: 16pt;
          /*top: calc($circleSize / 2 - 16pt / 2);*/
          top: 29.5pt;
        }
        &.top-text {
          /*top: calc($circleSize / 2 - 16pt / 2 - 9pt - 2pt);*/
          top: 18pt;
        }
        &.bot-text {
          /*bottom: calc($circleSize / 2 - 16pt / 2 - 9pt - 2pt);*/
          bottom: 18pt;
        }
      }

      li {
        line-height: 14pt;
        font-size: 12pt;
      }
      height: 75pt;
      float: left;
    }
    h3 {
      font-family: 'Open Sans', sans-serif;
      font-weight: 600;
      font-size: 18pt;
    }
  }
  .block {
    margin-top: 0;
    margin-left: 0.5in;
    margin-right: 0.5in;

    h1, h2 {
      font-family: 'Open Sans', sans-serif;
      font-size: 22pt;
      font-weight: 100;
      margin-top: 0.25in;
      border-bottom: 1px solid #ccc;
      border-radius: 0px;
      margin-bottom: 0.1in;
    }
    h3, h4 {
      font-family: 'Open Sans', sans-serif;
      font-weight: 600;
    }
    h1 {
      font-size: 18pt;
      line-height: 18pt;
    }
    h2 {
      font-size: 14pt;
      line-height: 15pt;
    }
    h4 {
      font-size: 18pt;
      line-height: 18pt;
      font-weight: 600;
    }
    .fakebar {
      margin-top: 0.05in;
      margin-bottom: 0.15in;

      background-color: rgb(31, 87, 43);
      -webkit-print-color-adjust: exact !important;
      background: linear-gradient(rgb(31, 87, 43), rgb(31, 87, 43)) !important;

      height: 0.4in;
      line-height: 0.4in;
      > div {
        background-color: white;
        -webkit-print-color-adjust: exact !important;
        background: linear-gradient(white, white) !important;
        height: 100%;
        padding-left: 0.1in;
        float: right;
        font-size: 32pt;
        font-weight: 600;
        small {
          font-size: 18pt;
          font-weight: 600;
        }
      }
      > div::after {
        clear: both;
        content: ' ';
        display: table;
      }
    }
    p {
      font-size: 14pt;
      line-height: 18pt;
    }
    .left-col {
      break-inside: avoid;
      float: left;
      width: 4in;
      margin-right: 1in;
    }
    .right-col {
      break-inside: avoid;
      float: left;
      width: 2.5in;
      font-size: 14pt;
      line-height: 18pt;
      ul {
        padding-left: 2ex;
      }
    }
  }
  .dont-break {
    break-inside: avoid;
  }

  .dont-break::after {
    clear: both;
    content: ' ';
    display: table;
  }

  .dont-break.block.summary {
    padding-top: 0.25in;

    h1 {
      margin-top: 0.1in;
    }
  }

  .page-break {
    display: flex;
    align-items: center;
    page-break-before: always;

    hr {
      width: 100%;
    }

    @media screen {
      height: 1in;
    }

    @media print {
      height: 0.5in;

      hr {
        display: none
      }
    }
  }

  @media print {
    * {
      /* print-color-adjust: exact is needed for white text when overlaid on pie charts */
      -webkit-print-color-adjust: exact;
      print-color-adjust: exact;
      transition: none !important;
    }
  }
}
</style>


chore: 👷 fix date validation

chore: 👷 fix date validation

<template>

      <v-skeleton-loader width="150px" type="list-item" />
      <v-skeleton-loader
        v-for="idx in 3"
        :key="idx"
        class="mb-4"
        height="100px"
        type="image"
      />

        You are currently not staking any eth.

 0" class="pb-8">
        Pending

                  {{ pending.amount }} ETH

                  {{ '$' + pending.amountFiat }}

              <v-progress-circular
                size="18"
                width="2"
                color="primary"
                indeterminate
              />
              <v-icon class="ml-5" color="searchText" size="22">{{
                isExpanded(idx) ? 'mdi-chevron-up' : 'mdi-chevron-down'
              }}</v-icon>

More Info

              <v-progress-circular
                class="ml-4"
                size="18"
                width="2"
                color="primary"
                indeterminate
              />
              {{
                !pending.justStaked || txReceipt ? 'Processing' : 'Sending'
              }}

              <v-icon size="20" color="primary">mdi-check-circle</v-icon>
              Deposited

              <v-icon size="20" color="error">mdi-close-circle</v-icon>
              Failed

              View on Etherscan
                <v-icon color="primary" size="14">mdi-open-in-new</v-icon>
              View on EthVM
                <v-icon color="primary" size="14">mdi-open-in-new</v-icon>

                <v-progress-circular
                  size="18"
                  width="2"
                  color="primary"
                  indeterminate
                />
                Waiting for validator activation

              Estimated wait time: {{ pending.estimatedWaitTime }}

              Eth2 Beacon Chain transactions
                <v-icon color="primary" size="14">mdi-open-in-new</v-icon>

 0">
        Active

                {{ active.totalBalanceETH + ' ETH' }}

                {{ '$' + active.totalBalanceFiat }}

                Earned
                {{ active.earned + ' ETH' }}
                · Average APR {{ active.averageApr }}

          View Eth2 address
            <v-icon color="primary" size="14">mdi-open-in-new</v-icon>

</template>


Select: fix Switching select`s multiple-choice radio will cause an error (#15206) (#15587)

<template>

        <el-tag
          :closable="!selectDisabled"
          :size="collapseTagSize"
          :hit="selected[0].hitState"
          type="info"
          @close="deleteTag($event, selected[0])"
          disable-transitions>
          {{ selected[0].currentLabel }}
        </el-tag>
        <el-tag
          v-if="selected.length > 1"
          :closable="false"
          :size="collapseTagSize"
          type="info"
          disable-transitions>
          + {{ selected.length - 1 }}
        </el-tag>

      <transition-group @after-leave="resetInputHeight" v-if="!collapseTags">
        <el-tag
          v-for="item in selected"
          :key="getValueKey(item)"
          :closable="!selectDisabled"
          :size="collapseTagSize"
          :hit="item.hitState"
          type="info"
          @close="deleteTag($event, item)"
          disable-transitions>
          {{ item.currentLabel }}
        </el-tag>
      </transition-group>

      <input
        type="text"
        class="el-select__input"
        :class="[selectSize ? `is-${ selectSize }` : '']"
        :disabled="selectDisabled"
        :autocomplete="autoComplete || autocomplete"
        @focus="handleFocus"
        @blur="softFocus = false"
        @click.stop
        @keyup="managePlaceholder"
        @keydown="resetInputState"
        @keydown.down.prevent="navigateOptions('next')"
        @keydown.up.prevent="navigateOptions('prev')"
        @keydown.enter.prevent="selectOption"
        @keydown.esc.stop.prevent="visible = false"
        @keydown.delete="deletePrevTag"
        @compositionstart="handleComposition"
        @compositionupdate="handleComposition"
        @compositionend="handleComposition"
        v-model="query"
        @input="debouncedQueryChange"
        v-if="filterable"
        :style="{ 'flex-grow': '1', width: inputLength / (inputWidth - 32) + '%', 'max-width': inputWidth - 42 + 'px' }"
        ref="input">

    <el-input
      ref="reference"
      v-model="selectedLabel"
      type="text"
      :placeholder="currentPlaceholder"
      :name="name"
      :id="id"
      :autocomplete="autoComplete || autocomplete"
      :size="selectSize"
      :disabled="selectDisabled"
      :readonly="readonly"
      :validate-event="false"
      :class="{ 'is-focus': visible }"
      @focus="handleFocus"
      @blur="handleBlur"
      @keyup.native="debouncedOnInputChange"
      @keydown.native.down.stop.prevent="navigateOptions('next')"
      @keydown.native.up.stop.prevent="navigateOptions('prev')"
      @keydown.native.enter.prevent="selectOption"
      @keydown.native.esc.stop.prevent="visible = false"
      @keydown.native.tab="visible = false"
      @paste.native="debouncedOnInputChange"
      @mouseenter.native="inputHovering = true"
      @mouseleave.native="inputHovering = false">
      <template slot="prefix" v-if="$slots.prefix">
        <slot name="prefix"></slot>
      </template>
      <template slot="suffix">

      </template>
    </el-input>
    <transition
      name="el-zoom-in-top"
      @before-enter="handleMenuEnter"
      @after-leave="doDestroy">
      <el-select-menu
        ref="popper"
        :append-to-body="popperAppendToBody"
        v-show="visible && emptyText !== false">
        <el-scrollbar
          tag="ul"
          wrap-class="el-select-dropdown__wrap"
          view-class="el-select-dropdown__list"
          ref="scrollbar"
          :class="{ 'is-empty': !allowCreate && query && filteredOptionsCount === 0 }"
          v-show="options.length > 0 && !loading">
          <el-option
            :value="query"
            created
            v-if="showNewOption">
          </el-option>
          <slot></slot>
        </el-scrollbar>
        <template v-if="emptyText && (!allowCreate || loading || (allowCreate && options.length === 0 ))">
          <slot name="empty" v-if="$slots.empty"></slot>

            {{ emptyText }}

        </template>
      </el-select-menu>
    </transition>

</template>


Allow filtering by untested and incompatible separately

<template>
  <v-container>
    <v-layout wrap>
      <v-flex xs12>
        <h1 class="display-2 mb-3">
         Reports
        </h1>
      </v-flex>

      <v-flex xs12>
        <v-select
            v-model="selectedTags"
            :items="tags"
            label="Show reports by tag(s)"
            clearable
            small-chips
            multiple
            solo
            hide-details
          >
        </v-select>
      </v-flex>
      <v-flex sm6>
        <v-switch
          v-model="showUntested"
          label="Show untested reports"
          color="red"
          >
        </v-switch>
      </v-flex>
      <v-flex sm6>
        <v-switch
          v-model="showIncompatible"
          label="Show incompatible reports"
          color="red"
          >
        </v-switch>
      </v-flex>

      <v-flex xs12>
        <report-list :reports="filteredReports" />
      </v-flex>

    </v-layout>
  </v-container>
</template>

<script>
import { reportService } from '../api/report-service'
import ReportList from '../components/report-list'

export default {
  components: {
    ReportList
  },
  data: () => ({
    showIncompatible: false,
    showUntested: false,
    version: "V12",
    selectedTags: [],
    reports: []
  }),
  computed: {
    tags: function () {
      const allTags = this.reports.reduce(getTagsFromReports,[])
      const uniqueTags = getUniqueTags(allTags)
      return uniqueTags
    },
    filteredReports: function() {
      return this.reports.filter(report => {
        const isCompatible = report.compatible.includes(this.version)
        const isIncompatible = report.notCompatible.includes(this.version)
        const isUntested = !isCompatible && !isIncompatible

        const hasSelectedTags = this.selectedTags.length > 0
        const meetsTagRequirements = hasSelectedTags ? this.hasSelectedTag(report) : true

        if(meetsTagRequirements) {
          return isCompatible
            || (this.showUntested && isUntested)
            || (this.showIncompatible && isIncompatible)
        }

        return false
      })
    }
  },
  methods: {
    getReports: function() {
      reportService.getReports().then(reports => {
        this.reports = reports
      })
    },
    hasSelectedTag(report) {
      return report.tags.reduce(
        (acc,cur) => {
          return acc || this.selectedTags.includes(cur)
        }, false)
    }
  },
  watch: {
    '$route': {
      handler: 'getReports',
      immediate: true
    }
  }
}

function getTagsFromReports(allTags, report) {
  allTags.push(...report.tags)
  return allTags
}

function getUniqueTags(allTags) {
  return [...new Set(allTags)]
}
</script>

<template>
  <v-container>
    <v-layout wrap>
      <v-flex xs12>
        <h1 class="display-2 mb-3">
         Reports
        </h1>
      </v-flex>

      <v-flex xs12>
        <v-select
            v-model="selectedTags"
            :items="tags"
            label="Show reports by tag(s)"
            clearable
            small-chips
            multiple
            solo
            hide-details
          ></v-select>
        <v-switch
          v-model="showIncompatible"
          label="Show incompatible reports"
          color="red"
          ></v-switch>
      </v-flex>

      <v-flex xs12>
        <report-list :reports="filteredReports" />
      </v-flex>

    </v-layout>
  </v-container>
</template>

<script>
import { reportService } from '../api/report-service'
import ReportList from '../components/report-list'

export default {
  components: {
    ReportList
  },
  data: () => ({
    showIncompatible: false,
    version: "V12",
    selectedTags: [],
    reports: []
  }),
  computed: {
    tags: function () {
      const allTags = this.reports.reduce(getTagsFromReports,[])
      const uniqueTags = getUniqueTags(allTags)
      return uniqueTags
    },
    filteredReports: function() {
      return this.reports.filter(report => {
        const showForCompatibility = this.showIncompatible ? true : report.compatible.includes(this.version)
        const showForTags = this.selectedTags.length === 0 ? true : report.tags.reduce((acc,cur) => {
          return acc || this.selectedTags.includes(cur)
        }, false)
        return showForCompatibility && showForTags;
      })
    }
  },
  methods: {
    getReports: function() {
      reportService.getReports().then(reports => {
        this.reports = reports
      })
    }
  },
  watch: {
    '$route': {
      handler: 'getReports',
      immediate: true
    }
  }
}

function getTagsFromReports(allTags, report) {
  allTags.push(...report.tags)
  return allTags
}

function getUniqueTags(allTags) {
  return [...new Set(allTags)]
}
</script>


Fix overlay

Fix overlay

<template>

        <button
          v-if="mobile | tablet"
          :aria-label="closeNav"
          class="close" @click="toggleNav"
          :style="{ fontSize: headerHeight/2 + 'px' }">
          <ui-icon :icon="mobile ? 'arrow_back' : 'menu'"/>
        </button>

Kolibri

      <ui-menu
        class="nav-main"
        :options="menuOptions"
        hasIcons
        @select="navigate"
        role="navigation"
        :aria-label="ariaLabel"
        :style="{ maxWidth: width + 'px' }">
      </ui-menu>

{{ footerMsg }}

            <ui-icon icon="copyright"/>
            2017 Learning Equality

    <login-modal v-if="loginModalVisible"/>

</template>


Klasse bei Login ergänzt

Klasse bei Login ergänzt

<template>

Swop ist das Tauschtool für Deine Uni-Veranstaltungen.

Login

Logge Dich mit Deiner Uni-Mail ein.

        <form class="signUp">

              <input v-model.trim="username" class="input" :class="{'is-danger': LoginError}" v-on:keyup.enter="nextInput" type="mail" placeholder="Deine Uni-Mail" name="username" required>

              <input v-model="password" id="password" class="input" :class="{'is-danger': LoginError}" v-on:keyup.enter="logIn" type="password" placeholder="Dein swop-Passwort" name="password" required>

              <router-link :to="{name: 'passwort-vergessen'}"class="button is-outlined has-text-primary forget-password">
                Vergessen?
              </router-link>

{{LoginError}}

        </form>

<router-link :to="{name: 'signup'}" class="is-white font-klein margin-right">Noch kein swop-Mitglied? </router-link>Einloggen

</template>

<template>

Swop ist das Tauschtool für Deine Uni-Veranstaltungen.

Login

Logge Dich mit Deiner Uni-Mail ein.

        <form class="signUp">

              <input v-model.trim="username" class="input" :class="{'is-danger': LoginError}" v-on:keyup.enter="nextInput" type="mail" placeholder="Deine Uni-Mail" name="username" required>

              <input v-model="password" id="password" class="input" :class="{'is-danger': LoginError}" v-on:keyup.enter="logIn" type="password" placeholder="Dein swop-Passwort" name="password" required>

              <router-link :to="{name: 'passwort-vergessen'}"class="button is-outlined has-text-primary">
                Vergessen?
              </router-link>

{{LoginError}}

        </form>

<router-link :to="{name: 'signup'}" class="is-white font-klein margin-right">Noch kein swop-Mitglied? </router-link>Einloggen

</template>


spacing

spacing

<script>
  export default {
    render: function (c) {
      var self = this;
      var pages = [];
      for (var pageId in self.pages) {
        var page = self.pages[pageId];
        pages.push(c(page.component, {tag: 'component', props: self.$route && self.$route.params, key:pageId}))
      }
      return c('div',
        {
          staticClass: 'pages',
          ref: 'pages',
          class: self.classesObject,
          on: {
            'page:beforeremove': self.onPageBeforeRemove
          }
        },
        [
          self.$slots.default,
          pages
        ]
      )
    },
    props: {
      'navbar-fixed': Boolean,
      'navbar-through': Boolean,
      'toolbar-fixed': Boolean,
      'toolbar-through': Boolean,
      'tabbar-fixed': Boolean,
      'tabbar-through': Boolean,
      'tabbar-labels-fixed': Boolean,
      'tabbar-labels-through': Boolean,
      'theme': String,
      'layout': String
    },
    data: function () {
      return {
        pages: {}
      }
    },
    computed: {
      classesObject: function () {
        var co = {
          'navbar-fixed': this.navbarFixed || this.navbarThrough && this.$theme.material,
          'navbar-through': this.navbarThrough,
          'toolbar-fixed': this.toolbarFixed,
          'toolbar-through': this.toolbarThrough,
          'tabbar-fixed': this.tabbarFixed,
          'tabbar-through': this.tabbarThrough,
          'tabbar-labels-fixed': this.tabbarLabelsFixed,
          'tabbar-labels-through': this.tabbarLabesThrough
        }
        if (this.theme) co['theme-' + this.theme] = true;
        if (this.layout) co['layout-' + this.layout] = true;
        return co;
      }
    },
    methods: {
      onPageBeforeRemove: function (e) {
        var idToRemove;
        for (var id in this.pages) {
          if (e.target === this.pages[id].pageElement) {
            idToRemove = id;
            break;
          }
        }
        if (idToRemove) this.$delete(this.pages, idToRemove);
      },
      onRouteChange: function (event) {
        var self = this;
        var pageComponent = event.route.component;
        var view = event.view;
        var currentView = self.$parent.f7View || self.$parent.$el.f7View;

        if (view !== currentView) return;

        const previousRoute = self.$route.router.findMatchingRoute(view.url) || { route: { path: '/', pagePath: '/' } };
        const pageRouteChanged = previousRoute.route.pagePath !== event.route.pagePath;
        const childRouteChanged = !pageRouteChanged && previousRoute.route.path !== event.route.path;
        const shouldUpdatePages = pageRouteChanged || (!childRouteChanged && (event.options.reload || view.params.allowDuplicateUrls));

        if (!shouldUpdatePages) return;

        var id = new Date().getTime();

        self.$set(self.pages, id, {component: pageComponent});

        view.allowPageChange = false;

        self.$nextTick(function () {
          var newPage = view.pagesContainer.querySelector('.page:last-child');

          self.pages[id].pageElement = newPage;

          view.allowPageChange = true;

          const options = Object.assign(event.options, {
            pageElement: newPage
          });

          if (options.isBack) {
            view.router.back(options);
          }
          else {
            view.router.load(options);
          }
        });
      }
    }
  }
</script>

<script>
  export default {
    render: function (c) {
      var self = this;
      var pages = [];
      for (var pageId in self.pages) {
        var page = self.pages[pageId];
        pages.push(c(page.component, {tag: 'component', props: self.$route.params, key:pageId}))
      }
      return c('div',
        {
          staticClass: 'pages',
          ref: 'pages',
          class: self.classesObject,
          on: {
            'page:beforeremove': self.onPageBeforeRemove
          }
        },
        [
          self.$slots.default,
          pages
        ]
      )
    },
    props: {
      'navbar-fixed': Boolean,
      'navbar-through': Boolean,
      'toolbar-fixed': Boolean,
      'toolbar-through': Boolean,
      'tabbar-fixed': Boolean,
      'tabbar-through': Boolean,
      'tabbar-labels-fixed': Boolean,
      'tabbar-labels-through': Boolean,
      'theme': String,
      'layout': String
    },
    data: function () {
      return {
        pages: {}
      }
    },
    computed: {
      classesObject: function () {
        var co = {
          'navbar-fixed': this.navbarFixed || this.navbarThrough && this.$theme.material,
          'navbar-through': this.navbarThrough,
          'toolbar-fixed': this.toolbarFixed,
          'toolbar-through': this.toolbarThrough,
          'tabbar-fixed': this.tabbarFixed,
          'tabbar-through': this.tabbarThrough,
          'tabbar-labels-fixed': this.tabbarLabelsFixed,
          'tabbar-labels-through': this.tabbarLabesThrough
        }
        if (this.theme) co['theme-' + this.theme] = true;
        if (this.layout) co['layout-' + this.layout] = true;
        return co;
      }
    },
    methods: {
      onPageBeforeRemove: function (e) {
        var idToRemove;
        for (var id in this.pages) {
          if (e.target === this.pages[id].pageElement) {
            idToRemove = id;
            break;
          }
        }
        if (idToRemove) this.$delete(this.pages, idToRemove);
      },
      onRouteChange: function (event) {
        var self = this;
        var pageComponent = event.route.component;
        var view = event.view;
        var currentView = self.$parent.f7View || self.$parent.$el.f7View;

        if (view !== currentView) return;

        const previousRoute = self.$route.router.findMatchingRoute(view.url) || { route: { path: '/', pagePath: '/' } };
        const pageRouteChanged = previousRoute.route.pagePath !== event.route.pagePath;
        const childRouteChanged = !pageRouteChanged && previousRoute.route.path !== event.route.path;
        const shouldUpdatePages = pageRouteChanged || (!childRouteChanged && (event.options.reload || view.params.allowDuplicateUrls));

        if (shouldUpdatePages) {
          var id = new Date().getTime();

          self.$set(self.pages, id, {component: pageComponent});

          view.allowPageChange = false;

          self.$nextTick(function () {
            var newPage = view.pagesContainer.querySelector('.page:last-child');

            self.pages[id].pageElement = newPage;

            view.allowPageChange = true;

            const options = Object.assign(event.options, {
              pageElement: newPage
            });

            if (options.isBack) {
              view.router.back(options);
            }
            else {
              view.router.load(options);
            }
          });
        }
      }
    }
  }
</script>


Fix notification card styling

Fix notification card styling

<template>

{{ receivedAtFormatted }}

</template>


[#] Check if Xsplit's scenes exist

[#] Check if Xsplit's scenes exist

<template>

    <Toolbar :title="'Settings'"></Toolbar>

        label
        <input type="text" id="title" class="mdc-text-field__input" :value="xsplit.title" v-model.lazy="xsplit.title" v-on:change="updateXsplit({title: $event.target.value})">
        Title

        photo
        <input type="text" id="picture" class="mdc-text-field__input" :value="xsplit.picture" v-model.lazy="xsplit.picture" v-on:change="updateXsplit({picture: $event.target.value})">
        Picture

        photo_album
        <input type="text" id="logo" class="mdc-text-field__input" :value="xsplit.logo" v-model.lazy="xsplit.logo" v-on:change="updateXsplit({logo: $event.target.value})">
        Logo

        photo_size_select_large
        <input type="text" id="background" class="mdc-text-field__input" :value="xsplit.background" v-model.lazy="xsplit.background" v-on:change="updateXsplit({background: $event.target.value})">
        Background

 0">
        <select class="mdc-select" v-on:change="changeScene($event)" id="activeScene">
          <option v-for="scene in xsplit.scenes" :key="scene._id" :value="scene._id" :selected="scene.active">{{ scene.name }}</option>
        </select>
        Scene

</template>

<template>

    <Toolbar :title="'Settings'"></Toolbar>

        label
        <input type="text" id="title" class="mdc-text-field__input" :value="xsplit.title" v-model.lazy="xsplit.title" v-on:change="updateXsplit({title: $event.target.value})">
        Title

        photo
        <input type="text" id="picture" class="mdc-text-field__input" :value="xsplit.picture" v-model.lazy="xsplit.picture" v-on:change="updateXsplit({picture: $event.target.value})">
        Picture

        photo_album
        <input type="text" id="logo" class="mdc-text-field__input" :value="xsplit.logo" v-model.lazy="xsplit.logo" v-on:change="updateXsplit({logo: $event.target.value})">
        Logo

        photo_size_select_large
        <input type="text" id="background" class="mdc-text-field__input" :value="xsplit.background" v-model.lazy="xsplit.background" v-on:change="updateXsplit({background: $event.target.value})">
        Background

        <select v-if="xsplit.scenes" class="mdc-select" v-on:change="changeScene($event)" id="activeScene">
          <option v-for="scene in xsplit.scenes" :key="scene._id" :value="scene._id" :selected="scene.active">{{ scene.name }}</option>
        </select>
        Scene

</template>


chore(DocParameters): fix lint

chore(DocParameters): fix lint

<template>

 0"
      class="px-2"
    >
      MISSING ITEMS:
      {{ missingItems.join(', ') }}

    <v-data-iterator
      :search="search"
      :items="computedItems"
      sort-by="name"
      :items-per-page="-1"
      class="component-parameters pa-2"
      hide-default-footer
    >
      <template #default="{ items }">
        <v-flex
          v-for="item in items"
          :key="item.name"
          xs12
          grey
          lighten-2
          mt-2
        >
          <v-layout wrap px-2 py-1>
            <v-flex
              v-for="(header, i) in headers"
              :key="header.value"
              :class="header.class"
            >

                <template v-if="i === 0">
                  <v-chip
                    v-if="item.newIn"
                    x-small
                    label
                    text-color="white"
                    color="primary"
                  >
                    New in — v{{ item.newIn }}
                  </v-chip>
                  <v-chip
                    v-else-if="item.deprecatedIn"
                    x-small
                    label
                    color="red lighten-3"
                  >
                    Deprecated in — v{{ item.deprecatedIn }}
                  </v-chip>
                </template>

            </v-flex>
          </v-layout>
          <v-layout
            grey
            lighten-4
            pa-2
            wrap
          >
            <v-flex grey--text text--darken-3 xs12>
              <doc-markdown
                :code="item.description"
                class="justify"
              />
            </v-flex>
            <v-flex>

              <doc-markup
                v-if="item.example"
                class="mt-2 mb-0"
                lang="ts"
                value="example"
              >{{ genTypescriptDef(item.example) }}</doc-markup>

            </v-flex>
          </v-layout>
        </v-flex>
      </template>
    </v-data-iterator>

</template>


feat : 嗯哼

feat : 嗯哼


Fix menu

Fix menu

<template>

    <sui-menu
      class="docs-sidebar"
      is="sui-sidebar"
      inverted="true"
      vertical="true"
      visible
    >
      <sui-menu-item>
        <sui-image :src="`${publicPath}static/images/logo.png`" spaced="right" size="mini" />

          Semantic UI Vue

            {{ version }}

      </sui-menu-item>
      <sui-menu-item>
        <sui-menu-header>Getting Started</sui-menu-header>
        <sui-menu-menu>
          <router-link is="sui-menu-item" to="/">Quick Start</router-link>

            Github
            <sui-icon name="github" />

        </sui-menu-menu>
      </sui-menu-item>
      <sui-menu-item>
        <sui-input
          icon="search"
          inverted
          placeholder="Start typing..."
          transparent
          v-model="search"
          @keypress.native="openFirst"
        />
      </sui-menu-item>
      <sui-menu-menu v-if="search">
        <router-link
          is="sui-menu-item"
          :key="entry.component"
          :active="!index"
          :to="entry.href"
          v-for="(entry, index) in matchingComponents"
        >
          {{ entry.content }}
          Press Enter
        </router-link>
      </sui-menu-menu>
      <sui-menu-item :key="mod.name" v-if="!search" v-for="mod in modules">
        <sui-menu-header>{{ mod.name }}</sui-menu-header>
        <sui-menu-menu>
          <router-link
            is="sui-menu-item"
            :key="comp"
            :to="getUrl(mod.name, comp)"
            v-for="comp in mod.components"
          >
            {{ comp }}
          </router-link>
        </sui-menu-menu>
      </sui-menu-item>
    </sui-menu>

      <router-view></router-view>

</template>

<template>

    <sui-menu
      class="docs-sidebar"
      is="sui-sidebar"
      inverted="true"
      vertical="true"
      visible
    >
      <sui-menu-item>
        <sui-image :src="`${publicPath}static/images/logo.png`" spaced="right" size="mini" />

          Semantic UI Vue

            {{ version }}

      </sui-menu-item>
      <sui-menu-item>
        <sui-menu-header>Quick Start</sui-menu-header>
        <sui-menu-menu>
          <router-link is="sui-menu-item" to="/">Usage</router-link>

            Github
            <sui-icon name="github" />

        </sui-menu-menu>
      </sui-menu-item>
      <sui-menu-item>
        <sui-input
          icon="search"
          inverted
          placeholder="Start typing..."
          transparent
          v-model="search"
          @keypress.native="openFirst"
        />
      </sui-menu-item>
      <sui-menu-menu v-if="search">
        <router-link
          is="sui-menu-item"
          :key="entry.component"
          :active="!index"
          :to="entry.href"
          v-for="(entry, index) in matchingComponents"
        >
          {{ entry.content }}
          Press Enter
        </router-link>
      </sui-menu-menu>
      <sui-menu-item :key="mod.name" v-if="!search" v-for="mod in modules">
        <sui-menu-header>{{ mod.name }}</sui-menu-header>
        <sui-menu-menu>
          <router-link
            is="sui-menu-item"
            :key="comp"
            :to="getUrl(mod.name, comp)"
            v-for="comp in mod.components"
          >
            {{ comp }}
          </router-link>
        </sui-menu-menu>
      </sui-menu-item>
    </sui-menu>

      <router-view></router-view>

</template>


移除不必要的data

移除不必要的data

<template>

请选择查看范围

        {{op.text}}

      <DatePicker
        v-show="currOption==='week'"
        type="daterange"
        v-model="date"
        placement="bottom-start"
        placeholder="请选择一个时间段"
        :options="weekPreSetting"
      ></DatePicker>

        <OptionGroup
          v-if="dateRangeListOpt.hasGroup"
          v-for="(group, index) in dateRangeListOpt.list"
          :key="index"
          :label="group.text"
        >
          {{item.text}}
        </OptionGroup>

        {{item.text}}

      查询
      {{targetDateRangeText}}

{{reportTitle}}

    <my-summary
      v-if="isShow"
      :data="weekReports"
      :export-name="fileName"
      :isloading="isloading"
      :showDetail="showDetailList"
      :showDate="showDate"
    ></my-summary>

</template>


add content page link to badge

add content page link to badge


fix(front): change current app on the app is deleted

<template>

          应用管理

        <vue-loading v-if="loading" type="spiningDubbles" color="rgb(90, 193, 221)" :size="{ width: '50px', height: '50px' }" />

              名称

              语言

              备注

              操作

                {{ row.name }}

                {{ row.language }}

                {{ row.description }}

                  编辑

                  删除

          <button class="btn btn-primary" @click="editApp({})">
            添加
          </button>

    <appEditModal ref="appEditModal" @save="onEdit($event)" />

</template>


#412: Changed Direct with No transfers in trip-results.

<template>

{{trip.segments[0].departureDate}}

{{trip.departure | formatTime(accountPreferences.timeFormat[0])}} {{trip.from}}

      	<div :class="`small ${transferQty===0 ? 'text-success' : transferQty===1 ? 'text-info' : 'text-warning'} text-uppercase transfer-indicator`">
          {{transferQty===0 ? $t("noTransfers") : transferQty===1 ? `${transferQty} ${$t("transfer")}` : `${transferQty} ${$t("transfers")}`}}

{{trip.arrival | formatTime(accountPreferences.timeFormat[0])}} {{trip.to}}

{{$t('travelTime')}}

{{trip.duration}}

      <a class="trip-details" :href="`#${index}`" @click="$emit('openTripDetails')" data-toggle="modal">{{$t('tripDetails')}}

          {{$t(fare.lexiconKeys.name)}} {{currency}}{{fare.valueToDisplay}}

</template>

<template>

{{trip.segments[0].departureDate}}

{{trip.departure | formatTime(accountPreferences.timeFormat[0])}} {{trip.from}}

      	<div :class="`small ${transferQty===0 ? 'text-success' : transferQty===1 ? 'text-info' : 'text-warning'} text-uppercase transfer-indicator`">
          {{transferQty===0 ? $t("direct") : transferQty===1 ? `${transferQty} ${$t("transfer")}` : `${transferQty} ${$t("transfers")}`}}

{{trip.arrival | formatTime(accountPreferences.timeFormat[0])}} {{trip.to}}

{{$t('travelTime')}}

{{trip.duration}}

      <a class="trip-details" :href="`#${index}`" @click="$emit('openTripDetails')" data-toggle="modal">{{$t('tripDetails')}}

          {{$t(fare.lexiconKeys.name)}} {{currency}}{{fare.valueToDisplay}}

</template>


Minor refactor

Minor refactor

<template>

                 0 && pagination.total > pagination.max" class="text-muted">
                  {{pagination.offset+1}}
                  -

                  {{pagination.offset+reports.length}}

                of

                  <span class="summary-count" :class="{ 'text-strong': pagination.total < 1, 'text-info': pagination.total > 0 }" v-if="pagination.total>=0">
                    {{pagination.total}}

                  {{$tc('execution',pagination.total>0?pagination.total:0)}}

          <activity-filter v-model="query" :event-bus="eventBus" :opts="filterOpts" v-if="showFilters"></activity-filter>

              <input type=checkbox id=auto-refresh v-model=autorefresh />
              {{$t('Auto refresh')}}

            0 && showBulkDelete" class="spacing-x">

                  <i18n path="bulk.selected.count">
                    {{bulkSelectedIds.length}}
                  </i18n>

                      <i18n path="select.all"/>

                      <i18n path="select.none"/>

                  <btn size="xs" type="danger" class="btn-fill"
                        :disabled="bulkSelectedIds.length<1"
                        @click="showBulkEditConfirm=true">
                      <i18n path="delete.selected.executions"/>
                  </btn>

                      <i18n path="cancel.bulk.delete"/>

              <btn size="xs" type="default" v-if="auth.deleteExec && !bulkEditMode" @click="bulkEditMode=true">
                  {{$t('bulk.delete')}}
              </btn>

    <modal  v-model="showBulkEditCleanSelections" id="cleanselections" :title="$t('Clear bulk selection')" append-to-body>

      <i18n tag="p" path="clearselected.confirm.text">
        {{bulkSelectedIds.length}}
      </i18n>

        <btn data-dismiss="modal" @click="showBulkEditCleanSelections=false">{{$t('cancel')}}</btn>
        <button type="submit"
                class="btn btn-default  "
                data-dismiss="modal"
                @click="bulkEditDeselectAll">
            {{$t('Only shown executions')}}
        </button>
        <button class="btn btn-danger "
                data-dismiss="modal"
                @click="bulkEditDeselectAllPages">
            {{$t('all')}}
        </button>

    </modal>

    <modal v-model="showBulkEditConfirm" id="bulkexecdelete" :title="$t('Bulk Delete Executions')" append-to-body>
      <i18n tag="p" path="delete.confirm.text">
        {{bulkSelectedIds.length}}
        {{$tc('execution',bulkSelectedIds.length)}}
      </i18n>

          <btn @click="showBulkEditConfirm=false">
              {{$t('cancel')}}
          </btn>
          <btn type="danger"
                @click="performBulkDelete"
                data-dismiss="modal">
              {{$t('Delete Selected')}}
          </btn>

    </modal>

    <modal v-model="showBulkEditResults" id="bulkexecdeleteresult" :title="$t('Bulk Delete Executions: Results')" append-to-body>

                        {{$t('Requesting bulk delete, please wait.')}}

 0"
                        class="text-info">

                        <i18n path="bulkresult.attempted.text" tag="p" >
                          {{bulkEditResults.requestCount}}
                        </i18n>

 0"
                        class="text-success">

                        <i18n path="bulkresult.success.text" tag="p" >
                          {{bulkEditResults.successCount}}
                        </i18n>

 0"
                            class="text-warning">

                        <i18n path="bulkresult.failed.text" tag="p" >
                          {{bulkEditResults.failedCount}}
                        </i18n>

 0">

{{message}}

{{bulkEditError}}

                <btn @click="showBulkEditResults=false">{{$t('close')}}</btn>

    </modal>

0" class="running-executions">

                <input
                type="checkbox"
                name="bulk_edit"
                :value="exec.id"
                v-model="bulkSelectedIds"
                :disabled="exec.status==='running' || exec.status==='scheduled' || exec.status==='queued'"
                class="_defaultInput"/>

                            {{exec.dateStarted.date | moment(momentJobFormat)}}

                            {{exec.dateStarted.date | moment('calendar',null)}}

                            {{exec.dateStarted.date | moment('from','now')}}

                      <progress-bar v-if="exec.status === 'scheduled'" :value="100" striped  type="default" label :label-text="$t('job.execution.starting.0',[runningStartedDisplay(exec.dateStarted.date)])" ></progress-bar>
                      <progress-bar v-else-if="exec.status === 'queued'" :value="100" striped  type="default" label :label-text="$t('job.execution.queued')" ></progress-bar>
                      <progress-bar v-else-if="exec.job && exec.job.averageDuration" :value="jobDurationPercentage(exec)" striped active type="info" label min-width></progress-bar>
                      <progress-bar v-else-if="exec.dateStarted.date" :value="100" striped active type="info" label :label-text="$t('running')" ></progress-bar>

                    <i18n path="by" default="by"/>
                    {{exec.user}}

                    {{exec.job.name}}

                          {{key}}
                          {{value}}

                    {{exec.description}}

                    #{{exec.id}}

0">

                {{ $tc('info.newexecutions.since.0', sincecount) }}

 0">
        <tr class="link activity_row autoclickable"
            @click="autoBulkEdit(rpt)"
            @click.middle="middleClickRow(rpt)"
            :class="[`ali-${rpt.execution.id}`,{succeed:rpt.status==='succeed',fail:rpt.status==='fail',missed:rpt.status==='missed',highlight:highlightExecutionId==rpt.executionId,job:rpt.jobId,adhoc:!rpt.jobId}]"
            v-for="rpt in reports"
            :key="rpt.execution.id"
          >

                <input
                type="checkbox"
                name="bulk_edit"
                :value="rpt.executionId"
                v-model="bulkSelectedIds"
                class="_defaultInput"/>

            <td class="right date"
              v-tooltip.bottom="{
                text: $t(rpt.status==='missed'?'info.missed.0.1':'info.completed.0.1',[jobCompletedISOFormat(rpt.dateCompleted),jobCompletedFromNow(rpt.dateCompleted)]),
                'viewport': `.ali-${rpt.execution.id}`
            }">

                        {{rpt.dateCompleted | moment(momentJobFormat)}}

                        {{rpt.dateCompleted | moment('calendar',null)}}

                        {{rpt.dateCompleted | moment('from','now')}}

                0">{{rpt.node.failed}} {{$t('failed')}}
                0">{{rpt.node.succeeded}} {{$t('ok')}}

                missed
                {{formatDurationMomentHumanize(rpt.duration)}}

                <i18n path="by" default="by"/>
                {{rpt.user}}

                    {{rpt.jobName}}

                    {{rpt.executionString}}

                    {{$t('Referenced')}}

                      {{$t('job.has.been.deleted.0',[rpt.jobName])}}

                    {{rpt.execution.status}}

                        {{key}}:
                        {{value}}

                {{rpt.execution.argString}}

                #{{rpt.executionId}}

    <div v-if="reports.length < 1 " class="loading-area">

          {{$t('results.empty.text')}}

        <div class="loading-text" v-if="loading && lastDate<0">

          {{$t('Loading...')}}

          {{$t('error.message.0',[loadError])}}

    <offset-pagination
          :pagination="pagination"
          @change="changePageOffset($event)"
          :disabled="loading"
          :showPrefix="false"
        >
        </offset-pagination>

</template>


Changed an import

Changed an import

<template>

    <v-toolbar color="primary" dark>
      <v-toolbar-title> Project requirements </v-toolbar-title>
      <v-spacer />
      <v-tooltip top transition="none">
        <template v-slot:activator="{ on, attrs }">
          <v-btn
            @click="getRecommendations"
            style="font-weight: bold"
            rounded
            depressed
            small
            v-on="on"
            v-bind="attrs"
            color="secondary"
            class="white--text"
          >
            Recommendations
            <v-icon color="white">mdi-checkbox-marked-circle</v-icon>
          </v-btn>
        </template>
        Proceed to fetching recommendations from the selected projects.
      </v-tooltip>
    </v-toolbar>
    <v-data-table
      :items="this.allRows"
      :hide-default-header="true"
      :headers="headers"
      item-key="name"
      class="text-center elevation-1"
    >
      <template v-slot:header="">

            Project
            APIs
            VM permissions
            Disks permissions
            Other permissions

      </template>

      <template
        v-for="requirement in requirementList"
        v-slot:[`item.${requirement}`]="item"
      >
        <v-tooltip
          :key="requirement"
          v-if="item.item.satisfiesRequirement(requirement)"
          top
          transition="none"
        >
          <template v-slot:activator="{ on, attrs }">
            <v-icon v-on="on" v-bind="attrs" color="green"
              >mdi-check-bold</v-icon
            >
          </template>
          Requirement for {{ requirement }} is satisfied.
        </v-tooltip>
        <v-tooltip
          :key="requirement"
          v-else-if="!item.item.hasRequirement(requirement)"
          top
          transition="none"
        >
          <template v-slot:activator="{ on, attrs }">
            <v-icon v-on="on" v-bind="attrs" color="grey">mdi-help</v-icon>
          </template>
          One of the requirements needed for checking the
          {{ requirement }} requirement is not satisfied.
        </v-tooltip>
        <v-tooltip :key="requirement" v-else top transition="none">
          <template v-slot:activator="{ on, attrs }">
            <v-icon v-on="on" v-bind="attrs" color="red"
              >mdi-alert-circle</v-icon
            >
          </template>
          {{ item.item.getErrorMessage(requirement) }}
        </v-tooltip>
      </template>
    </v-data-table>

</template>


fix: update the mail address

fix: update the mail address

<template>

{{ $t("about.title") }}

        <form action="https://formspree.io/pulsar.dev.team@gmail.com" method="POST" name="sentMessage" id="contactForm" novalidate>

            {{ $t('contact.form.name.label') }}
            <input v-model="name" type="text" name="name" class="form-control" :placeholder="$t('contact.form.name.label') + '*'" id="name" required :data-validation-required-message="$t('contact.form.name.validation')">

            {{ $t('contact.form.email.label') }}
            <input v-model="email" type="email" name="_replyto" class="form-control" :placeholder="$t('contact.form.email.label') + '*'" id="email" required :data-validation-required-message="$t('contact.form.email.validation')">
            {{ $t('contact.form.email.helpText') }}

{{ $t('contact.form.email.error') }}

            {{ $t('contact.form.message.label') }}
            <textarea v-model="message" name="message" class="form-control" :placeholder="$t('contact.form.message.label') + '*'" id="message" rows="5" required :data-validation-required-message="$t('contact.form.message.validation')"></textarea>

          <button type="submit" class="btn btn-primary" :disabled="!canSubmit()">{{ $t('contact.form.submit') }}</button>
        </form>

</template>

<template>

{{ $t("about.title") }}

        <form action="https://formspree.io/teampulsar.dev@gmail.com" method="POST" name="sentMessage" id="contactForm" novalidate>

            {{ $t('contact.form.name.label') }}
            <input v-model="name" type="text" name="name" class="form-control" :placeholder="$t('contact.form.name.label') + '*'" id="name" required :data-validation-required-message="$t('contact.form.name.validation')">

            {{ $t('contact.form.email.label') }}
            <input v-model="email" type="email" name="_replyto" class="form-control" :placeholder="$t('contact.form.email.label') + '*'" id="email" required :data-validation-required-message="$t('contact.form.email.validation')">
            {{ $t('contact.form.email.helpText') }}

{{ $t('contact.form.email.error') }}

            {{ $t('contact.form.message.label') }}
            <textarea v-model="message" name="message" class="form-control" :placeholder="$t('contact.form.message.label') + '*'" id="message" rows="5" required :data-validation-required-message="$t('contact.form.message.validation')"></textarea>

          <button type="submit" class="btn btn-primary" :disabled="!canSubmit()">{{ $t('contact.form.submit') }}</button>
        </form>

</template>


Change indentation

Change indentation

<template>
<div>

</div>
</template>

<script>
export default {
  data() {
    return {

    };
  }
}
</script>

<style lang="scss" scoped>
  @import '~styles/vars.scss';
</style>


update cover image postion

update cover image postion

<template>

FEweekly

👍🏼{{info.type_name}}

{{list.title}}

{{list.describe}}

  <el-menu :default-active="activeIndex2" class="history" mode="horizontal" @select="handleSelect" style="width:116px; position:fixed; right:10px; top:10px;  border-top:1px solid #e5e5e5; background:#fff; ">
      <el-submenu index="2">
            <template slot="title">往期内容</template>

                 <el-menu-item :index="data.period">第{{data.period}}期</el-menu-item>

      </el-submenu>
   </el-menu>

</template>

<template>

👍🏼{{info.type_name}}

{{list.title}}

{{list.describe}}

  <el-menu :default-active="activeIndex2" class="history" mode="horizontal" @select="handleSelect" style="width:116px; position:fixed; right:10px; top:10px;  border-top:1px solid #e5e5e5; background:#fff; ">
      <el-submenu index="2">
            <template slot="title">往期内容</template>

                 <el-menu-item :index="data.period">第{{data.period}}期</el-menu-item>

      </el-submenu>
   </el-menu>

</template>


Ensure the first registration is the Member itself

<template>

October 1st — Church Picnic

             Register today

You must update your Profile with your name before you can register. To do so, click <router-link to="/profile">here</router-link>.

          <form>

To start, click the button to Register yourself.

After that, you can add and register other persons, for example: your family members, or your friends (Adventist or not).

The Church will cover both Transportation and the Entry fees when we get there, but only if the person is a Member of the Church or a Sabbath School class.

                  <button @click="showSubForm = true" class="btn btn-info" type="button">
                    <template v-if="!selfInSubform">Register myself</template>
                    <template v-else>Register another person</template>
                  </button>
                  <button v-if="subFormSelectedIndex !== -1" @click="removePersonFromSubform"
                          class="btn btn-danger" type="button">Remove</button>

                    Name of the person
                    <input v-model="subFormData.name" type="text" class="form-control" />

                    Please check what applies

                        <input v-model="subFormData.wantsTransportation" type="checkbox" />Need Special Transportation

                        <input v-model="subFormData.isChurchMember" type="checkbox" />Member of the English-Speaking Church

                        <input v-model="subFormData.isSabbathSchoolMember" type="checkbox" />Registered in one of the English-Speaking Church Sabbath school class

                    <button @click.prevent.stop="addPersonToSubform"
                        :disabled="selfInSubform && subFormData.name.length < 2" class="btn btn-primary" type="button">Add to the List</button>
                    <button @click="showSubForm = false" class="btn btn-default" type="button">Do not add</button>

                      Name
                      Fees

                        {{registration.name}}
                        {{memberName(registration.memberId)}}

                        (Covered by the Church)
                        {{2000 + (registration.wantsTransportation ? 4000 : 0)}} Ar

The fees must be paid on, or before Wednesday 27th of September 2017. If you miss this date, your registration will be removed.

The Church is covering Transportation for one or more of the persons you have registered. Those must go with the Special Transportation on sunday, otherwise, you will pay the full Transportation fees of 4000 Ar per person.

                  <button :disabled="!selfInSubform" class="btn btn-primary" type="button"> Finish + Confirm registration</button>
                  <button @click="showRegistrationForm = false, showSubForm = false" class="btn btn-default" type="button">Cancel registration</button>

          </form>

You need to pay the fees, totalling xxx Ar. Remember, the deadline is on Wednesday.

Here are the list of those going ...

Don't wait, those who come first will enter first when we arrive.

              Registered by
              Name
              When

              Cell 1
              Cell 2
              Cell 2

              Cell 3
              Cell 2
              Cell 4

</template>

<template>

October 1st — Church Picnic

             Register today

You must update your Profile with your name before you can register. To do so, click <router-link to="/profile">here</router-link>.

          <form>

To start, click the button to Register yourself.

After that, you can add and register other persons, for example: your family members, or your friends (Adventist or not).

The Church will cover both Transportation and the Entry fees when we get there, but only if the person is a Member of the Church or a Sabbath School class.

                  <button @click="showSubForm = true" class="btn btn-info" type="button">
                    <template v-if="registrationFromForm.length === 0">Register myself</template>
                    <template v-else>Register another person</template>
                  </button>
                  <button class="btn btn-danger" type="button">Remove</button>

 0" class="form-group">
                    Name of the person
                    <input v-model="subFormData.name" type="text" class="form-control" />

                    Please check what applies

                        <input v-model="subFormData.wantsTransportation" type="checkbox" />Need Special Transportation

                        <input v-model="subFormData.isChurchMember" type="checkbox" />Member of the English-Speaking Church

                        <input v-model="subFormData.isSabbathSchoolMember" type="checkbox" />Registered in one of the English-Speaking Church Sabbath school class

                    <button @click.prevent.stop="addPersonToSubform"
                        :disabled="registrationFromForm.length !== 0 && subFormData.name.length < 2" class="btn btn-primary" type="button">Add to the List</button>
                    <button @click="showSubForm = false" class="btn btn-default" type="button">Do not add</button>

                      Name
                      Fees

                        {{registration.name}}
                        {{memberName(registration.memberId)}}

                        (Covered by the Church)
                        {{2000 + (registration.wantsTransportation ? 4000 : 0)}} Ar

The fees must be paid on, or before Wednesday 27th of September 2017. If you miss this date, your registration will be removed.

The Church is covering Transportation for one or more of the persons you have registered. Those must go with the Special Transportation on sunday, otherwise, you will pay the full Transportation fees of 4000 Ar per person.

                  <button class="btn btn-primary" type="button"> Finish + Confirm registration</button>
                  <button @click="showRegistrationForm = false, showSubForm = false" class="btn btn-default" type="button">Cancel registration</button>

          </form>

You need to pay the fees, totalling xxx Ar. Remember, the deadline is on Wednesday.

Here are the list of those going ...

Don't wait, those who come first will enter first when we arrive.

              Registered by
              Name
              When

              Cell 1
              Cell 2
              Cell 2

              Cell 3
              Cell 2
              Cell 4

</template>