Commit 18365e43 by Vitalik

Init

parents
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
node_modules
npm-debug.log
121 App
=================
## Install dependencies
``` javascript
npm install
```
## Dev
``` javascript
npm run dev
```
## Prod
``` javascript
npm run build
```
## DB
``` SQL
ALTER TABLE `guests` ADD `state` ENUM('planned','present','absent') NOT NULL DEFAULT 'planned' AFTER `resrt`;
```
{
"name": "onetoone",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev_": "cross-env NODE_ENV=development HOT=1 NODE_PORT=3030 node webpack/devServer.js",
"dev": "rm -rf ./build && NODE_ENV=development webpack --progress --colors --watch --config webpack/webpack.config.dev.js",
"build": "rm -rf ./build && webpack --progress --colors --config webpack/webpack.config.prod.js",
"publish:demo": "npm run build && surge -d vuepack.surge.sh -p build"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-core": "6.11.4",
"babel-loader": "6.2.4",
"babel-plugin-transform-runtime": "6.9.0",
"babel-preset-es2015": "6.9.0",
"babel-preset-stage-0": "6.5.0",
"babel-runtime": "6.9.2",
"css-loader": "0.23.1",
"date-input-polyfill": "2.11.4",
"extract-text-webpack-plugin": "1.0.1",
"kountdown": "0.1.2",
"moment": "2.15.1",
"postcss-cssnext": "2.7.0",
"pug": "2.0.0-beta4",
"resolve-url-loader": "1.6.0",
"style-loader": "0.13.1",
"stylus": "0.54.5",
"stylus-loader": "2.1.2",
"vue": "1.0.26",
"vue-data-table": "0.1.1",
"vue-hot-reload-api": "2.0.5",
"vue-html-loader": "1.2.3",
"vue-loader": "8.5.3",
"vue-multiselect": "1.1.3",
"vue-resource": "0.9.3",
"vue-router": "0.7.13",
"vue-style-loader": "1.0.0",
"vuex": "1.0.0-rc.2",
"webpack": "1.13.1"
}
}
<template lang="pug">
#app
top-nav
router-view
modal-form-candidate
modal-move-candidate
</template>
<script>
import 'date-input-polyfill'
import store from './vuex/store'
import {
setServerSync,
setDev,
updateGuests,
updateSchedule,
updateScheduleGuests,
updateCandidates,
updateSchools,
updateReserve,
syncMapScheduleSlots,
syncMapGuestSlot,
syncMapGuestSchool,
} from './vuex/actions'
import { SyncResource } from './resources'
import KountdownLib from 'kountdown'
import topNav from './components/top-nav'
import modalFormCandidate from './components/modal-form-candidate'
import modalMoveCandidate from './components/modal-move-candidate'
import moment from 'moment'
const PERIOD_UPDATE = 5000
export default {
store,
data() {
return {
countdownData: {
totalSeconds: 0
},
currentGuests: [],
currentTime: null,
}
},
ready() {
window.app = this
setInterval(() => {
if (!this.serverSync || !this.routeIsSchedule) return
if (!this.meetingId) return
SyncResource.get({
meetingId: this.meetingId,
schoolId: this.schoolId
}).then((res) => {
updateSchools(store, store.state.schools, res.data.schools)
updateGuests(store, store.state.guests, res.data.guests)
updateCandidates(store, store.state.candidates, res.data.candidates, store.state.editableCandidateId)
updateScheduleGuests(store, store.state.scheduleGuests, res.data.schedule_guests, store.state.schedule)
updateReserve(store, store.state.reserve, res.data.reserve)
syncMapScheduleSlots(store, store.state.mapScheduleSlots, res.data.mapScheduleSlots)
syncMapGuestSlot(store, store.state.mapGuestSlot, res.data.mapGuestSlot)
syncMapGuestSchool(store, store.state.mapGuestSchool, res.data.mapGuestSchool)
})
}, PERIOD_UPDATE)
setInterval(() => {
if (!this.routeIsSchedule) return
if (this.currentSlots.length) {
let changedCurrentTime = false
let newCurrentTime = this.currentSlots.map(x => x.id).pop()
if (newCurrentTime != this.currentTime) {
this.currentTime = newCurrentTime
changedCurrentTime = true
}
if (!this.countdownData.totalSeconds || changedCurrentTime) {
this.kountdown
.setEndDate(this.currentSlots.pop().end / 1000)
.start()
}
this.currentGuests = this.currentSlots
.map(slot => slot.guests)
.reduce((a, b) => {
return a.concat(b)
})
} else {
this.kountdown.setEndDate(new Date())
this.currentGuests = []
}
}, 1000)
if (this.user.role == 'organizer') {
}
},
vuex: {
getters: {
serverSync: state => state.serverSync,
meetingId: state => state.meeting.id,
schoolId: state => state.schoolId,
slots: state => {
let date = new Date,
y = date.getFullYear(),
m = date.getMonth() + 1,
d = date.getDate()
let currentMD = `${y}-${m < 10 ? '0' + m : m}-${d < 10 ? '0' + d : d}`
return Object.values(state.schedule)
.map(time => {
if (state.dev) {
time.timeslot = time.timeslot.replace(/\d{4}-\d{2}-\d{2}/, currentMD)
time.timeslotend = time.timeslotend.replace(/\d{4}-\d{2}-\d{2}/, currentMD)
}
let slots = (state.mapScheduleSlots[time.key] || [])
let guests = Object.entries(state.mapGuestSlot)
.filter(([guestId, slotId]) => {
return state.mapGuestSchool[guestId] == state.schoolId &&
slots.find(id => id == slotId)
})
.map(([guestId]) => guestId)
return {
id : time.key,
start : moment(time.timeslot, 'YYYY-MM-DD HH:mm:ss'),
end : moment(time.timeslotend, 'YYYY-MM-DD HH:mm:ss'),
guests
}
})
.filter(time => time.guests)
}
},
actions: {
setServerSync,
setDev
}
},
computed: {
routeName() {
return this.$route.name
},
routeIsSchedule() {
return this.routeName.includes('schedule')
},
user() {
let user = window.user
user.permissions = new Set(Object.entries(user.permissions).filter(([k, v]) => v).map(([k]) => k))
return user
},
kountdown() {
return new KountdownLib({
endDate: new Date(),
stringFormatter: 'mm:ss',
onUpdate: (data) => {
this.countdownData = data
}
})
},
currentSlots: {
cache: false,
get() {
let currentTime = moment()
return this.slots
.filter(slot => {
return currentTime.isBetween(slot.start, slot.end)
})
}
},
},
components: {
topNav,
modalFormCandidate,
modalMoveCandidate,
}
}
</script>
<style lang="stylus">
html,
body
overflow-x: hidden
body
padding-top: 4.3rem
footer
padding: 1.8rem 0
#app
margin-bottom: 20vh
</style>
<template lang="pug">
.countdown( :class='{danger: danger}', v-show='show && candidates.length', transition='fade')
center
.time
| {{ data.stringValue }}
.desc(v-if='candidates.length')
span.separator to finish with
ul(v-if='candidates.length > 1')
li(v-for='item in candidates') {{ item }}
span(v-else) {{ candidates[0] }}
</template>
<script>
const ALARM_MINUTES_LEFT = 5
export default {
props: {
data: {
type: Object,
default() {
return {
stringValue: '00:00',
minutes: 0,
totalSeconds: 0
}
}
},
candidates: {
type: Array,
default: []
}
},
computed: {
show() {
return !!this.data.totalSeconds
},
danger() {
return this.data.minutes < ALARM_MINUTES_LEFT
}
}
}
</script>
<style lang="stylus" scoped>
$height = 8vh
$font-size = 6vh
$color-time = white
$color-desc = white
.countdown
position: fixed
display: block
bottom: 0
left: 0
z-index: 1000
width: 100vw
background-color: hsla(0, 0%, 0%, 0.67)
transition: background-color 1000ms linear
height: $height
&.danger
background-color: hsla(0, 95%, 39%, 0.67)
.time
display: inline-block
font-family: monospace
text-align: center
white-space: nowrap
font-size: $font-size
line-height: $height
font-weight: bold
color: $color-time
.desc
display: inline-block
font-size: ($font-size / 2)
font-weight: normal
color: $color-desc
.separator
padding: 0 1vw
ul
font-size: ($font-size / 3)
padding-left: 0
margin-top: 0
list-style: none;
display: inline-block
text-align: left;
</style>
<template lang="pug">
p.lead Export event applicants
.row
.col-xs-12.col-md-6.form-group
multiselect(
:options='meetings',
:searchable='true',
:close-on-select='true',
:custom-label='selectMeetingLabel',
:allow-empty='false',
@update='selectMeeting',
deselect-label=''
key='id'
label='name'
placeholder='Select event'
)
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox', v-model='showArchived')
span.custom-control-indicator
span.custom-control-description Show archived
.col-xs-12.col-md-6.form-group
multiselect(
:options='schools',
:selected='selectedSchool',
:searchable='true',
:close-on-select='true',
:allow-empty='false',
:disabled='!selectedMeeting.id',
@update='selectSchool',
deselect-label=''
key='id'
label='name'
placeholder='Select BS'
)
form(action='/export', method='GET')
input(type='hidden', name='meetingId', :value='selectedMeeting.id')
input(type='hidden', name='schoolId', :value='selectedSchool.id')
.row
.col-xs-12
button.btn.btn-outline-primary(
type='submit',
:disabled='!selectedSchool.id || !selectedMeeting.id',
@click='exportToXLS'
) Export to XLS
</template>
<script>
import Multiselect from 'vue-multiselect'
import { getDataForExport } from '../vuex/actions'
import { ExportResource } from '../resources'
export default {
data() {
return {
showArchived: false,
selectedMeeting: {},
selectedSchool: {},
}
},
vuex: {
getters: {
dataForExport: state => state.dataForExport,
},
actions: {
getDataForExport
}
},
created() {
this.getDataForExport()
},
computed: {
meetings() {
if (!this.dataForExport.meetings) return []
if (this.showArchived) return this.dataForExport.meetings
return this.dataForExport.meetings.filter(meeting => meeting.archived == 0)
},
schools() {
if (!this.selectedMeeting || !this.selectedMeeting.id) return []
let schoolsId = Array.from(this.dataForExport.mapMeetingSchools[this.selectedMeeting.id])
if (!schoolsId || !schoolsId.length) return []
return this.dataForExport.schools.filter(school => schoolsId.includes(school.id))
}
},
methods: {
selectMeetingLabel({ name, archived }) {
if (archived == 1) {
return `${name} [archived]`
}
return name
},
selectMeeting(meeting) {
this.selectedSchool = {}
this.selectedMeeting = meeting
},
selectSchool(school) {
this.selectedSchool = school
},
exportToXLS() {
if (!this.selectedMeeting.id || !this.selectedSchool.id) return
ExportResource.get({
meetingId: this.selectedMeeting.id,
schoolId: this.selectedSchool.id,
}).then(res => {
})
}
},
components: {
Multiselect
}
}
</script>
<template lang="pug">
.row
.col-xs-12
p.lead Meetings
div.m-b-1(v-if='role == "organizer"')
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox', v-model='showArchived')
span.custom-control-indicator
span.custom-control-description Show archived
.row
.col-xs-12.col-sm-6.col-md-4.col-lg-3(v-for='meeting in filteredMeetings')
.card
img.card-img-top.img-fluid( :src='meeting.img')
.card-block
.card-title {{ meeting.name }}
div(v-if='dev')
span.tag.tag-default id:{{ meeting.id }}
| &nbsp;
span.tag.tag-warning(v-if='meeting.archived == 1') Archived
a.card-link.pull-xs-right(href="#", @click='goToMeeting(meeting)') Schedule
</template>
<script>
import { ScheduleResource } from '../resources'
import { bootstrap, setMeeting } from '../vuex/actions'
export default {
data() {
return {
showArchived: false
}
},
vuex: {
getters: {
dev: state => state.dev,
meetings: state => state.meetings
},
actions: {
bootstrap,
setMeeting
}
},
computed: {
filteredMeetings() {
if (this.showArchived) return this.meetings
return this.meetings.filter(meeting => meeting.archived == 0)
}
},
methods: {
goToMeeting(meeting) {
ScheduleResource.get({ meetingId: meeting.id })
.then((res) => {
this.bootstrap(res.data)
this.$nextTick(() => {
this.$route.router.go({path: `/schedule/${meeting.id}`})
})
})
}
}
}
</script>
<style lang="stylus" scoped>
.card
img
width: 100%
max-height: 108px
.card-block
padding: 1rem
</style>
<template lang="pug">
Modal(
:show.sync='show',
@hidden='onClose',
modal-size='modal-lg'
title='Move 121 to another slot'
)
div(slot='body')
form(v-if='candidate')
h4 {{ candidate.firstname }} {{ candidate.surname }}
hr
.row
.col-xs-12.col-sm-6
h5 From
.form-group.row
label(class='col-sm-12 col-md-2 col-form-label') School:
.col-sm-12.col-md-10
p.form-control-static: strong {{ schoolFrom.name }}
.form-group.row
label(class='col-sm-12 col-md-2 col-form-label') Time:
.col-sm-12.col-md-10
p.form-control-static: strong {{ timeFrom.start }}
.col-xs-12.hidden-sm-up
hr
.col-xs-12.col-sm-6
h5 To
.form-group.row
label(class='col-sm-12 col-md-2 col-form-label') School:
.col-sm-12.col-md-10
multiselect(
:options="schools",
:selected="schoolFrom",
:searchable="true",
:close-on-select="true",
:allow-empty="false",
@update="selectSchool",
deselect-label=""
key="id"
label="name"
placeholder="Select school"
)
.form-group.row
label(class='col-sm-12 col-md-2 col-form-label') Time:
.col-sm-12.col-md-10
multiselect(
:options="freeTimesBySchool",
:selected="selectedSlot",
:searchable="true",
:close-on-select="true",
:custom-label="selectSlotLabel",
:disabled='(!freeTimesBySchool && !freeTimesBySchool.length) || existsInSchool',
:allow-empty="false",
@update="selectTime",
deselect-label=""
key="id"
label="time"
placeholder="Select time"
)
div(slot='footer')
button.btn.btn-secondary(type='button', data-dismiss='modal') Close
| &nbsp;
button.btn.btn-primary(type='button', :disabled='!moveToSlot', @click='move') Save changes
</template>
<script>
import Multiselect from 'vue-multiselect'
import Modal from './modal'
import {
movableCandidateData,
patchGuest,
deleteReserve,
updateMapGuestSlot,
updateMapGuestSchool,
} from '../vuex/actions'
const MAX_CANDIDATES_IN_SLOT = 2
export default {
data() {
return {
show: false,
moveToSchool: null,
moveToSlot: null,
selectedSlot: {},
existsInSchool: false
}
},
vuex: {
getters: {
guests: state => Object.values(state.guests),
candidateData: state => state.movableCandidateData,
candidate: state => state.candidates[state.movableCandidateData.candidateId || null],
schoolFrom: state => state.schools[state.movableCandidateData.schoolId || null] || {},
timeFrom: state => {
if (state.movableCandidateData.scheduleId == -1) return { start: '- reserv -' }
let [time] = Object.entries(state.mapScheduleSlots)
.find(([time, slots]) => slots.includes(state.movableCandidateData.scheduleId))
return state.schedule[time]
},
schools: state => Object.values(state.schools),
freeTimes: state => {
let freeTimes = {}
for (let school of Object.values(state.schools)) {
let busySlots = Object.entries(state.mapGuestSchool)
.filter(([guestId, schoolId]) => {
return schoolId == school.id &&
state.guests[guestId].schedule_id
})
.map(([guestId]) => state.mapGuestSlot[guestId])
freeTimes[school.id] = Object.values(state.schedule)
.map(time => {
let freeSlots = state.mapScheduleSlots[time.key]
.filter(slotId => {
return !busySlots.includes(slotId) &&
!school.closed121.includes(slotId)
})
return {
id : time.key,
time : time.start,
slots : state.mapScheduleSlots[time.key],
freeSlots,
}
})
.filter(time => {
if (
school.id == state.movableCandidateData.schoolId && time.slots.includes(state.movableCandidateData.scheduleId)
) return false
return time.freeSlots.length
})
}
return freeTimes
}
},
actions: {
movableCandidateData,
patchGuest,
deleteReserve,
updateMapGuestSlot,
updateMapGuestSchool,
}
},
computed: {
freeTimesBySchool() {
if (this.existsInSchool) return []
if (!this.moveToSchool) this.moveToSchool = this.schoolFrom.id
return this.freeTimes[this.moveToSchool] || []
}
},
watch: {
candidateData(data) {
if (data.candidateId) {
this.show = true
this.selectSchool(this.schoolFrom)
}
}
},
methods: {
selectSchool(school) {
this.moveToSchool = school.id
this.moveToSlot = null
this.selectedSlot = {}
this.existsInSchool = !!this.guests.find(guest => {
return (
guest.id != this.candidateData.guestId &&
guest.candidate_id == this.candidate.id &&
guest.school_id == school.id
)
})
},
selectTime(time) {
this.moveToSlot = time.freeSlots.shift()
},
selectSlotLabel({ time, freeSlots }) {
if (!time) return ''
return `${time} - ${freeSlots.length} free`
},
move(e) {
if (!this.moveToSchool || !this.moveToSlot) return
if (!this.permissions.has('candidate.move')) return
let $button = $(e.currentTarget)
$button.prop('disabled', true)
this.patchGuest(this.candidateData.guestId, {
school_id : this.moveToSchool,
schedule_id : this.moveToSlot,
// managerstatus : 1,
// schoolstatus : 3
}).then(() => {
if (this.candidateData.reserve) {
this.deleteReserve(this.candidateData.schoolId, this.candidateData.reserve)
}
/*this.$nextTick(() => {
this.updateMapGuestSchool(this.candidateData.guestId, this.moveToSchool)
this.$nextTick(() => {
this.updateMapGuestSlot(this.candidateData.guestId, this.moveToSlot)
})
})*/
/*setTimeout(() => {
this.updateMapGuestSchool(this.candidateData.guestId, this.moveToSchool)
}, 100)
setTimeout(() => {
this.updateMapGuestSlot(this.candidateData.guestId, this.moveToSlot)
}, 100)*/
this.updateMapGuestSchool(this.candidateData.guestId, this.moveToSchool)
this.updateMapGuestSlot(this.candidateData.guestId, this.moveToSlot)
$button.prop('disabled', false)
this.show = false
})
},
onClose() {
this.moveToSchool = null
this.moveToSlot = null
this.movableCandidateData({})
}
},
components: {
Modal,
Multiselect
}
}
</script>
<style lang="stylus" scoped>
</style>
<template lang="pug">
.modal.fade(tabindex='-1', role='dialog', transition='modal')
.modal-dialog( :class='modalSize')
.modal-content
.modal-header
button.close(type='button', data-dismiss='modal', aria-label='Close')
span(aria-hidden='true') &times;
h4 {{ title }}
.modal-body
slot(name='body')
.modal-footer
slot(name='footer')
</template>
<script>
export default {
props: {
show: {
type: Boolean,
required: true,
twoWay: true
},
modalSize: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
ready() {
$(this.$el).on('hidden.bs.modal', () => {
this.show = false
this.$emit('hidden')
})
},
watch: {
show(show) {
$(this.$el).modal(['hide', 'show'][+show])
}
}
}
</script>
<style lang="stylus">
.modal-enter,
.modal-leave
opacity: 0
.modal-enter .modal-container,
.modal-leave .modal-container
transform: scale(1.1)
</style>
<template lang="pug">
modal-form-newcomer( :show.sync='showModalForm', v-if='allowNewcomer')
button.btn.btn-secondary(type='button', @click='addCandidate', v-if='allowNewcomer') New comer
</template>
<script>
import modalFormNewcomer from './modal-form-newcomer'
import {
patchGuest,
editCandidateProfile,
movableCandidateData,
updateMapGuestSlot,
addReserve
} from '../vuex/actions'
export default {
data() {
return {
showModalForm: false
}
},
vuex: {
getters: {
},
actions: {
patchGuest,
editCandidateProfile,
movableCandidateData,
updateMapGuestSlot,
addReserve
}
},
computed: {
allowNewcomer() {
return this.permissions.has('candidate.full')
}
},
methods: {
addCandidate(e) {
e.preventDefault()
this.showModalForm = true
},
},
components: {
modalFormNewcomer
}
}
</script>
<template lang="pug">
.container
.text-xs-center
h1 Oops!
h4 404 Not Found
p.lead Sorry, an error has occurred, Requested page not found!
a.btn.btn-primary(v-link='{ name: "index" }') Take Me Home
</template>
<script>
export default {
}
</script>
<template lang="pug">
.card
.card-block
h4.card-title Reserve
ul.list-group.list-group-flush
li.list-group-item(v-for='reserve in reserveList')
| {{ reserve.candidate.firstname }} {{ reserve.candidate.surname }}
.pull-xs-right(v-if='permissions.has("reserve.move")')
button.btn.btn-secondary.btn-sm(type='button', @click='moveCandidate(reserve)') Move
newcomer
</template>
<script>
import newcomer from './newcomer'
import {
movableCandidateData
} from '../vuex/actions'
export default {
vuex: {
getters: {
reserveList: state => {
return (state.reserve[+state.schoolId] || []).map(item => {
item.candidate = state.candidates[item.candidate_id]
return item
})
},
},
actions: {
movableCandidateData
}
},
methods: {
moveCandidate(reserve) {
this.movableCandidateData({
candidateId : reserve.candidate_id,
guestId : reserve.guest_id,
schoolId : reserve.school_id,
scheduleId : -1,
reserve
})
}
},
components: {
newcomer
}
}
</script>
<style lang="stylus">
.list-group-flush
.list-group-item
border-left: 0
border-right: 0
</style>
<template lang='pug'>
.candidate( :class='state')
.title {{ stateIcon }} {{ candidate.firstname }}&nbsp;{{ candidate.surname }}&nbsp;
span.tag.tag-default(v-if='dev')
| {{ candidate.id }}
small.number(v-if='role != "school"') {{ candidate.phone }}
</template>
<script>
export default {
props: [ 'candidate', 'state' ],
data() {
return {
stateIcons: {
present: '✔',
absent: '✘'
}
}
},
vuex: {
getters: {
dev: state => state.dev,
}
},
computed: {
stateIcon() {
return this.stateIcons[this.state]
}
}
}
</script>
<style lang="stylus" scoped>
.candidate
&.absent
opacity: .5
</style>
<template lang="pug">
table#grid.table.table-sm.nowrap
thead
tr
th(style='height:72px') Time
th(v-for='school in schools')
img(height='50', :src='school.picture', :alt='school.name', :title='school.name')
span.hidden-xs-up {{ school.name }}
tbody
tr(v-for='time in schedule')
td.headcol
p {{ time.start }}
strong: div.countdown(class='countdown-{{ time.key }}', style='display:none')
td.schedule-time(v-for='school in schools')
.schedule-slot(v-for='slotId in mapScheduleSlots[time.key]')
span.tag.tag-default(v-if='checkClosedSlot(school.id, slotId)') CLOSED
div(v-else)
span.tag.tag-default(v-if='!((scheduleGuests[school.id] || [])[slotId] || []).length') Free
schedule-slot( :slot-id='slotId', :current='false', :school-id='school.id')
</template>
<script>
import scheduleGuest from './schedule-guest'
import scheduleSlot from './schedule-slot'
import dataTablesBS4 from '../helpers/dataTables.bootstrap4'
const PAGE_LENGTH = 3
const ALARM_MINUTES_LEFT = 5
export default {
ready() {
dataTablesBS4()
$.fn.DataTable.ext.pager.times = (pages) => {
return Object.values(this.schedule)
.filter((_, key) => {
return key % PAGE_LENGTH == 0
})
.map(x => x.start)
}
$('#grid').on('init.dt', () => {
this.$nextTick(() => {
this.$emit('updateCountdown')
})
})
const table = $('#grid').DataTable({
dom: 'Btp',
pagingType: 'times',
stateSave: true,
buttons: [{
extend: 'colvis',
text: 'Schools visibility',
columns: ':not(:first-child)',
}],
scrollX: true,
scrollCollapse: true,
sort: false,
fixedColumns: true,
})
table.page.len( PAGE_LENGTH ).draw()
this.$watch('currentTime', () => {
this.$emit('updateCountdown')
})
$('#grid').on('page.dt', () => {
this.$nextTick(() => {
this.$emit('updateCountdown')
})
})
this.$on('updateCountdown', () => {
$('.headcol .countdown').hide()
$(`.countdown-${this.currentTime}`).show()
})
},
vuex: {
getters: {
dev: state => state.dev,
schools: state => state.schools,
schedule: state => state.schedule,
scheduleGuests: state => state.scheduleGuests,
mapScheduleSlots: state => state.mapScheduleSlots
}
},
computed: {
countdownData() {
return this.$root.countdownData
},
currentTime() {
return this.$root.currentTime
}
},
watch: {
'countdownData': {
handler: (data) => {
if (data.totalSeconds == 0) {
$('.countdown:visible').hide()
}
$('.countdown:visible').text(data.stringValue)
.toggleClass('danger', data.minutes < ALARM_MINUTES_LEFT)
},
deep: true
}
},
methods: {
checkClosedSlot(schoolId, slotId) {
return !!Object.values(this.schools[schoolId].closed121).find(id => id == slotId)
}
},
components: {
scheduleGuest,
scheduleSlot,
}
}
</script>
<style lang="stylus">
.DTFC_LeftBodyWrapper table
background-color: white
.DTFC_LeftBodyLiner,
.DTFC_RightBodyLiner
overflow-y: hidden !important
#grid_paginate
margin-top: 1em
table
.card
min-width: 20rem
min-height: 6rem
.card-block
padding: .5rem
.schedule-time
border: 1px solid rgba(0,0,0,.125)
.schedule-slot
min-width: 20rem
border-radius: .25rem
padding: 0.5rem
.countdown.danger
color: #d65757
</style>
<template lang="pug">
.guest(v-if='guest', transition='fade')
.d-block
.d-inline-block.w-100
schedule-candidate(class='pull-xs-left', :candidate='candidate', :state='guest.state')
.pull-xs-left.pull-sm-right.m-l-1
.dropdown(v-if='permissions.has("candidate.move")')
button.btn.btn-secondary.btn-sm.dropdown-toggle(type='button', data-toggle='dropdown', aria-haspopup='true', aria-expanded='false')
| Edit
.dropdown-menu.dropdown-menu-right
a.dropdown-item(href='#', @click="editCandidate") Edit profile
a.dropdown-item(href='#', @click="moveCandidate") Move time
a.dropdown-item(href='#', @click="moveToReserve") Move to reserve
a.dropdown-item(href='#', @click="patchGuest(guest.id, {state: 'planned'})") Reset state
div(v-else)
button.btn.btn-secondary.btn-sm(type='button', v-if='permissions.has("candidate.edit")', @click="editCandidate") Edit
.row
form.col-xs-12(transition='fade')
.btn-group.btn-group-sm.pull-xs-left(data-toggle='buttons', v-show='current || role == "organizer"')
label.btn.btn-outline-success(
@click='patchGuest(guest.id, {state: "present"})',
:class='{active: guest.state == "present"}'
)
input(type='radio', name='_state', value='present', v-model='guest.state')
| Interview
label.btn.btn-outline-warning(
@click='patchGuest(guest.id, {state: "absent"})',
:class='{active: guest.state == "absent"}'
)
input(type='radio', name='_state', value='absent', v-model='guest.state')
| Absent
|&nbsp;
.btn-group.btn-group-sm.w-100(
data-toggle='buttons',
v-show='!current && (guest.state == "present" || role == "organizer")'
)
label.btn.btn-outline-secondary(v-on:click='onlyComments = !onlyComments')
input(type='checkbox', name='_state', value='true', v-model='onlyComments')
| Notes
|&nbsp;
fieldset.w-100( :class="{ 'hidden-xs-up': !canShowComments }")
.form-group
textarea.form-control(
rows='3',
v-model='comments', debounce='500'
) {{ guest.comments }}
button.btn.btn-sm.btn-outline-primary(type='button', v-on:click='saveComments') Save notes
</template>
<script>
import scheduleCandidate from '../components/schedule-candidate'
import {
patchGuest,
editCandidateProfile,
movableCandidateData,
updateMapGuestSlot,
updateMapGuestSchool,
addReserve
} from '../vuex/actions'
export default {
props: {
guestId: Number,
current: {
type: Boolean,
default: false
}
},
data() {
return {
comments: '',
onlyComments: false
}
},
vuex: {
getters: {
view: state => state.view,
guests: state => state.guests,
candidates: state => state.candidates
},
actions: {
patchGuest,
editCandidateProfile,
movableCandidateData,
updateMapGuestSlot,
updateMapGuestSchool,
addReserve
}
},
computed: {
guest() {
return this.guests[this.guestId]
},
candidate() {
return this.guest ? this.candidates[this.guest.candidate_id] : null
},
canShowComments() {
if (!this.guest) return false
return (this.guest.state == 'present' || this.role == 'organizer') && (this.current || this.onlyComments)
}
},
methods: {
saveComments() {
if (this.guest.comments == this.comments) return
this.patchGuest(this.guestId, { comments: this.comments })
},
changeCanShowComments() {
this.canShowComments = true
},
editCandidate(e) {
e.preventDefault()
this.editCandidateProfile(this.guest.candidate_id)
},
moveCandidate(e) {
e.preventDefault()
this.movableCandidateData({
candidateId : this.guest.candidate_id,
guestId : this.guest.id,
schoolId : this.guest.school_id,
scheduleId : this.guest.schedule_id,
})
},
moveToReserve(e) {
e.preventDefault()
this.patchGuest(this.guestId, {
schedule_id : null,
schoolstatus : 2,
seminarstatus : 0
}).then(() => {
this.updateMapGuestSlot(this.guestId, null)
this.updateMapGuestSchool(this.guestId, this.guest.school_id)
this.addReserve(this.guest.school_id, {
candidate_id : this.guest.candidate_id,
guest_id : this.guestId,
school_id : this.guest.school_id,
})
})
}
},
components: {
scheduleCandidate
}
}
</script>
<style lang="stylus" scoped>
.dropdown-item
width: inherit
@media (min-width: 544px)
.form-inline .form-group
display: block
margin-bottom: .5rem
</style>
<template lang="pug">
countdown( :data='countdownData', :candidates='currentCandidates')
#schedule.row
.col-xs-12(v-for='time in schedule', :class="{'current-time': currentTime == time.key}", )
.schedule-time
h4
span.time.tag.tag-default {{ time.start }}
| &nbsp;
span.tag.tag-default(v-if='closedTimes.has(time.key)') CLOSED
.row(v-if='!closedTimes.has(time.key)')
.col-xs-12.col-sm-12.col-md-6(v-for='slotId in mapScheduleSlots[time.key]')
schedule-slot( :slot-id='slotId', :current='currentTime == time.key', :school-id='schoolId')
</template>
<script>
import scheduleSlot from '../components/schedule-slot'
import countdown from '../components/countdown'
import kountdown from 'kountdown'
export default {
vuex: {
getters: {
schoolId: state => state.schoolId,
schedule: state => state.schedule,
guests: state => state.guests,
candidates: state => state.candidates,
mapScheduleSlots: state => state.mapScheduleSlots,
closed121: state => (state.schools[state.schoolId] || {}).closed121 || []
}
},
ready() {
document.querySelector('body').focus()
},
computed: {
countdownData() {
return this.$root.countdownData
},
closedTimes() {
return new Set(Object.keys(this.schedule)
.filter(time => {
let slotId = (this.mapScheduleSlots[time][0] || null)
return this.closed121.find(id => id == slotId)
}))
},
currentCandidates() {
return this.$root.currentGuests
.filter(guestId => this.guests[guestId].state == 'present')
.map(guestId => this.guests[guestId].candidate_id)
.map(candidateId => {
let candidate = this.candidates[candidateId]
return `${candidate.firstname} ${candidate.surname}`
})
},
currentTime() {
return this.$root.currentTime
}
},
components: {
scheduleSlot,
countdown
}
}
</script>
<style lang="stylus">
.schedule-time
padding: .5rem
margin-bottom: .75rem
border: 1px solid rgba(0, 0, 0, 0.125)
border-radius: .25rem
.current-time
.schedule-time
border-color: #5cb85c
</style>
<template lang="pug">
nav.navbar.navbar-light.bg-faded
h1.navbar-brand {{ meeting.name }}
ul.nav.navbar-nav.pull-xs-right(v-if='permissions.has("nav")')
li.nav-item(v-bind:class="[ !schoolId ? 'active' : '' ]")
a.nav-link(href='#', @click='goFullTable') Full Table
li.nav-item(v-bind:class="[ schoolId ? 'active' : '' ]")
a.nav-link(href='#', v-bind:class="[ active == 'list' ? active : '' ]") By School
li.nav-item
form.form-inline
multiselect(
:options="schools",
:selected="selectedSchool",
:searchable="true",
:close-on-select="true",
:allow-empty="false",
@update="selectSchool",
deselect-label=""
key="id"
label="name"
placeholder="Select school"
)
</template>
<script>
import Multiselect from 'vue-multiselect'
import { updateCurrentSchool, changeView } from '../vuex/actions'
export default {
vuex: {
getters: {
meeting: state => state.meeting,
schoolId: state => state.schoolId,
selectedSchool: state => state.schools[state.schoolId],
schools: state => Object.values(state.schools),
},
actions: {
updateCurrentSchool,
changeView
}
},
ready() {
if (this.$root.user.role == 'school' && !this.schoolId) {
this.$nextTick(() => this.selectSchool(this.schools[0]))
}
},
methods: {
selectSchool(school) {
this.updateCurrentSchool(school.id)
this.changeView('list')
setTimeout(() => {
this.$route.router.go({path: `/schedule/${this.meeting.id}/${school.id}`})
}, 100)
},
goFullTable() {
this.$route.router.go({path: `/schedule/${this.meeting.id}`})
this.updateCurrentSchool(null)
this.changeView('grid')
}
},
components: {
Multiselect
}
}
</script>
<style lang="stylus" scoped>
.navbar
margin-bottom: 1rem
.multiselect
min-width: 20rem
</style>
<template lang="pug">
div.m-b-1(v-if='dev')
span.tag.tag-primary slot:{{ slotId }}
| &nbsp;
span.tag.tag-info(v-if='guestId') guest:{{ guestId }}
| &nbsp;
span.tag.tag-danger(v-if='closed') CLOSED
div.m-b-1(v-if='!closed && guestId')
schedule-guest( :guest-id='guestId', :current='current' )
</template>
<script>
import scheduleGuest from '../components/schedule-guest'
export default {
props: [ 'slotId', 'current', 'schoolId' ],
vuex: {
getters: {
dev: state => state.dev,
schools: state => state.schools,
mapGuestSlot: state => state.mapGuestSlot,
mapGuestSchool: state => state.mapGuestSchool,
}
},
computed: {
guestId() {
// console.log('computed guestId', this.mapGuestSlot)
return +Object.entries(this.mapGuestSlot)
.filter(([guestId, slotId]) => {
return this.mapGuestSchool[guestId] == this.schoolId &&
slotId == this.slotId
})
.map(([guestId]) => guestId)
.pop()
},
closed() {
return !!Object.values((this.schools[this.schoolId] || {}).closed121 || []).find(id => id == this.slotId)
}
},
ready() {
},
watch: {
mapGuestSlot: {
handler: function(newData, oldData) {
console.log('watch mapGuestSlot', this.slotId, newData, oldData)
},
deep: true
}
},
components: {
scheduleGuest,
}
}
</script>
<style lang="stylus" scoped>
.card
transition: border-color 500ms linear
.card-block
padding: 1rem
.guest + .guest
border-left: 1px solid #d2d2d4
</style>
<template lang="pug">
legend System log
.row.m-b-1
.form-group
label.control-label.col-sm-2 Show:
.col-sm-8
.col-xs-12.col-sm-3.col-md-4(v-for='(type, data) in filter.types')
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox', v-model='filter.types[type].show')
span.custom-control-indicator
span.custom-control-description {{ data.title }}
.row
.col-xs-12.col-sm-4.col-md-4
.form-group
label.control-label Filter by user:
multiselect(
:options='users',
:searchable='true',
:close-on-select='true',
:allow-empty='false',
@search-change='asyncFindUsers',
@update='selectUser',
deselect-label=''
key='id'
label='username'
placeholder='All users'
)
.col-xs-12.col-sm-8.col-md-8
label.control-label Filter by period:
.form-inline
input.form-control(type='date', v-model='filter.dateFrom')
| &nbsp;&minus;&nbsp;
input.form-control(type='date', v-model='filter.dateTo')
button.btn.btn-outline-success(@click='updateSystemLogs') Refresh preview
a(href='data:text/plain;charset=utf-8,{{ encodeURIComponent(logsPreview) }}', download='logs.txt').btn.btn-outline-primary.pull-xs-right Export to TXT
.row.m-t-2
.col-xs-12
textarea#system-log-content.form-control(style='height: 500px; margin-top: 20px;')
| {{ logsPreview }}
</template>
<script>
import Multiselect from 'vue-multiselect'
import { SystemLogResource } from '../resources'
import {
getSystemLogs,
getSystemLogUsers
} from '../vuex/actions'
export default {
data() {
return {
filter: {
user: {},
dateFrom: null,
dateTo: null,
types: {
login: {
show: true,
title: 'Login'
},
logout: {
show: true,
title: 'Logout'
},
export: {
show: true,
title: 'Export'
},
change_state: {
show: true,
title: 'Change state'
},
write_comments: {
show: true,
title: 'Write comments'
},
move: {
show: true,
title: 'Move other slot'
},
move_to_reserve: {
show: true,
title: 'Move to reserve'
},
move_from_reserve: {
show: true,
title: 'Move from reserve'
},
change_applicant: {
show: true,
title: 'Change applicant'
},
},
},
}
},
vuex: {
getters: {
logs: state => state.systemLog.logs,
users: state => state.systemLog.users
},
actions: {
getSystemLogUsers,
getSystemLogs,
}
},
created() {
// this.getSystemLogs()
this.getSystemLogUsers()
},
computed: {
logsPreview() {
return this.logs.map(log => {
log.data = JSON.parse(log.data)
if (!log.data.role) return null
let user = `${log.data.role} «${log.data.username}»`
let action = this.filter.types[log.type].title.toLowerCase()
let desc = ''
switch (log.type) {
case 'export':
desc = `"${log.data.filename}"`
break
case 'change_state':
switch (log.data.state) {
case 'planned':
action = 'reset state for'
log.data.state = ''
break
case 'present':
action = 'started interview with'
log.data.state = ''
break
case 'absent':
action = 'marked'
log.data.state = 'as absent'
break
}
desc = ${log.data.candidate.name}» ${log.data.state}`
break
case 'write_comments':
desc = `for «${log.data.candidate.name}»: new "${log.data.new_comments}", old "${log.data.old_comments}"`
break
case 'move':
desc = ${log.data.candidate.name}» from "${log.data.old.school.name + ' - ' + log.data.old.schedule.time}" to "${log.data.new.school.name + ' - ' + log.data.new.schedule.time}"`
break
case 'move_to_reserve':
desc = ${log.data.candidate.name}» from "${log.data.old.school.name + ' - ' + log.data.old.schedule.time}"`
break
case 'move_from_reserve':
desc = ${log.data.candidate.name}» to "${log.data.new.school.name + ' - ' + log.data.new.schedule.time}"`
break
case 'change_applicant':
desc = ${log.data.candidate.name}»`
break
default:
break
}
let text = `${log.datetime} - ${user} ${action} ${desc}`
return text
return `${log.datetime} - ${log.text ? `"${log.text}"` : ''}`
}).filter(x => x).join('\n')
}
},
methods: {
selectUser(user) {
this.filter.user = user
this.updateSystemLogs()
},
asyncFindUsers(query) {
this.getSystemLogUsers(query)
},
updateSystemLogs() {
this.getSystemLogs({
userId: this.filter.user.id,
dateFrom: this.filter.dateFrom,
dateTo: this.filter.dateTo,
types: Object.entries(this.filter.types)
.filter(([type, data]) => data.show)
.map(([type]) => type)
})
}
},
watch: {
'filter': {
handler: function() {
this.updateSystemLogs()
},
deep: true
},
'logsPreview': {
handler: function(logs) {
}
}
},
components: {
Multiselect
}
}
</script>
<template lang="pug">
nav.navbar.navbar-fixed-top.navbar-dark.bg-inverse
.container
a.navbar-brand(v-link='{ name: "index" }') MBA25 1-2-1
ul.nav.navbar-nav(v-if="role == 'organizer'")
li.nav-item
a.nav-link(v-link='{ name: "index", exact: true }')
| Meetings
li.nav-item
a.nav-link(v-link='{ name: "export" }')
| Export
li.nav-item
a.nav-link(v-link='{ name: "system-log" }')
| System log
ul.nav.navbar-nav.pull-xs-right
li.nav-item.nav-link
| {{ username }}
li.nav-item
form(action='/onetoone/user', method='POST')
input(type='hidden', name='action', value='logout')
button.btn.btn-link.nav-link(type='submit') Log out
</template>
<script>
export default {
vuex: {
getters: {
meetingId: state => state.meeting.id,
}
},
computed: {
username() {
return this.$root.user.username
}
}
}
</script>
export default () => {
$.fn.DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) {
var api = new $.fn.DataTable.Api( settings );
var classes = settings.oClasses;
var lang = settings.oLanguage.oPaginate;
var aria = settings.oLanguage.oAria.paginate || {};
var btnDisplay, btnClass, counter=0;
var attach = function( container, buttons ) {
var i, ien, node, button;
var clickHandler = function ( e ) {
e.preventDefault();
if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) {
api.page( e.data.action ).draw( 'page' );
}
};
for ( i=0, ien=buttons.length ; i<ien ; i++ ) {
button = buttons[i];
if ( $.isArray( button ) ) {
attach( container, button );
}
else {
btnDisplay = button;
btnClass = page === counter ?
'active' : '';
if ( btnDisplay ) {
node = $('<li>', {
'class': classes.sPageButton+' '+btnClass,
'id': null
} )
.append( $('<a>', {
'href': '#',
'aria-controls': settings.sTableId,
'aria-label': aria[ button ],
'data-dt-idx': counter,
'tabindex': settings.iTabIndex,
'class': 'page-link'
} )
.html( btnDisplay )
)
.appendTo( container );
settings.oApi._fnBindAction(
node, {action: counter}, clickHandler
);
counter++;
}
}
}
};
// IE9 throws an 'unknown error' if document.activeElement is used
// inside an iframe or frame.
var activeEl;
try {
// Because this approach is destroying and recreating the paging
// elements, focus is lost on the select button which is bad for
// accessibility. So we want to restore focus once the draw has
// completed
activeEl = $(host).find(document.activeElement).data('dt-idx');
}
catch (e) {}
attach(
$(host).empty().html('<ul class="pagination"/>').children('ul'),
buttons
);
if ( activeEl ) {
$(host).find( '[data-dt-idx='+activeEl+']' ).focus();
}
};
}
import Vue from 'vue'
import VueRouter from 'vue-router'
import app from './app'
import routes from './routes'
import store from './vuex/store'
import {
bootstrap,
} from './vuex/actions'
Vue.use(VueRouter)
Vue.transition('fade', {
css: false,
enter: function (el, done) {
$(el)
.css('opacity', 0)
.animate({ opacity: 1 }, 500, done)
},
enterCancelled: function (el) {
$(el).stop()
},
leave: function (el, done) {
$(el).animate({ opacity: 0 }, 500, done)
},
leaveCancelled: function (el) {
$(el).stop()
}
})
Vue.mixin({
computed: {
permissions() {
return this.$root.user.permissions
},
role() {
return this.$root.user.role
}
}
})
Vue.http.interceptors.push((request, next) => {
next((res) => {
// session timeout
if (res.status == 403) {
window.location = '/'
}
})
})
const router = new VueRouter({
hashbang: false,
history: true,
mode: 'html5',
root: '/',
linkActiveClass: 'active'
})
router.mode = 'html5'
router.map(routes)
document.addEventListener('DOMContentLoaded', () => {
if (window.bootstrapData) {
bootstrap(store, window.bootstrapData)
}
router.start(app, '#app')
})
import Vue from 'vue'
import VueResource from 'vue-resource'
Vue.use(VueResource)
export const GuestResource = Vue.resource('/api/guests{/id}.json')
export const CandidateResource = Vue.resource('/api/candidates{/id}.json')
export const SyncResource = Vue.resource('/onetoone/schedule{/meetingId}{/schoolId}.json')
export const ScheduleResource = Vue.resource('/onetoone/schedule{/meetingId}{/schoolId}.json')
export const ExportResource = Vue.resource('/onetoone/export{/meetingId}{/schoolId}.json')
export const SystemLogResource = Vue.resource('/onetoone/systemlog{/action}.json')
export const UsersResource = Vue.resource('/onetoone/users.json')
export default {
'/schedule/:meetingId': {
name: 'schedule-grid',
component: require('./views/grid')
},
'/schedule/:meetingId/:schoolId': {
name: 'schedule-list',
component: require('./views/list')
},
'/export': {
name: 'export',
component: require('./views/export')
},
'/system-log': {
name: 'system-log',
component: require('./views/system-log')
},
'/odmin': {
name: 'odmin',
component: require('./views/odmin')
},
'/': {
name: 'index',
component: require('./views/index'),
},
'*': {
component: require('./components/not-found')
},
}
<template lang="pug">
.container
export-applicants
</template>
<script>
import exportApplicants from '../components/export-applicants'
export default {
components: {
exportApplicants
}
}
</script>
<template lang="pug">
.container
schedule-nav
.container-fluid
schedule-grid
</template>
<script>
import scheduleNav from '../components/schedule-nav'
import scheduleGrid from '../components/schedule-grid'
export default {
components: {
scheduleNav,
scheduleGrid
}
}
</script>
<template lang="pug">
.container
meetings
</template>
<script>
import meetings from '../components/meetings'
export default {
components: {
meetings
}
}
</script>
<template lang="pug">
.container
schedule-nav
.row
div( :class='permissions.has("reserve.view") ? "col-xs-8" : "col-xs-12"')
schedule-list
.col-xs-4(v-if='permissions.has("reserve.view")')
reserve-list
</template>
<script>
import scheduleNav from '../components/schedule-nav'
import scheduleList from '../components/schedule-list'
import reserveList from '../components/reserve-list'
export default {
components: {
scheduleNav,
scheduleList,
reserveList
}
}
</script>
<template lang="pug">
.container
h1 Hello Одминчег
div.m-b-1(v-if='role == "organizer"')
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox', v-model='enabledDev')
span.custom-control-indicator
span.custom-control-description {{ enabledDev == true ? 'Вымкнуть' : 'Умкнуть' }} Dev Mode
br
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox', v-model='showCountdown', :change='enabledCountdown')
span.custom-control-indicator
span.custom-control-description {{ showCountdown == true ? 'Вымкнуть' : 'Умкнуть' }} Timer {{ countdownData | json }}
countdown( :data='countdownData', :candidates='currentCandidates')
</template>
<script>
import { setDev } from '../vuex/actions'
import countdown from '../components/countdown'
import KountdownLib from 'kountdown'
export default {
data() {
return {
kountdown: {},
countdownData: {
totalSeconds: 0
},
showCountdown: false
}
},
vuex: {
getters: {
dev: state => state.dev,
},
actions: {
setDev
}
},
computed: {
kountdown() {
return new KountdownLib({
endDate: new Date(),
stringFormatter: 'mm:ss',
onUpdate: (data) => {
this.countdownData = data
}
})
},
enabledDev: {
get() {
return this.dev
},
set(newValue) {
this.setDev(newValue)
}
},
currentCandidates() {
return [
'Одминчег #1',
'Одминчег #2'
]
}
},
ready() {
trackJs.track('odmin ready')
/*this.kountdown = new KountdownLib({
endDate: new Date(),
stringFormatter: 'mm:ss',
onUpdate: (data) => {
console.log(data)
this.countdownData = data
}
})*/
this.$watch('showCountdown', (show) => {
if (show == true) {
this.kountdown
.setEndDate(new Date().getTime() / 1000 + (5 * 60 + 30))
.start()
} else {
this.kountdown.setEndDate(new Date())
}
})
},
components: {
countdown
}
}
</script>
<template lang="pug">
.container
system-log
</template>
<script>
import systemLog from '../components/system-log'
export default {
components: {
systemLog
}
}
</script>
import * as types from './mutation-types'
import {
ScheduleResource,
GuestResource,
CandidateResource,
ExportResource,
SystemLogResource
} from '../resources'
export const bootstrap = ({dispatch}, data) => {
if (data.school_id) {
dispatch(types.VIEW, ['grid', 'list'][+!!data.school_id])
}
dispatch(types.SCHOOL_ID , data.school_id)
dispatch(types.MEETING , data.meeting)
dispatch(types.MEETINGS , data.meetings)
dispatch(types.SCHOOLS , data.schools)
dispatch(types.SCHEDULE , data.schedule)
dispatch(types.SCHEDULE_SLOTS , data.scheduleSlots)
dispatch(types.SCHEDULE_GUESTS , data.schedule_guests)
dispatch(types.CANDIDATES , data.candidates)
dispatch(types.GUESTS , data.guests)
dispatch(types.RESERVE , data.reserve)
dispatch(types.COUNTRIES , data.countries)
dispatch(types.MAP_SCHEDULE_SLOTS , data.mapScheduleSlots)
dispatch(types.MAP_GUEST_SLOT , data.mapGuestSlot)
dispatch(types.MAP_GUEST_SCHOOL , data.mapGuestSchool)
}
export const setServerSync = ({ dispatch }, val) => {
dispatch(types.SERVER_SYNC, val)
}
export const setDev = ({ dispatch }, val) => {
localStorage.dev = !!val
dispatch(types.DEV, !!val)
}
export const setMeeting = ({ dispatch }, meeting) => {
// ScheduleResource.get({ meeting.id })
// .then(() => {
//
// })
dispatch(types.MEETING, meeting)
}
function update(dispatch, type, oldData, newData, opts = {}) {
let ignore = opts.ignore || []
for (let [id, data] of Object.entries(newData)) {
if (ignore.find(i => i == id)) {
console.log('update ignore', type, data)
continue
}
for (let [key, val] of Object.entries(data)) {
if (!oldData[id]) {
dispatch(type, id, data)
console.log('update not found old', type, id)
continue
}
let prev = oldData[id][key],
next = val
if (prev !== null && typeof(prev) == 'object') {
prev = (Object.values(prev)).join()
}
if (next !== null && typeof(next) == 'object') {
next = (Object.values(next)).join()
}
if (prev != next) {
dispatch(type, id, data)
continue
}
}
}
}
function mapSync(dispatch, type, oldData, newData) {
for (let [id, data] of Object.entries(oldData)) {
if (id && data && !newData[id]) {
console.log('mapSync rm ', id, data)
dispatch(type, id, null)
}
}
for (let [id, data] of Object.entries(newData)) {
if (!oldData[id]) {
console.log('ADD ', type, data);
dispatch(type, id, data)
continue
}
let prev = oldData[id],
next = data
if (prev != null && typeof(prev) == 'object') {
prev = prev.join()
}
if (next != null && typeof(next) == 'object') {
next = next.join()
}
if (prev != next) {
console.log('UPDATE ', type, prev, next);
dispatch(type, id, data)
}
}
}
export const syncMapScheduleSlots = ({ dispatch }, oldData, newData) => {
mapSync(dispatch, types.UPDATE_MAP_SCHEDULE_SLOTS, oldData, newData)
}
export const syncMapGuestSlot = ({ dispatch }, oldData, newData) => {
mapSync(dispatch, types.UPDATE_MAP_GUEST_SLOT, oldData, newData)
}
export const updateMapGuestSlot = ({ dispatch }, id, data) => {
dispatch(types.UPDATE_MAP_GUEST_SLOT, id, data)
}
export const syncMapGuestSchool = ({ dispatch }, oldData, newData) => {
mapSync(dispatch, types.UPDATE_MAP_GUEST_SCHOOL, oldData, newData)
}
export const updateMapGuestSchool = ({ dispatch }, id, data) => {
dispatch(types.UPDATE_MAP_GUEST_SCHOOL, id, data)
}
export const updateSchools = ({ dispatch }, oldData, newData) => {
if (!Object.keys(oldData).length) return dispatch(types.SCHOOLS, newData)
update(dispatch, types.UPDATE_SCHOOL, oldData, newData)
}
export const updateGuests = ({ dispatch }, oldData, newData) => {
if (!Object.keys(oldData).length && Object.keys(newData).length) return dispatch(types.GUESTS, newData)
update(dispatch, types.UPDATE_GUEST, oldData, newData)
}
export const addReserve = ({ dispatch }, schoolId, data) => {
dispatch(types.ADD_RESERVE, schoolId, data)
}
export const updateReserve = ({ dispatch }, oldData, newData) => {
// if (JSON.stringify(oldData) != JSON.stringify(newData))
if (Object.keys(newData).length != Object.keys(oldData).length) {
console.log('updateReserve', 'all update', newData, Object.keys(newData), Object.keys(oldData))
return dispatch(types.RESERVE, !Object.keys(newData).length ? {} : newData)
}
// remove data
for (let [schoolId, oldReserves] of Object.entries(oldData)) {
if (schoolId && !newData[schoolId]) {
console.log('updateReserve', 'clear schools reverses', schoolId)
dispatch(types.UPDATE_RESERVE, schoolId, [])
continue
}
// for (let oldReserve of oldReserves) {
// if (newData[schoolId])
// }
}
for (let [schoolId, newReserves] of Object.entries(newData)) {
if (!oldData[schoolId] || (oldData[schoolId].length != newReserves.length)) {
console.log('updateReserve', 'update schools reverses', schoolId)
dispatch(types.UPDATE_RESERVE, schoolId, newData[schoolId])
continue
}
}
// console.info(oldData, oldData.length)
/*if (Object.keys(oldData).length && Object.keys(newData).length) {
for (let [schoolId, oldReserves] of Object.entries(oldData)) {
if ((undefined !== oldReserves && oldReserves.length) != newData[schoolId].length) {
dispatch(types.UPDATE_RESERVE, schoolId, newData[schoolId])
continue
}
}
} else {
dispatch(types.RESERVE, newData)
}*/
}
export const deleteReserve = ({ dispatch }, schoolId, reserve) => {
dispatch(types.DELETE_RESERVE, schoolId, reserve)
}
export const updateCandidates = ({ dispatch }, oldData, newData, editableCandidateId = null) => {
if (!Object.keys(oldData).length && Object.keys(newData).length) return dispatch(types.CANDIDATES, newData)
update(dispatch, types.UPDATE_CANDIDATE, oldData, newData, {
ignore: [editableCandidateId]
})
}
export const getCandidateProfile = ({ dispatch }, id) => {
return CandidateResource.get({ id })
.then(res => {
dispatch(types.UPDATE_CANDIDATE, id, res.data)
})
}
export const editCandidateProfile = ({ dispatch }, id) => {
dispatch(types.EDITABLE_CANDIDATE_ID, id)
}
export const movableCandidateData = ({ dispatch }, data) => {
dispatch(types.MOVABLE_CANDIDATE_DATA, data)
}
export const saveCandidateProfile = ({ dispatch }, id, data) => {
if (!id) {
return CandidateResource.save({ 'Candidate': data })
.then(res => {
dispatch(types.UPDATE_CANDIDATE, res.data.candidate.id, res.data.candidate)
return res.data
})
}
return CandidateResource.update({ id }, data)
.then(res => {
dispatch(types.UPDATE_CANDIDATE, id, data)
})
}
export const updateGuest = ({ dispatch }, id, data) => {
dispatch(types.UPDATE_GUEST, id, data)
}
export const updateScheduleGuests = ({ dispatch }, oldData, newData, schedule) => {
if (!Object.keys(oldData).length) {
return dispatch(types.SCHEDULE_GUESTS , newData)
}
for (let [schoolId, newItem] of Object.entries(newData)) {
for (let slot of Object.values(schedule)) {
let oldGuests = (oldData[schoolId] || [])[slot.key] || []
let newGuests = newItem[slot.key] || []
if (newGuests.join() != oldGuests.join()) {
dispatch(types.SCHEDULE_GUEST, schoolId, slot.key, newGuests)
}
}
}
}
export const updateSchedule = ({ dispatch }, schedule) => {
dispatch(types.SCHEDULE, schedule)
}
export const updateCurrentSchool = ({dispatch}, schoolId) => {
dispatch(types.SCHOOL_ID, schoolId)
}
export const changeView = ({ dispatch }, view) => {
dispatch(types.VIEW, view)
}
export const addGuest = ({dispatch}, data) => {
return GuestResource.save({ 'Guest': data })
.then(res => {
console.log('dispatch guest', res)
dispatch(types.UPDATE_GUEST, res.data.guest.id, res.data.guest)
return res.data
})
}
export const patchGuest = ({dispatch}, id, data) => {
return GuestResource.update({ id }, data)
.then(res => {
dispatch(types.PATCH_GUEST, id, data)
})
}
export const getDataForExport = ({ dispatch }, data) => {
ExportResource.get()
.then(res => {
dispatch(types.DATA_FOR_EXPORT, res.data)
})
}
export const getSystemLogs = ({ dispatch }, filter) => {
SystemLogResource.get({ filter: JSON.stringify(filter) })
.then(res => {
dispatch(types.SYSTEM_LOGS, res.data)
})
}
export const getSystemLogUsers = ({ dispatch }, query = '') => {
SystemLogResource.get({ action: 'users', query: query })
.then(res => {
dispatch(types.SYSTEM_LOG_USERS, res.data)
})
}
export const SERVER_SYNC = 'SERVER_SYNC'
export const DEV = 'DEV'
export const VIEW = 'VIEW'
export const SCHOOL_ID = 'SCHOOL_ID'
export const MEETING = 'MEETING'
export const MEETINGS = 'MEETINGS'
export const SCHOOLS = 'SCHOOLS'
export const UPDATE_SCHOOL = 'UPDATE_SCHOOL'
export const SCHEDULE = 'SCHEDULE'
export const SCHEDULE_SLOTS = 'SCHEDULE_SLOTS'
export const SCHEDULE_GUESTS = 'SCHEDULE_GUESTS'
export const SCHEDULE_GUEST = 'SCHEDULE_GUEST'
export const CANDIDATES = 'CANDIDATES'
export const UPDATE_CANDIDATE = 'UPDATE_CANDIDATE'
export const CANDIDATE_PROFILE = 'CANDIDATE_PROFILE'
export const EDITABLE_CANDIDATE_ID = 'EDITABLE_CANDIDATE_ID'
export const MOVABLE_CANDIDATE_DATA = 'MOVABLE_CANDIDATE_DATA'
export const GUESTS = 'GUESTS'
export const UPDATE_GUEST_STATE = 'UPDATE_GUEST_STATE'
export const UPDATE_GUEST = 'UPDATE_GUEST'
export const PATCH_GUEST = 'PATCH_GUEST'
export const RESERVE = 'RESERVE'
export const ADD_RESERVE = 'ADD_RESERVE'
export const UPDATE_RESERVE = 'UPDATE_RESERVE'
export const DELETE_RESERVE = 'DELETE_RESERVE'
export const COUNTRIES = 'COUNTRIES'
export const MAP_SCHEDULE_SLOTS = 'MAP_SCHEDULE_SLOTS'
export const MAP_GUEST_SLOT = 'MAP_GUEST_SLOT'
export const MAP_GUEST_SCHOOL = 'MAP_GUEST_SCHOOL'
export const UPDATE_MAP_SCHEDULE_SLOTS = 'UPDATE_MAP_SCHEDULE_SLOTS'
export const UPDATE_MAP_GUEST_SLOT = 'UPDATE_MAP_GUEST_SLOT'
export const UPDATE_MAP_GUEST_SCHOOL = 'UPDATE_MAP_GUEST_SCHOOL'
export const DATA_FOR_EXPORT = 'DATA_FOR_EXPORT'
export const SYSTEM_LOGS = 'SYSTEM_LOGS'
export const SYSTEM_LOG_USERS = 'SYSTEM_LOG_USERS'
import * as types from './mutation-types'
export default {
[types.SERVER_SYNC](state, val) {
state.serverSync = val
},
[types.DEV](state, val) {
state.dev = val
},
[types.VIEW](state, view) {
state.view = view
},
[types.SCHOOLS](state, schools) {
if (Array.isArray(schools) && !schools.length) {
state.schools = {}
return
}
state.schools = schools
},
[types.UPDATE_SCHOOL](state, id, data) {
if (data) {
state.schools[id] = data
}
},
[types.SCHOOL_ID](state, schoolId) {
state.schoolId = schoolId
},
[types.MEETING](state, meeting) {
if (meeting) {
state.meeting = meeting
}
},
[types.MEETINGS](state, meetings) {
if (meetings) {
state.meetings = meetings
}
},
[types.SCHEDULE](state, schedule) {
if (Array.isArray(schedule) && !schedule.length) {
state.schedule = {}
return
}
state.schedule = schedule
},
[types.SCHEDULE_SLOTS](state, scheduleSlots) {
if (Array.isArray(scheduleSlots) && !scheduleSlots.length) {
state.scheduleSlots = {}
return
}
state.scheduleSlots = scheduleSlots
},
[types.SCHEDULE_GUESTS](state, scheduleGuests) {
if (Array.isArray(scheduleGuests) && !scheduleGuests.length) {
state.scheduleGuests = {}
return
}
state.scheduleGuests = scheduleGuests
},
[types.SCHEDULE_GUEST](state, schoolId, scheduleId, guests) {
if (schoolId && cheduleId && guests) {
state.scheduleGuests[schoolId][scheduleId] = guests
}
},
[types.CANDIDATES](state, candidates) {
if (Array.isArray(candidates) && !candidates.length) {
state.candidates = {}
return
}
state.candidates = candidates
},
[types.UPDATE_CANDIDATE](state, id, data) {
if (data.country_id) {
data.country = state.countries.find(c => c.id == data.country_id)
}
state.candidates[id] = data
},
[types.CANDIDATE_PROFILE](state, id, profile) {
state.candidates[id].profile = profile
},
[types.EDITABLE_CANDIDATE_ID](state, id) {
state.editableCandidateId = id
},
[types.MOVABLE_CANDIDATE_DATA](state, data) {
state.movableCandidateData = data
},
[types.GUESTS](state, guests) {
if (Array.isArray(guests) && !guests.length) {
state.guests = {}
return
}
state.guests = guests
},
[types.UPDATE_GUEST](state, id, data) {
state.guests[id] = data
},
[types.PATCH_GUEST](state, id, data) {
for (let [key, val] of Object.entries(data)) {
state.guests[id][key] = val
}
},
[types.RESERVE](state, data) {
if (Array.isArray(data) && !data.length) {
state.reserve = {}
return
}
state.reserve = data
},
[types.ADD_RESERVE](state, schoolId, data) {
if (!state.reserve[schoolId]) {
state.reserve[schoolId] = []
}
state.reserve[schoolId].push(data)
},
[types.UPDATE_RESERVE](state, schoolId, data) {
state.reserve[schoolId] = data
},
[types.DELETE_RESERVE](state, schoolId, reserve) {
state.reserve[schoolId].$remove(reserve)
},
[types.COUNTRIES](state, data) {
if (data) {
state.countries = data
}
},
[types.MAP_SCHEDULE_SLOTS](state, data) {
if (Array.isArray(data) && !data.length) {
state.mapScheduleSlots = {}
return
}
state.mapScheduleSlots = data
},
[types.MAP_GUEST_SLOT](state, data) {
if (Array.isArray(data) && !data.length) {
state.mapGuestSlot = {}
return
}
state.mapGuestSlot = data
},
[types.MAP_GUEST_SCHOOL](state, data) {
if (Array.isArray(data) && !data.length) {
state.mapGuestSchool = {}
return
}
state.mapGuestSchool = data
},
[types.UPDATE_MAP_SCHEDULE_SLOTS](state, id, data) {
if (data == 'delete') {
delete state.mapScheduleSlots[id]
} else {
state.mapScheduleSlots[id] = data
}
},
[types.UPDATE_MAP_GUEST_SLOT](state, id, data) {
if (data == 'delete') {
state.mapGuestSlot[id] = null
} else {
state.mapGuestSlot[id] = data
}
},
[types.UPDATE_MAP_GUEST_SCHOOL](state, id, data) {
if (data == 'delete') {
delete state.mapGuestSchool[id]
} else {
state.mapGuestSchool[id] = data
}
},
[types.DATA_FOR_EXPORT](state, data) {
state.dataForExport = data
},
[types.SYSTEM_LOGS](state, data) {
state.systemLog.logs = data
},
[types.SYSTEM_LOG_USERS](state, data) {
state.systemLog.users = data
},
}
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import createLogger from 'vuex/logger'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
serverSync : true,
dev : localStorage.dev == "true",
view : 'grid',
meetingId : null,
schoolId : null,
meetings : [],
schools : {},
schedule : {},
scheduleSlots : {},
scheduleGuests : {},
candidates : {},
guests : {},
reserve : {},
countries : [],
mapScheduleSlots : {},
mapGuestSlot : {},
mapGuestSchool : {},
editableCandidateId : null,
movableCandidateData : {},
dataForExport : {},
systemLog : {
logs: [],
users: [],
}
},
mutations,
plugins: process.env.NODE_ENV !== 'production'
? [createLogger()]
: []
})
var webpack = require('webpack')
var config = require('./webpack.config')
var path = require('path')
config.devtool = 'inline-eval-cheap-source-map'
config.plugins = [
new webpack.NoErrorsPlugin(),
new webpack.DefinePlugin({
'__DEV__': true,
'process.env': JSON.stringify('development')
}),
].concat(config.plugins)
module.exports = config
var webpack = require('webpack')
var path = require('path')
module.exports = {
entry: ['./src/main.js'],
output: {
path: path.join(process.cwd(), '../app/webroot/onetoone/'),
filename: 'bundle.js',
publicPath: '/'
},
resolve: {
extensions: ['', '.js', '.vue', '.css']
},
module: {
loaders: [
{
test: /\.js$/,
loaders: ['babel'],
exclude: [/node_modules/]
},
{
test: /\.vue$/,
loaders: ['vue']
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'url?limit=100&name=images/[hash].[ext]',
exclude: [/node_modules/]
}
]
},
vue: {
autoprefixer: false,
postcss:[
require('postcss-cssnext')()
]
},
plugins: [],
babel: {
presets: ['es2015', 'stage-0'],
plugins: ['transform-runtime']
}
}
var webpack = require('webpack')
var config = require('./webpack.config')
var path = require('path')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
// config.devtool = 'source-map'
config.output.filename = 'bundle.js'
config.output.publicPath = './assets/'
config.plugins = [
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.DefinePlugin({
'__DEV__': false,
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
new webpack.optimize.UglifyJsPlugin({
compressor: {
warnings: false
},
comments: false
}),
new ExtractTextPlugin('bundle.css'),
].concat(config.plugins)
config.vue.loaders = {
css: ExtractTextPlugin.extract(
'vue-style-loader',
'css-loader?sourceMap'
)
}
module.exports = config
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment