first commit
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="colors">
|
||||
<ul v-if="colorsConfig.length > 0">
|
||||
<li v-for="(config, index) in colorsConfig" :key="index" class="color-readiness__item">
|
||||
<at-input
|
||||
:value="getPercent(config)"
|
||||
class="color-readiness__start"
|
||||
type="number"
|
||||
placeholder="Start percent"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@blur="setStart(index, $event)"
|
||||
>
|
||||
<template slot="append">
|
||||
<span v-if="config.start < 1">%</span>
|
||||
<span v-else>{{ $t('Over Time') }}</span>
|
||||
</template>
|
||||
</at-input>
|
||||
|
||||
<at-input
|
||||
v-if="config.start < 1"
|
||||
:value="parseInt(config.end * 100)"
|
||||
class="color-readiness__end"
|
||||
type="number"
|
||||
placeholder="End percent"
|
||||
:min="1"
|
||||
:max="100"
|
||||
@blur="setEnd(index, $event)"
|
||||
>
|
||||
<template slot="append">
|
||||
<span>%</span>
|
||||
</template>
|
||||
</at-input>
|
||||
<div class="color-readiness__color">
|
||||
<ColorInput
|
||||
class="color-input at-input__original"
|
||||
:value="config.color"
|
||||
@change="setColor(config, $event)"
|
||||
/>
|
||||
</div>
|
||||
<at-button class="color-readiness__remove" @click.prevent="remove(index)">
|
||||
<span class="icon icon-x"></span>
|
||||
</at-button>
|
||||
</li>
|
||||
</ul>
|
||||
<at-button-group>
|
||||
<at-button class="color-readiness__add" :disabled="isDisabledAddButton" @click.prevent="add">{{
|
||||
$t('control.add')
|
||||
}}</at-button>
|
||||
<at-button class="color-readiness__reset" @click.prevent="reset">{{ $t('control.reset') }}</at-button>
|
||||
</at-button-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ColorInput from '@/components/ColorInput';
|
||||
|
||||
const defaultInterval = { color: '#3ba8da', start: 0, end: 0.1 };
|
||||
|
||||
export default {
|
||||
props: {
|
||||
colorsConfig: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ColorInput,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
usedIntervals() {
|
||||
return this.colorsConfig.map(el => ({
|
||||
start: parseInt(el.start * 100),
|
||||
end: parseInt(el.end * 100),
|
||||
}));
|
||||
},
|
||||
freeIntervals() {
|
||||
if (!this.usedIntervals.length) {
|
||||
return [{ start: 0, end: 100 }];
|
||||
}
|
||||
|
||||
const lastIndex = this.usedIntervals.length - 1;
|
||||
return this.usedIntervals.reduce(
|
||||
(accum, curEl, i, arr) => {
|
||||
const index = i === arr.length - 1 ? i : i + 1;
|
||||
|
||||
//if have OverTime
|
||||
if (lastIndex === index && curEl.start === 100 && curEl.end === 0) {
|
||||
return accum;
|
||||
}
|
||||
|
||||
//if the first interval doesn't start from null
|
||||
if (i === 0 && curEl.start !== 0) {
|
||||
accum[i].end = curEl.start - 1;
|
||||
}
|
||||
|
||||
//if first interval starts from null, then remove the default intterval
|
||||
if (i === 0 && accum[i].end === 0) {
|
||||
accum.splice(i, 1);
|
||||
}
|
||||
|
||||
//if not have last free interval
|
||||
if (lastIndex === i && curEl.end !== 100) {
|
||||
return [...accum, { start: curEl.end + 1, end: 100 }];
|
||||
}
|
||||
|
||||
//if there's no Overtime, we can add it
|
||||
if (lastIndex === i && curEl.start !== 100) {
|
||||
return [...accum, { start: 100, end: '' }];
|
||||
}
|
||||
|
||||
// if the interval is 100
|
||||
if (arr[index].start === curEl.end) {
|
||||
return accum;
|
||||
}
|
||||
|
||||
// if there's free interval
|
||||
if (arr[index].start - 1 !== curEl.end) {
|
||||
return [...accum, { start: curEl.end + 1, end: arr[index].start - 1 }];
|
||||
}
|
||||
|
||||
return accum;
|
||||
},
|
||||
[{ start: 0, end: 0 }],
|
||||
);
|
||||
},
|
||||
isDisabledAddButton() {
|
||||
return this.freeIntervals.length === 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
let interval = defaultInterval;
|
||||
|
||||
if (this.freeIntervals.length > 0) {
|
||||
interval = {
|
||||
start: this.freeIntervals[0].start / 100,
|
||||
end: this.freeIntervals[0].end / 100,
|
||||
color: '#3ba8da',
|
||||
};
|
||||
}
|
||||
|
||||
this.$emit('addColorReadiness', [interval]);
|
||||
},
|
||||
|
||||
remove(index) {
|
||||
this.$emit('onRemoveRelation', index);
|
||||
},
|
||||
setEnd(index, ev) {
|
||||
const newEnd = ev.target.valueAsNumber;
|
||||
if (this.colorsConfig[index].end === newEnd / 100) {
|
||||
return;
|
||||
}
|
||||
|
||||
const haveThisInterval = this.usedIntervals.filter((el, i) => {
|
||||
if (index !== i) {
|
||||
return newEnd >= el.start && newEnd <= el.end;
|
||||
}
|
||||
});
|
||||
|
||||
if (haveThisInterval.length > 0) {
|
||||
this.$Notify({
|
||||
type: 'error',
|
||||
title: this.$t('message.error'),
|
||||
message: this.$t('settings.color_interval.notification.interval_already_in_use'),
|
||||
});
|
||||
|
||||
this.$forceUpdate();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('setEnd', index, newEnd / 100);
|
||||
},
|
||||
setStart(index, ev) {
|
||||
const newStart = ev.target.valueAsNumber;
|
||||
if (this.colorsConfig[index].start === newStart / 100) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newStart >= 100) {
|
||||
const haveOverTime = this.colorsConfig.filter(el => el.start === 1).length;
|
||||
if (haveOverTime > 0) {
|
||||
this.$Notify({
|
||||
type: 'error',
|
||||
title: this.$t('message.error'),
|
||||
message: this.$t('settings.color_interval.notification.gt_100'),
|
||||
});
|
||||
|
||||
this.$forceUpdate();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const haveThisInterval = this.usedIntervals.filter((el, i) => {
|
||||
if (index !== i) {
|
||||
return newStart >= el.start && newStart <= el.end;
|
||||
}
|
||||
});
|
||||
|
||||
if (haveThisInterval.length > 0) {
|
||||
this.$Notify({
|
||||
type: 'error',
|
||||
title: this.$t('message.error'),
|
||||
message: this.$t('settings.color_interval.notification.interval_already_in_use'),
|
||||
});
|
||||
|
||||
this.$forceUpdate();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('setStart', index, newStart / 100);
|
||||
|
||||
if (newStart === 100) {
|
||||
this.$emit('setEnd', index, 0);
|
||||
} else if (this.colorsConfig[index].end === 0) {
|
||||
//eslint-disable-next-line vue/no-mutating-props
|
||||
this.colorsConfig[index].end = 1;
|
||||
}
|
||||
},
|
||||
getPercent(config) {
|
||||
if (config.start >= 1) {
|
||||
config.isOverTime = true;
|
||||
this.$emit('setOverTime', this.colorsConfig);
|
||||
}
|
||||
if (config.start < 1 && 'isOverTime' in config) {
|
||||
this.$delete(config, 'isOverTime');
|
||||
this.$emit('setOverTime', this.colorsConfig);
|
||||
}
|
||||
|
||||
return parseInt(config.start * 100);
|
||||
},
|
||||
setColor(config, event) {
|
||||
this.$set(config, 'color', event);
|
||||
},
|
||||
reset() {
|
||||
this.$emit('reset');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.color-input {
|
||||
width: 170px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
padding: 0px;
|
||||
border: none;
|
||||
}
|
||||
.color-readiness {
|
||||
&__item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
&__start,
|
||||
&__end,
|
||||
&__color {
|
||||
flex: 1;
|
||||
margin-right: 0.5em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
&__remove {
|
||||
height: 40px;
|
||||
}
|
||||
&__color {
|
||||
max-width: 170px;
|
||||
}
|
||||
}
|
||||
input[type='color' i]::-webkit-color-swatch-wrapper,
|
||||
input[type='color' i]::-webkit-color-swatch {
|
||||
padding: 0px;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
12
resources/frontend/core/modules/Settings/locales/en.json
Normal file
12
resources/frontend/core/modules/Settings/locales/en.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"settings": {
|
||||
"company_language": "Company language",
|
||||
"color_interval": {
|
||||
"label": "Progress of the working day",
|
||||
"notification": {
|
||||
"gt_100": "The value can't be greater than 100",
|
||||
"interval_already_in_use": "This interval already in use"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
resources/frontend/core/modules/Settings/locales/ru.json
Normal file
12
resources/frontend/core/modules/Settings/locales/ru.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"settings": {
|
||||
"company_language": "Язык компании",
|
||||
"color_interval": {
|
||||
"label": "Прогресс рабочего дня",
|
||||
"notification" : {
|
||||
"gt_100": "Значение не может быть больше 100",
|
||||
"interval_already_in_use": "Этот интервал уже используется"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
resources/frontend/core/modules/Settings/module.init.js
Normal file
24
resources/frontend/core/modules/Settings/module.init.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { hasRole } from '@/utils/user';
|
||||
|
||||
export const ModuleConfig = {
|
||||
routerPrefix: 'settings',
|
||||
loadOrder: 10,
|
||||
moduleName: 'Settings',
|
||||
};
|
||||
|
||||
export function init(context) {
|
||||
const sectionGeneral = require('./sections/general');
|
||||
context.addCompanySection(sectionGeneral.default);
|
||||
context.addUserMenuEntry({
|
||||
label: 'navigation.company_settings',
|
||||
icon: 'icon-settings',
|
||||
to: { name: 'company.settings.general' },
|
||||
displayCondition: store => hasRole(store.getters['user/user'], 'admin'),
|
||||
});
|
||||
context.addLocalizationData({
|
||||
en: require('./locales/en'),
|
||||
ru: require('./locales/ru'),
|
||||
});
|
||||
|
||||
return context;
|
||||
}
|
||||
201
resources/frontend/core/modules/Settings/sections/general.js
Normal file
201
resources/frontend/core/modules/Settings/sections/general.js
Normal file
@@ -0,0 +1,201 @@
|
||||
import LanguageSelector from '@/components/LanguageSelector';
|
||||
import TimezonePicker from '@/components/TimezonePicker';
|
||||
import CompanyService from '../services/company.service';
|
||||
import ColorSelect from '../components/ColorSelect';
|
||||
import PrioritySelect from '@/components/PrioritySelect';
|
||||
import ScreenshotsStateSelect from '@/components/ScreenshotsStateSelect';
|
||||
import { store } from '@/store';
|
||||
import { hasRole } from '@/utils/user';
|
||||
|
||||
export default {
|
||||
// Check if this section can be rendered and accessed, this param IS OPTIONAL (true by default)
|
||||
// NOTICE: this route will not be added to VueRouter AT ALL if this check fails
|
||||
// MUST be a function that returns a boolean
|
||||
accessCheck: async () => hasRole(store.getters['user/user'], 'admin'),
|
||||
|
||||
scope: 'company',
|
||||
|
||||
order: 0,
|
||||
|
||||
route: {
|
||||
// After processing this route will be named as 'settings.exampleSection'
|
||||
name: 'company.settings.general',
|
||||
|
||||
// After processing this route can be accessed via URL 'settings/example'
|
||||
path: '/company/general',
|
||||
|
||||
meta: {
|
||||
// After render, this section will be labeled as 'Example Section'
|
||||
label: 'settings.general',
|
||||
|
||||
// Service class to gather the data from API, should be an instance of Resource class
|
||||
service: new CompanyService(),
|
||||
|
||||
// Renderable fields array
|
||||
fields: [
|
||||
{
|
||||
label: 'settings.company_timezone',
|
||||
key: 'timezone',
|
||||
render: (h, props) => {
|
||||
const value = props.values.timezone ?? 'UTC';
|
||||
return h(TimezonePicker, {
|
||||
props: {
|
||||
value,
|
||||
},
|
||||
on: {
|
||||
onTimezoneChange(ev) {
|
||||
props.inputHandler(ev);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'field.work_time',
|
||||
key: 'work_time',
|
||||
maxValue: 24,
|
||||
minValue: 0,
|
||||
fieldOptions: {
|
||||
type: 'number',
|
||||
placeholder: 'field.work_time',
|
||||
},
|
||||
tooltipValue: 'tooltip.work_time',
|
||||
},
|
||||
{
|
||||
label: 'settings.color_interval.label',
|
||||
key: 'color',
|
||||
displayable: store =>
|
||||
'work_time' in store.getters['user/companyData'] && store.getters['user/companyData'].work_time,
|
||||
tooltipValue: 'tooltip.color_intervals',
|
||||
render(h, props) {
|
||||
const defaultConfig = [
|
||||
{
|
||||
start: 0,
|
||||
end: 0.75,
|
||||
color: '#ffb6c2',
|
||||
},
|
||||
{
|
||||
start: 0.76,
|
||||
end: 1,
|
||||
color: '#93ecda',
|
||||
},
|
||||
{
|
||||
start: 1,
|
||||
end: 0,
|
||||
color: '#3cd7b6',
|
||||
isOverTime: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (!Array.isArray(props.currentValue)) {
|
||||
'color' in props.companyData
|
||||
? (props.currentValue = props.companyData.color)
|
||||
: (props.currentValue = defaultConfig);
|
||||
|
||||
this.inputHandler(props.currentValue);
|
||||
}
|
||||
|
||||
return h(ColorSelect, {
|
||||
props: {
|
||||
colorsConfig: props.currentValue,
|
||||
},
|
||||
on: {
|
||||
addColorReadiness(data) {
|
||||
props.inputHandler(
|
||||
[...props.currentValue, ...data].sort((a, b) => {
|
||||
return a.start - b.start;
|
||||
}),
|
||||
);
|
||||
},
|
||||
onRemoveRelation(index) {
|
||||
props.currentValue.splice(index, 1);
|
||||
props.inputHandler(props.currentValue);
|
||||
},
|
||||
setOverTime(data) {
|
||||
props.inputHandler(data);
|
||||
},
|
||||
reset() {
|
||||
props.inputHandler(defaultConfig);
|
||||
},
|
||||
setStart(index, newStart) {
|
||||
props.currentValue[index].start = newStart;
|
||||
props.inputHandler(props.currentValue);
|
||||
},
|
||||
setEnd(index, newEnd) {
|
||||
props.currentValue[index].end = newEnd;
|
||||
props.inputHandler(props.currentValue);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'field.auto_thin',
|
||||
key: 'auto_thinning',
|
||||
fieldOptions: {
|
||||
type: 'switch',
|
||||
placeholder: 'field.auto_thin',
|
||||
},
|
||||
tooltipValue: 'tooltip.auto_thin',
|
||||
},
|
||||
{
|
||||
label: 'field.screenshots_state',
|
||||
key: 'screenshots_state',
|
||||
render: (h, props) => {
|
||||
return h(ScreenshotsStateSelect, {
|
||||
props: {
|
||||
value: store.getters['screenshots/getCompanyStateWithOverrides'](
|
||||
props.values.screenshots_state,
|
||||
),
|
||||
isDisabled: store.getters['screenshots/isCompanyStateLocked'],
|
||||
hideIndexes: [0],
|
||||
},
|
||||
on: {
|
||||
input(value) {
|
||||
props.inputHandler(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'field.default_priority',
|
||||
key: 'default_priority_id',
|
||||
render: (h, props) => {
|
||||
const value = props.values.default_priority_id ?? 0;
|
||||
|
||||
return h(PrioritySelect, {
|
||||
props: {
|
||||
value,
|
||||
clearable: true,
|
||||
},
|
||||
on: {
|
||||
input(value) {
|
||||
props.inputHandler(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'settings.company_language',
|
||||
key: 'language',
|
||||
render: (h, props) => {
|
||||
const lang = props.values.language ?? 'en';
|
||||
|
||||
return h(LanguageSelector, {
|
||||
props: {
|
||||
value: lang,
|
||||
},
|
||||
on: {
|
||||
setLanguage(lang) {
|
||||
props.inputHandler(lang);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import axios from '@/config/app';
|
||||
import SettingsService from '@/services/settings.service';
|
||||
|
||||
/**
|
||||
* Section service class.
|
||||
* Used to fetch data from api for inside DynamicSettings.vue
|
||||
* Data is stored inside store -> settings -> sections -> data
|
||||
*/
|
||||
export default class CompanyService extends SettingsService {
|
||||
/**
|
||||
* API endpoint URL
|
||||
* @returns string
|
||||
*/
|
||||
getItemRequestUri() {
|
||||
return `company-settings`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch item data from api endpoint
|
||||
* @returns {data}
|
||||
*/
|
||||
async getAll() {
|
||||
return (await axios.get(this.getItemRequestUri(), { ignoreCancel: true })).data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save item data
|
||||
* @param data
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async save(payload) {
|
||||
const { data } = await axios.patch(this.getItemRequestUri(), payload);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user