first commit

This commit is contained in:
Noor E Ilahi
2026-01-09 12:54:53 +05:30
commit 7ccf44f7da
1070 changed files with 113036 additions and 0 deletions

View File

@@ -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>

View 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"
}
}
}
}

View File

@@ -0,0 +1,12 @@
{
"settings": {
"company_language": "Язык компании",
"color_interval": {
"label": "Прогресс рабочего дня",
"notification" : {
"gt_100": "Значение не может быть больше 100",
"interval_already_in_use": "Этот интервал уже используется"
}
}
}
}

View 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;
}

View 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);
},
},
});
},
},
],
},
},
};

View File

@@ -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;
}
}