instantjob-recruiter-client
Version:
a set of tools for creating an instantjob recruiter react client
1,070 lines (1,034 loc) • 46.4 kB
JSX
import React, {Component} from 'react'
import {connect} from 'react-redux'
import classNames from 'classnames/bind'
import {
MdError, MdWarning, MdCheck, MdCheckBox, MdCheckBoxOutlineBlank, MdFileUpload,
MdKeyboardArrowLeft, MdKeyboardArrowRight,
} from 'react-icons/lib/md'
import Menu from './menu'
import Button from 'components/utils/button'
import UserTable from './user_table'
import FieldCategoriesSelector from './field_categories_selector'
import {
array_from_hash, array_from_set, set_from_array, human_plural, human_join, deep_merge, for_all,
create_hash, hash_key_with_value, hash_with_key, map_hash, array_contains, group_hash_by_value,
filter_hash,
} from '../common/utilities'
import request from '../common/request'
import store from '../common/store'
import {color} from '../common/constants'
import {
action_create_field, action_create_value,
} from '../common/fields'
import {action_invite_users, get_user_status, make_user_status} from '../common/users'
import {alert_info, alert_success} from '../actions/display'
import {update_users} from '../actions/users'
import {
update_user_comments, add_users_value, remove_users_value,
update_exclusive_entity_value, clear_exclusive_entity_value,
} from '../actions/fields'
import {combined_actions} from 'actions/base'
import {get_users} from '../selectors/users'
let cx = classNames.bind(require('../styles/upload_results.scss'))
const get_uploaded_users = (upload_users, props_users) => {
return map_hash(upload_users, (user) => ({
...user,
status: user.existing ? get_user_status(props_users[user.existing]) : make_user_status('new'),
}))
}
class UploadResultsStatic extends Component {
constructor(props) {
super(props)
this.state = {
active_field_type: 'summary',
active_field: null,
messages: {
summary: [],
invitations: [],
instant_job_field: {},
agency_field: {},
},
invitations: true,
importing: {
instant_job_field: {},
agency_field: {},
},
imported: {
instant_job_field: {},
agency_field: {},
},
statistics: 'old',
}
this.render_page = this.render_page.bind(this)
this.send_invitations = this.send_invitations.bind(this)
this.import_users = this.import_users.bind(this)
this.import_field = this.import_field.bind(this)
this.render_navigation = this.render_navigation.bind(this)
}
componentWillUnmount() {
this.mounted = false
}
componentDidMount() {
this.mounted = true
let summary = [], invitations = [], instant_job_field = {}, agency_field = {}
let {
users,
fields,
values,
instant_job_fields_present,
new_users_count,
old_users_count,
duplicate_users_count,
invalid_emails,
} = this.props.upload_results
if (new_users_count > 0) {
this.setState({statistics: 'new'})
}
let new_field_names = Object.keys(fields)
.map((id) => fields[id])
.filter((field) => !field.existing)
.map((field) => field.name)
let total_fields_count = Object.keys(fields).length + array_from_set(instant_job_fields_present).length
summary.new_fields = {
new_field_names,
total_fields_count,
component: MessageFieldsCount,
}
if (duplicate_users_count > 0) {
summary.duplicate_users = {
duplicate_users_count,
component: MessageDuplicateUsers,
}
}
if (invalid_emails.length > 0) {
summary.duplicate_users = {
invalid_emails,
component: MessageInvalidEmails,
}
}
if (new_users_count == 0 && old_users_count == 0) {
summary.empty_file = {
component: MessageEmptyFile,
}
}
if (new_users_count > 0) {
invitations.new_users = {
validated: false,
all_users: Object.keys(users).length,
new_users: new_users_count,
component: MessageInviteUsers,
onClick: () => this.setState(deep_merge(this.state, {
messages: {invitations: {new_users: {validated: !this.state.messages.invitations.new_users.validated}}}
})),
}
}
let absent_fields = array_from_hash(filter_hash(
group_hash_by_value(this.props.instant_job_fields),
(labels) => for_all(labels, (label) => !instant_job_fields_present[label])
)).map((labels) => labels[0])
if (absent_fields.length > 0) {
invitations.absent_fields = {
validated: false,
absent_fields,
component: MessageAbsentFields,
onClick: () => this.setState(deep_merge(this.state, {
messages: {invitations: {absent_fields: {validated: !this.state.messages.invitations.absent_fields.validated}}}
})),
}
}
array_from_set(instant_job_fields_present).forEach((field_label) => {
let field_name = this.props.instant_job_fields[field_label]
if (new_users_count > 0) {
instant_job_field[field_name] = deep_merge(instant_job_field[field_name], {new: {valid: {
component: MessageInstantJobFieldNew,
informational: true,
}}})
}
if (old_users_count > 0) {
let conflicting_users = array_from_hash(users).filter((user) => {
if (user.existing) {
return (user[field_name] || "") != (this.props.users[user.existing][field_name] || "")
} else {
return false
}
})
let conflicting_users_with_app_count = conflicting_users.filter((user) => this.props.users[user.existing].app_name).length
let conflicting_users_without_app_count = conflicting_users.length - conflicting_users_with_app_count
if (conflicting_users.length > 0) {
if (conflicting_users_without_app_count > 0) {
instant_job_field[field_name] = deep_merge(instant_job_field[field_name], {old: {conflict: {
component: MessageFieldConflict,
conflicting_users_count: conflicting_users_without_app_count,
selected: null,
exists_new_user: false,
on_select: (selected) => this.setState(deep_merge(this.state, {
messages: {instant_job_field: {[field_name]: {old: {conflict: {selected}}}}}
})),
}}})
}
if (conflicting_users_with_app_count > 0) {
instant_job_field[field_name] = deep_merge(instant_job_field[field_name], {old: {conflict_with_app: {
component: MessageInstantJobAppUserConflict,
conflicting_users_count: conflicting_users_with_app_count,
informational: true,
}}})
}
} else {
instant_job_field[field_name] = deep_merge(instant_job_field[field_name], {old: {identical: {
component: MessageFieldIdentical,
informational: true,
}}})
}
}
})
array_from_hash(fields).forEach((field) => {
if (field.existing) {
let conflicting_users_count, new_values_old_users
const field_values = array_from_set(field.values).map((id) => values[id])
switch (field.category) {
case 'non_exclusive':
if (new_users_count > 0) {
let new_values_new_users = field_values.filter((value) => !value.existing && value.created_by_new_users).map((value) => value.name)
if (new_values_new_users.length > 0) {
agency_field[field.id] = deep_merge(agency_field[field.id], {new: {new_values_new_users: {
component: MessageNewValuesNewUsers,
new_field_values: new_values_new_users,
new_users_count,
validated: false,
onClick: () => this.setState(deep_merge(this.state, {
messages: {agency_field: {[field.id]: {new: {new_values_new_users: {validated: !this.state.messages.agency_field[field.id].new.new_values_new_users.validated}}}}}
})),
}}})
} else {
agency_field[field.id] = deep_merge(agency_field[field.id], {new: {ready: {
component: MessageAgencyFieldReady,
validated: true,
}}})
}
}
conflicting_users_count = array_from_hash(users).filter((user) => (
user.existing && !for_all(
field_values,
(value) => !((!value.existing && value.entity_ids[user.id]) || value.mismatching_users[user.id])
)
)).length
new_values_old_users = field_values.filter((value) => value.created_by_old_users).map((value) => value.name)
break
case 'exclusive':
if (new_users_count > 0) {
let new_values_new_users = field_values.filter((value) => !value.existing && value.created_by_new_users).map((value) => value.name)
if (new_values_new_users.length > 0) {
agency_field[field.id] = deep_merge(agency_field[field.id], {new: {new_values_new_users: {
component: MessageNewValuesNewUsers,
new_field_values: new_values_new_users,
new_users_count,
validated: false,
onClick: () => this.setState(deep_merge(this.state, {
messages: {agency_field: {[field.id]: {new: {new_values_new_users: {validated: !this.state.messages.agency_field[field.id].new.new_values_new_users.validated}}}}}
})),
}}})
} else {
agency_field[field.id] = deep_merge(agency_field[field.id], {new: {ready: {
component: MessageAgencyFieldReady,
validated: true,
}}})
}
}
conflicting_users_count = array_from_hash(users)
.filter((user) => user.existing && field.mismatching_user_ids[user.id])
.length
new_values_old_users = field_values
.filter((value) => value.created_by_old_users)
.map((value) => value.name)
break
case "comment":
if (new_users_count > 0) {
agency_field[field.id] = deep_merge(agency_field[field.id], {new: {ready: {
component: MessageAgencyFieldReady,
validated: true,
}}})
}
conflicting_users_count = array_from_hash(users).filter((user) => field.conflicting_comments[user.id]).length
new_values_old_users = []
break
default:
}
if (old_users_count > 0) {
if (conflicting_users_count > 0) {
agency_field[field.id] = deep_merge(agency_field[field.id], {old: {conflict: {
component: MessageFieldConflict,
conflicting_users_count,
new_field_values: new_values_old_users,
selected: null,
exists_new_user: new_users_count > 0,
on_select: (selected) => this.setState(deep_merge(this.state, {
messages: {agency_field: {[field.id]: {old: {conflict: {selected}}}}}
})),
}}})
} else {
agency_field[field.id] = deep_merge(agency_field[field.id], {old: {identical: {
component: MessageFieldIdentical,
informational: true,
}}})
}
}
} else {
agency_field[field.id] = deep_merge(agency_field[field.id], {common: {new_field: {
component: MessageNewField,
selected: null,
on_select: (category) => {
this.props.upload_results.fields = {
...this.props.upload_results.fields,
[field.id]: {
...field,
category
}
}
this.setState(deep_merge(this.state, {
messages: {agency_field: {[field.id]: {common: {new_field: {selected: category}}}}}
}))
},
}}})
}
})
this.setState({messages: {summary, invitations, instant_job_field, agency_field}})
}
send_invitations() {
this.setState(deep_merge(this.state, {importing: {invitations: true}}))
let users = array_from_hash(this.props.upload_results.users).filter((user) => !user.existing)
action_invite_users(users)
.then(() => this.setState(deep_merge(this.state, {imported: {invitations: true}})))
}
import_users(field_name) {
let users = array_from_hash(this.props.upload_results.users)
.filter((user) => user.existing && !this.props.users[user.existing].app_name)
.filter((user) => this.props.users[user.existing][field_name] != user[field_name])
.map((user) => ({...this.props.users[user.existing], [field_name]: user[field_name]}))
store.dispatch(update_users(users.filter((user) => user.user_id).map((user) => ({...user, id: user.user_id}))))
return request.asynchronous(`users/agency_update`, {users: users.map(({first_name, last_name, email, postal_code, phone_number}) => ({first_name, last_name, email, postal_code, phone_number}))})
.then((users) => store.dispatch(update_users(users)))
}
import_field(imported_field_id, old_users_import_mode) {
let {
users,
fields,
values,
} = this.props.upload_results
let field = fields[imported_field_id]
let users_to_import = array_from_hash(users)
if (old_users_import_mode == 'ignore') {
users_to_import = users_to_import.filter((user) => !user.existing)
}
let existing_users = hash_with_key(array_from_hash(this.props.users), 'email')
users = map_hash(users, (user) => ({...user, existing: existing_users[user.email]}))
let should_import_user = set_from_array(users_to_import.map((user) => users[user.id].existing.id))
let imported_users = hash_with_key(array_from_hash(users), 'email')
return (new Promise((resolve, reject) => {
if (field.existing) {
resolve(field.existing)
} else {
action_create_field(field)
.then((id) => resolve(id))
}
})).then((field_id) => {
const actions = []
switch (field.category) {
case "non_exclusive":
return Promise.all(array_from_set(field.values).map((id) => {
let imported_value = values[id]
return new Promise((resolve, reject) => {
if (imported_value.existing) {
resolve(imported_value)
} else {
action_create_value(field_id, imported_value.name)
.then((id) => resolve({...imported_value, existing: id}))
}
})
}))
.then((imported_values) => request.asynchronous("users/update_values", {values: imported_values.map((imported_value) => {
let value = this.props.values[imported_value.existing]
let result = {
id: value.id,
added: array_from_set(imported_value.entity_ids)
.map((id) => users[id].existing.id)
.filter((user_id) => !value.entity_ids[user_id] && should_import_user[user_id]),
removed: array_from_set(value.entity_ids).filter((user_id) => {
let imported_user = imported_users[this.props.users[user_id].email]
return imported_user && !imported_value.entity_ids[imported_user.id] && should_import_user[user_id]
})
}
actions.push(add_users_value(result.added, result.id))
actions.push(remove_users_value(result.removed, result.id))
return result
})}))
.then((result) => {
store.dispatch(combined_actions(actions))
return result
})
case "exclusive":
return Promise.all(array_from_set(field.values).map((id) => {
let imported_value = values[id]
return new Promise((resolve, reject) => {
if (imported_value.existing) {
resolve(imported_value)
} else {
action_create_value(field_id, imported_value.name)
.then((id) => resolve({...imported_value, existing: id}))
}
})
}))
.then((imported_values) => request.asynchronous("users/update_values", {values: imported_values.map((imported_value) => {
const value = this.props.values[imported_value.existing]
const {value_id_for} = this.props.fields[value.field_id] || {}
const result = {
id: value.id,
added: Object.keys(field.value_id_for)
.filter(
(user_id) => field.value_id_for[user_id] == imported_value.id &&
value_id_for[users[user_id].existing.id] != field.value_id_for[user_id]
)
.map((id) => users[id].existing.id)
.filter((user_id) => should_import_user[user_id]),
removed: Object.keys(value_id_for)
.filter((user_id) => {
if (value_id_for[user_id] != value.id) {
return false
}
const imported_user = imported_users[this.props.users[user_id].email]
return imported_user && !field.value_id_for[imported_user.id] && should_import_user[user_id]
})
}
result.added.forEach((user_id) => {
actions.push(update_exclusive_entity_value(user_id, value.id))
})
result.removed.forEach((user_id) => {
actions.push(clear_exclusive_entity_value(user_id, value.id))
})
return result
})}))
.then((result) => {
store.dispatch(combined_actions(actions))
return result
})
case "comment":
let query = array_from_hash(users).filter((user) => (
should_import_user[user.existing.id] && (
field.conflicting_comments[user.id] || (
!field.existing &&
field.comments[user.id] != ""
)
)
)).map((user) => ({id: user.existing.id, comment: field.comments[user.id]}))
store.dispatch(update_user_comments(field_id, map_hash(hash_with_key(query), (user) => user.comment)))
return request.asynchronous("users/update_comments", {users: query, field: {id: field_id}})
default:
}
})
}
render_page() {
let exists_new_user = this.props.upload_results.new_users_count > 0
let exists_old_user = this.props.upload_results.old_users_count > 0
switch (this.state.active_field_type) {
case 'summary':
case 'invitations':
let messages = array_from_hash(this.state.messages[this.state.active_field_type])
let message_wrappers = (messages || []).map(({component: MessageWrapper, ...props}, index) => (
<MessageWrapper key={index} {...props} imported={this.state.imported.invitations} />
))
if (this.state.active_field_type == 'summary') {
return (
<div className={cx('upload-results__content-container')}>
<div className={cx('upload-results__content-title')}>
Aperçu
</div>
{message_wrappers}
{exists_old_user || exists_new_user ? (
<UserTable {...this.props.upload_results} editable={false} place_filterable={false} users={get_uploaded_users(this.props.upload_results.users, this.props.users)}/>
) : null}
</div>
)
} else {
return (
<div className={cx('upload-results__content-container')}>
<div className={cx('upload-results__content-title')}>
Invitations
</div>
<div className={cx('upload-results__messages')}>
{message_wrappers}
</div>
<div className={cx('upload-results__upload-button')}>
{this.state.imported.invitations ? (
<Button done>
{human_plural(this.props.upload_results.new_users_count, `L'invitation a été envoyée`, `Les invitations ont été envoyées`)}
</Button>
) : (this.state.importing.invitations ? (
<Button loading>
Importation en cours ...
</Button>
) : (
<Button
disabled={!for_all(messages, (message) => message.informational || message.validated || message.selected)}
onClick={this.send_invitations}
>
{human_plural(this.props.upload_results.new_users_count, `Envoyer l'invitation`, `Envoyer les invitations`)}
</Button>
))}
</div>
</div>
)
}
case 'instant_job_field':
case 'agency_field':
let imported = this.state.imported[this.state.active_field_type][this.state.active_field]
let new_field = this.state.active_field_type == 'agency_field' && !this.props.upload_results.fields[this.state.active_field].existing
let common_messages = array_from_hash((this.state.messages[this.state.active_field_type][this.state.active_field] || {}).common)
let common_message_wrappers = common_messages.map(({component: MessageWrapper, ...props}, index) => (
<MessageWrapper key={index} {...props} imported={imported} />
))
let new_users_messages = array_from_hash((this.state.messages[this.state.active_field_type][this.state.active_field] || {}).new)
let new_users_message_wrappers = new_users_messages.map(({component: MessageWrapper, ...props}, index) => (
<MessageWrapper key={index} {...props} imported={imported} />
))
let old_users_messages = array_from_hash((this.state.messages[this.state.active_field_type][this.state.active_field] || {}).old)
let old_users_message_wrappers = old_users_messages.map(({component: MessageWrapper, ...props}, index) => (
<MessageWrapper key={index} {...props} imported={imported} />
))
let statistics
let switcher = exists_new_user && exists_old_user ? (
<div className={cx('upload-results__statistics-switcher')}>
{["new", "old"].map((option) => (
<div
key={option}
className={cx('upload-results__statistics-switcher-option', {'upload-results__statistics-switcher-option_selected': this.state.statistics == option})}
onClick={() => this.setState({statistics: option})}
>
{{"new": "Nouveaux candidats", "old": "Anciens candidats"}[option]}
</div>
))}
</div>
) : null
const filter_user = (user) => this.state.statistics == (user.existing ? "old" : "new")
let field
if (this.state.active_field_type == 'instant_job_field') {
field = {name: hash_key_with_value(this.props.instant_job_fields, this.state.active_field)}
let all_users = array_from_hash(this.props.upload_results.users).filter(filter_user)
let filled_users_count = all_users.filter((user) => user[this.state.active_field]).length
statistics = (
<div className={cx('upload-results__statistics')}>
{switcher}
<div className={cx('upload-results__content-title')}>
Statistiques
</div>
<Statistics>
{{
"Renseigné": filled_users_count,
"Vide": all_users.length - filled_users_count,
}}
</Statistics>
</div>
)
} else {
field = this.props.upload_results.fields[this.state.active_field]
switch (field.category) {
case 'exclusive':
case 'non_exclusive':
let field_values = array_from_set(field.values).map((id) => this.props.upload_results.values[id])
let user_values_statistics = {}
field_values.forEach((value) => {
array_from_set(value.entity_ids).forEach((user_id) => {
if (filter_user(this.props.upload_results.users[user_id])) {
user_values_statistics[user_id] = (user_values_statistics[user_id] || 0) + 1
}
})
})
statistics = (
<div className={cx('upload-results__statistics')}>
{switcher}
<div className={cx('upload-results__content-title')}>
Statistiques
</div>
<Statistics>
{field_values.reduce((statistics, value) => {
statistics[value.name] = array_from_set(value.entity_ids).map((id) => this.props.upload_results.users[id]).filter(filter_user).length
return statistics
}, {})}
</Statistics>
{field.category == 'non_exclusive' ? (
<div>
<div className={cx('upload-results__content-title')}>
Nombre de valeurs
</div>
<Statistics>
{array_from_hash(user_values_statistics).reduce((statistics, value_count) => {
statistics[value_count] = (statistics[value_count] || 0) + 1
return statistics
}, {})}
</Statistics>
</div>
) : null}
</div>
)
break
case 'comment':
let all_users = array_from_hash(this.props.upload_results.users).filter(filter_user)
let {comments} = this.props.upload_results.fields[this.state.active_field]
let filled_users_count = all_users.filter(({id}) => comments[id]).length
statistics = (
<div className={cx('upload-results__statistics')}>
{switcher}
<div className={cx('upload-results__content-title')}>
Statistiques
</div>
<Statistics>
{{
"Renseigné": filled_users_count,
"Vide": all_users.length - filled_users_count,
}}
</Statistics>
</div>
)
break
default:
}
}
return (
<div className={cx('upload-results__content-container')}>
<div className={cx('upload-results__content-title')}>
{field.name}
</div>
<div className={cx('upload-results__main-content')}>
<div className={cx('upload-results__messages')}>
{common_message_wrappers}
{exists_new_user && new_users_messages.length > 0 ? (
<div className={cx('upload-results__content-title')}>
Candidats nouvellement importés
</div>
) : null}
{exists_new_user ? (
new_users_message_wrappers
) : null}
{exists_old_user && old_users_messages.length > 0? (
<div className={cx('upload-results__content-title')}>
Candidats déjà présents dans la base
</div>
) : null}
{exists_old_user ? (
old_users_message_wrappers
) : null}
</div>
{statistics}
</div>
<div className={cx('upload-results__upload-button')}>
{for_all([...new_users_messages, ...old_users_messages, ...common_messages], (message) => message.informational) ? (
<Button done>
{"Vous n'avez rien à faire"}
</Button>
) : (imported ? (
<Button done>
{new_field ? "Le champ a été créé et les données importées" : "Les données de ce champ ont été importées"}
</Button>
) : (this.state.importing[this.state.active_field_type][this.state.active_field] ? (
<Button loading>
Importation en cours ...
</Button>
) : (
<Button
disabled={!for_all([...new_users_messages, ...old_users_messages, ...common_messages], (message) => message.informational || message.validated || message.selected)}
onClick={() => {
let active_field = this.state.active_field, active_field_type = this.state.active_field_type
this.setState(deep_merge(this.state, {importing: {[active_field_type]: {[active_field]: true}}}))
this.forceUpdate()
let set_imported_state = () => {
if (this.mounted) {
this.setState(deep_merge(this.state, {imported: {[active_field_type]: {[active_field]: true}}}))
}
}
let messages = this.state.messages[active_field_type][active_field]
switch (active_field_type) {
case "instant_job_field":
this.import_users(active_field)
.then(set_imported_state)
break
case "agency_field":
this.import_field(active_field, new_field ? 'overwrite' : (exists_old_user && messages.old.conflict ? messages.old.conflict.selected : 'ignore'))
.then(set_imported_state)
break
default:
}
}}
>
{
(new_field) ?
"Créer le champ" :
((!exists_old_user ||
(exists_old_user && exists_new_user && for_all(old_users_messages, (message) => message.selected != "overwrite"))) ?
"Importer les données pour les nouveaux candidats" :
"Importer les données de ce champ")
}
</Button>
)))}
</div>
</div>
)
default:
return <div></div>
}
}
render_navigation(field_array) {
let current_index = field_array.findIndex((field) => field.active)
let decrement = () => {
field_array[current_index - 1].onClick()
}
let increment = () => {
field_array[current_index + 1].onClick()
}
return (
<div className={cx('upload-results__actions')}>
<div className={cx('upload-results__actions__button-container', 'upload-results__actions__button-container__reverse')}>
{current_index > 0 ? (
<Button secondary left_icon={<MdKeyboardArrowLeft />} onClick={decrement} disabled={current_index == 0}>
Précédent
</Button>
) : null}
</div>
<div className={cx('upload-results__actions__button-container')}>
{current_index + 1 < field_array.length ? (
<Button secondary right_icon={<MdKeyboardArrowRight />} disabled={field_array[current_index + 1] && field_array[current_index + 1].disabled} onClick={increment}>
Suivant
</Button>
) : (
<Button secondary right_icon={<MdCheck />} disabled={field_array[current_index + 1] && field_array[current_index + 1].disabled} onClick={this.props.close}>
Terminer
</Button>
)}
</div>
</div>
)
}
render() {
let status = {
invitations: for_all(array_from_hash(this.state.messages.invitations), (message) => message.informational || message.validated || message.selected) ? 'ready' : 'warning',
instant_job_field: create_hash(array_from_hash(this.props.instant_job_fields), (field_name) => {
let all_messages = this.state.messages.instant_job_field[field_name] || {}
let messages = [
...array_from_hash(all_messages.common),
...array_from_hash(all_messages.new),
...array_from_hash(all_messages.old),
]
if (for_all(messages, (message) => message.informational)) {
return 'imported'
} else if (for_all(messages, (message) => message.informational || message.validated || message.selected)) {
return 'ready'
} else {
return 'warning'
}
}),
agency_field: create_hash(Object.keys(this.props.upload_results.fields), (field_id) => {
let all_messages = this.state.messages.agency_field[field_id] || {}
let messages = [
...array_from_hash(all_messages.common),
...array_from_hash(all_messages.new),
...array_from_hash(all_messages.old),
]
if (for_all(messages, (message) => message.informational)) {
return 'imported'
} else if (for_all(messages, (message) => message.informational || message.validated || message.selected)) {
return 'ready'
} else {
return 'warning'
}
})
}
let exists_user = this.props.upload_results.new_users_count > 0 || this.props.upload_results.old_users_count > 0
let field_array = [
{
label: "Aperçu",
onClick: () => this.setState({active_field_type: 'summary'}),
active: this.state.active_field_type == 'summary',
disabled: false,
icon: <MenuIcon />,
display: true,
},
{
label: "Invitations",
onClick: () => this.setState({active_field_type: 'invitations'}),
active: this.state.active_field_type == 'invitations',
disabled: false,
icon: <MenuIcon status={this.state.imported['invitations'] ? 'imported' : status['invitations']} />,
display: this.props.upload_results.new_users_count > 0,
},
...array_from_set(this.props.upload_results.instant_job_fields_present)
.map((field_label) => {
let field_name = this.props.instant_job_fields[field_label]
return {
label: field_label,
onClick: () => this.setState({active_field_type: 'instant_job_field', active_field: field_name}),
active: this.state.active_field_type == 'instant_job_field' && this.state.active_field == field_name,
disabled: this.props.upload_results.new_users_count > 0 && !this.state.imported['invitations'],
icon: <MenuIcon status={this.state.imported['instant_job_field'][field_name] ? 'imported' : status['instant_job_field'][field_name]} />,
display: exists_user,
}
}),
...array_from_hash(this.props.upload_results.fields).map((field) => (
{
label: field.name,
onClick: () => this.setState({active_field_type: 'agency_field', active_field: field.id}),
active: this.state.active_field_type == 'agency_field' && this.state.active_field == field.id,
disabled: this.props.upload_results.new_users_count > 0 && !this.state.imported['invitations'],
icon: <MenuIcon status={this.state.imported['agency_field'][field.id] ? 'imported' : status['agency_field'][field.id]} />,
display: exists_user,
}
)),
].filter((tab) => tab.display)
return (
<div className={cx('upload-results')}>
<div className={cx('upload-results__import-button')}>
<Button icon={MdFileUpload} secondary onClick={this.props.upload_new_file}>
Charger un autre fichier
</Button>
</div>
<div className={cx('upload-results__container')}>
<div className={cx('upload-results__menu-container')}>
<Menu>
{field_array}
</Menu>
</div>
{this.render_page()}
</div>
{this.render_navigation(field_array)}
</div>
)
}
}
const Message = (props) => {
let Icon = {
'error': MdError,
'warning': MdWarning,
'ok': MdCheck,
}[props.status || 'ok']
return (
<div className={cx('upload-results__message', `upload-results__message_${props.status || 'ok'}`)}>
<div className={cx('upload-results__message-title')}>
<div className={cx('upload-results__message-title-icon')}>
<Icon />
</div>
<div className={cx('upload-results__message-title-label')}>
{props.title}
</div>
</div>
{props.children ? (
<div className={cx('upload-results__message-information')}>
{props.children}
</div>
) : null}
</div>
)
}
const MessageFieldsCount = (props) => {
let new_field_names_count = props.new_field_names.length
if (new_field_names_count > 0) {
return (
<Message
status={props.new_field_names.length > 0 ? 'warning' : 'ok'}
title={human_plural(props.total_fields_count, 'Votre fichier contient %% champ (colonne)', 'Votre fichier contient %% champs (colonnes)')}
>
<p>{human_plural(props.total_fields_count - props.new_field_names.length, '%% est déjà présent', '%% sont déjà présents')} sur InstantJob</p>
<p>{human_plural(props.new_field_names.length, '%% correspond à un nouveau champ', '%% correspondent à des nouveaux champs')} : {human_join(props.new_field_names)}</p>
</Message>
)
} else {
return (
<Message
status={props.new_field_names.length > 0 ? 'warning' : 'ok'}
title={human_plural(props.total_fields_count, '%% champ a été importé et était déjà présent sur InstantJob', '%% champs ont été importés et étaient déjà présents sur InstantJob')}
/>
)
}
}
const MessageInviteUsers = (props) => {
let title
if (props.new_users == props.all_users) {
title = `Votre fichier contient des données au sujet de ${human_plural(props.all_users, '%% nouveau candidat', '%% nouveaux candidats')}.`
} else {
title = `Votre fichier contient des données au sujet de ${human_plural(props.all_users, '%% candidat', '%% candidats')}.`
}
return (
<Message
status={props.validated ? 'ok' : 'warning'}
title={title}
>
{props.new_users == props.all_users ? null : (
<p>{human_plural(props.all_users - props.new_users, '%% était déjà présent', '%% étaient déjà présents')} sur InstantJob</p>
)}
{props.new_users == props.all_users ? null : (
<p>{human_plural(props.new_users, '%% est un nouveau candidat', '%% sont des nouveaux candidats')}</p>
)}
<Action imported={props.imported} status={props.validated ? 'checked' : 'unchecked'} onClick={props.onClick}>
{`Créer ${human_plural(props.new_users, '%% nouveau candidat sur InstantJob et lui', '%% nouveaux candidats sur InstantJob et leur')} envoyer une invitation à télécharger l'application.`}
</Action>
</Message>
)
}
const MessageAbsentFields = (props) => (
<Message
status={props.validated ? 'ok' : 'warning'}
title={`${human_plural(props.absent_fields.length, `Le champ "${props.absent_fields[0]}" n'a pas été trouvé`, `Les champs ${human_join(props.absent_fields.map((field) => `"${field}"`))} n'ont pas été trouvés`)} dans votre fichier.`}
>
<Action imported={props.imported} status={props.validated ? 'checked' : 'unchecked'} onClick={props.onClick}>
{`Laisser ${human_plural(props.absent_fields.length, 'ce champ vide', 'ces champs vides')} pour les nouveaux candidats importés.`}
</Action>
</Message>
)
const MessageInstantJobFieldNew = (props) => (
<Message title={`Les données ont déjà été importées car il s'agit d'un champ imposé par InstantJob.`} />
)
const MessageAgencyFieldReady = (props) => (
<Message title={`Données prêtes à être importées.`} />
)
const MessageFieldIdentical = (props) => (
<Message title={`Les données du fichier sont identiques aux données dans InstantJob.`} />
)
const MessageFieldConflict = (props) => {
let new_field_values_count = (props.new_field_values || []).length
return (
<Message
status={props.selected ? 'ok' : 'warning'}
title={`Pour ${human_plural(props.conflicting_users_count, '%% candidat', '%% candidats')}, les données de votre fichier ne correspondent pas aux données présentes dans votre base InstantJob.`}
>
{new_field_values_count ? (
<p>Nous avons également trouvé {human_plural(new_field_values_count, "%% nouvelle valeur", "%% nouvelles valeurs")} chez les candidats déjà présents : {human_join(props.new_field_values, 10)}</p>
) : null}
{props.exists_new_user ? (
<Action imported={props.imported} status={props.selected == 'ignore' ? 'checked' : (props.selected ? 'discarded' : 'unchecked')} onClick={() => props.on_select('ignore')}>
Ne pas prendre en compte les données importées et laisser la base InstantJob inchangée
</Action>
) : null}
<Action
imported={props.imported}
status={props.selected == 'overwrite' ? 'checked' : (props.selected && props.exists_new_user ? 'discarded' : 'unchecked')}
onClick={props.exists_new_user || !props.selected ? () => props.on_select('overwrite') : () => props.on_select(false)}
>
Ecraser les données sur InstantJob et les remplacer par les données de votre fichier {new_field_values_count > 0 ? `en créant ${human_plural(new_field_values_count, "%% nouvelle valeur", "%% nouvelles valeurs")}` : null}
</Action>
</Message>
)
}
const MessageInstantJobAppUserConflict = (props) => (
<Message
status={'warning'}
title={`Les données de ${human_plural(props.conflicting_users_count, "%% candidat ne pourront pas être modifiées car il indique", "%% candidats ne pourront pas être modifiées car ils indiquent")} ces informations via l'application.`}
/>
)
const MessageNewField = (props) => (
<Message status={props.selected ? 'ok' : 'warning'} title={"Ce champ n'existe pas dans la base InstantJob"}>
<p>Ce champ correspond-il a un champ de tri (à choix multiple) ou à un champ de commentaire ?</p>
<FieldCategoriesSelector
selected={props.selected}
on_select={props.imported ? () => {} : props.on_select}
/>
</Message>
)
const MessageDuplicateUsers = (props) => (
<Message
status={'warning'}
title={`Votre fichier contient ${human_plural(props.duplicate_users_count, "%% addresse mail", "%% adresses mail")} en double`}
>
<p>Nous avons conservé uniquement la dernière ligne correspondant à chaque adresse mail dupliquée</p>
</Message>
)
const MessageInvalidEmails = (props) => (
<Message
status={'warning'}
title={`Votre fichier contient ${human_plural(props.invalid_emails.length, "%% addresse mail invalide", "%% adresses mail invalides")} : ${human_join(props.invalid_emails, 10)}`}
>
<p>Il faut une adresse mail valide pour envoyer une invitation, {human_plural(props.invalid_emails.length, "ce candidat ne pourra donc pas être importé", "ces candidats ne pourront donc pas être importés")}.</p>
</Message>
)
const MessageEmptyFile = (props) => (
<Message
status={'error'}
title={`Votre fichier ne contient aucune adresse mail valide`}
/>
)
const MessageNewValuesNewUsers = (props) => (
<Message
status={props.validated ? 'ok' : 'warning'}
title={`Nous avons trouvé ${human_plural(props.new_field_values.length, "%% nouvelle valeur", "%% nouvelles valeurs")} chez ${human_plural(props.new_users_count, "le nouveau candidat", "les %% nouveaux candidats")} : ${human_join(props.new_field_values, 10)}`}
>
<Action imported={props.imported} status={props.validated ? 'checked' : 'unchecked'} onClick={props.onClick}>
Créer {human_plural(props.new_field_values.length, "%% nouvelle valeur", "%% nouvelles valeurs")}
</Action>
</Message>
)
const Action = (props) => {
let Icon = props.status == 'checked' ? MdCheckBox : MdCheckBoxOutlineBlank
return (
<div className={cx('upload-results__message-action', {[`upload-results__message-action_${props.status}`]: props.status, 'upload-results__message-action_editable': !props.imported})} onClick={props.imported ? () => {} : props.onClick}>
<div className={cx('upload-results__message-action-checkbox')}>
<Icon />
</div>
<div className={cx('upload-results__message-action-label')}>
{props.children}
</div>
</div>
)
}
const MenuIcon = (props) => {
switch (props.status) {
case 'imported':
return (
<div className={cx('upload-results__menu-icon', 'upload-results__menu-icon_imported')}>
<MdCheck />
</div>
)
case 'ready':
return (
<div className={cx('upload-results__menu-icon', 'upload-results__menu-icon_ready')}></div>
)
case 'warning':
return (
<div className={cx('upload-results__menu-icon', 'upload-results__menu-icon_warning')}></div>
)
case 'error':
return (
<div className={cx('upload-results__menu-icon', 'upload-results__menu-icon_error')}></div>
)
default:
return <div></div>
}
}
const Statistics = (props) => (
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'stretch'}}>
{Object.keys(props.children).map((label, index) => (
<div key={index} style={{display: 'flex', justifyContent: 'space-between'}}>
<div>
{label}
</div>
<div>
{props.children[label]}
</div>
</div>
))}
</div>
)
const UploadResults = connect(
(state) => ({
fields: state.fields.fields,
values: state.fields.values,
users: get_users(state),
}),
)(UploadResultsStatic)
UploadResults.defaultProps = {
upload_results: {
fields: [],
values: {},
users: [],
},
fields: {},
values: {},
users: {},
update_upload_results: () => {},
upload_new_file: () => {},
close: () => {},
}
export default UploadResults