4856-worker.time-control #1375

Merged
joan merged 32 commits from 4856-worker.time-control into dev 2023-03-16 06:39:02 +00:00
15 changed files with 477 additions and 58 deletions

View File

@ -8,13 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2310.01] - 2023-03-23
### Added
-
- (Trabajadores -> Control de horario) Ahora se puede confirmar/no confirmar el registro horario de cada semana desde esta sección
### Changed
-
-
### Fixed
- (Clientes -> Listado extendido) Resuelto error al filtrar por clientes inactivos desde la columna "Activo"
- (Clientes -> Listado extendido) Resuelto error al filtrar por clientes inactivos desde la columna "Activo"
- (General) Al pasar el ratón por encima del icono de "Borrar" en un campo, se hacía más grande afectando a la interfaz
## [2308.01] - 2023-03-09

View File

@ -2825,4 +2825,11 @@ INSERT INTO `vn`.`deviceProductionUser` (`deviceProductionFk`, `userFk`, `create
(1, 1, util.VN_NOW()),
(3, 3, util.VN_NOW());
INSERT INTO `vn`.`workerTimeControlMail` (`id`, `workerFk`, `year`, `week`, `state`, `updated`, `sendedCounter`, `reason`)
VALUES
(1, 9, 2000, 49, 'REVISE', util.VN_NOW(), 1, 'test2'),
(2, 9, 2000, 50, 'SENDED', util.VN_NOW(), 1, NULL),
(3, 9, 2000, 51, 'CONFIRMED', util.VN_NOW(), 1, NULL),
(4, 9, 2001, 1, 'SENDED', util.VN_NOW(), 1, NULL);

View File

@ -24,7 +24,7 @@
<div class="weekdays">
<section
ng-repeat="day in ::$ctrl.weekDays"
translate-attr="::{title: day.name}"
translate-attr="::{title: day.name}"
ng-click="$ctrl.selectWeekDay($event, day.index)">
<span>{{::day.localeChar}}</span>
</section>
@ -57,4 +57,4 @@
</section>
</div>
</div>
</div>
</div>

View File

@ -15,9 +15,9 @@ export default class Calendar extends FormInput {
constructor($element, $scope, vnWeekDays, moment) {
super($element, $scope);
this.weekDays = vnWeekDays.locales;
this.defaultDate = Date.vnNew();
this.displayControls = true;
this.moment = moment;
this.defaultDate = Date.vnNew();
}
/**
@ -207,14 +207,23 @@ export default class Calendar extends FormInput {
}
repeatLast() {
if (!this.formatDay) return;
if (this.formatDay) {
const days = this.element.querySelectorAll('.days > .day');
for (let i = 0; i < days.length; i++) {
this.formatDay({
$day: this.days[i],
$element: days[i]
});
}
}
let days = this.element.querySelectorAll('.days > .day');
for (let i = 0; i < days.length; i++) {
this.formatDay({
$day: this.days[i],
$element: days[i]
});
if (this.formatWeek) {
const weeks = this.element.querySelectorAll('.weeks > .day');
for (const week of weeks) {
this.formatWeek({
$element: week
});
}
}
}
}
@ -228,6 +237,7 @@ ngModule.vnComponent('vnCalendar', {
hasEvents: '&?',
getClass: '&?',
formatDay: '&?',
formatWeek: '&?',
displayControls: '<?',
hideYear: '<?',
hideContiguous: '<?',

View File

@ -0,0 +1,61 @@
module.exports = Self => {
Self.remoteMethodCtx('getMailStates', {
description: 'Get the states of a month about time control mail',
accessType: 'READ',
accepts: [{
arg: 'id',
type: 'number',
description: 'The worker id',
http: {source: 'path'}
},
{
arg: 'month',
type: 'number',
description: 'The number of the month'
},
{
arg: 'year',
type: 'number',
description: 'The number of the year'
}],
returns: [{
type: ['object'],
root: true
}],
http: {
path: `/:id/getMailStates`,
verb: 'GET'
}
});
Self.getMailStates = async(ctx, workerId, options) => {
const models = Self.app.models;
const args = ctx.args;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const times = await models.Time.find({
fields: ['week'],
where: {
month: args.month,
year: args.year
}
}, myOptions);
const weeks = times.map(time => time.week);
const weekNumbersSet = new Set(weeks);
const weekNumbers = Array.from(weekNumbersSet);
const workerTimeControlMails = await models.WorkerTimeControlMail.find({
where: {
workerFk: workerId,
year: args.year,
week: {inq: weekNumbers}
}
}, myOptions);
return workerTimeControlMails;
};
};

View File

@ -0,0 +1,29 @@
const models = require('vn-loopback/server/server').models;
describe('workerTimeControl getMailStates()', () => {
const workerId = 9;
const ctx = {args: {
month: 12,
year: 2000
}};
it('should get the states of a month about time control mail', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const response = await models.WorkerTimeControl.getMailStates(ctx, workerId, options);
expect(response[0].state).toEqual('REVISE');
expect(response[1].state).toEqual('SENDED');
expect(response[2].state).toEqual('CONFIRMED');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -47,6 +47,10 @@ module.exports = Self => {
if (typeof options == 'object')
Object.assign(myOptions, options);
const isHimself = userId == args.workerId;
if (!isHimself)
throw new UserError(`You don't have enough privileges`);
const workerTimeControlMail = await models.WorkerTimeControlMail.findOne({
where: {
workerFk: args.workerId,
@ -60,8 +64,6 @@ module.exports = Self => {
const oldState = workerTimeControlMail.state;
const oldReason = workerTimeControlMail.reason;
if (oldState == args.state) throw new UserError('Already has this status');
await workerTimeControlMail.updateAttributes({
state: args.state,
reason: args.reason || null

View File

@ -14,6 +14,9 @@
"year": {
"type": "number"
},
"month": {
"type": "number"
},
"week": {
"type": "number"
}

View File

@ -8,6 +8,7 @@ module.exports = Self => {
require('../methods/worker-time-control/sendMail')(Self);
require('../methods/worker-time-control/updateWorkerTimeControlMail')(Self);
require('../methods/worker-time-control/weeklyHourRecordEmail')(Self);
require('../methods/worker-time-control/getMailStates')(Self);
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')

View File

@ -25,11 +25,11 @@
<div class="totalBox vn-mb-sm" style="text-align: center;">
<h6>{{'Contract' | translate}} #{{$ctrl.businessId}}</h6>
<div>
{{'Used' | translate}} {{$ctrl.contractHolidays.holidaysEnjoyed || 0}}
{{'Used' | translate}} {{$ctrl.contractHolidays.holidaysEnjoyed || 0}}
{{'of' | translate}} {{$ctrl.contractHolidays.totalHolidays || 0}} {{'days' | translate}}
</div>
<div>
{{'Spent' | translate}} {{$ctrl.contractHolidays.hoursEnjoyed || 0}}
{{'Spent' | translate}} {{$ctrl.contractHolidays.hoursEnjoyed || 0}}
{{'of' | translate}} {{$ctrl.contractHolidays.totalHours || 0}} {{'hours' | translate}}
</div>
<div>
@ -40,11 +40,11 @@
<div class="totalBox" style="text-align: center;">
<h6>{{'Year' | translate}} {{$ctrl.year}}</h6>
<div>
{{'Used' | translate}} {{$ctrl.yearHolidays.holidaysEnjoyed || 0}}
{{'Used' | translate}} {{$ctrl.yearHolidays.holidaysEnjoyed || 0}}
{{'of' | translate}} {{$ctrl.yearHolidays.totalHolidays || 0}} {{'days' | translate}}
</div>
<div>
{{'Spent' | translate}} {{$ctrl.yearHolidays.hoursEnjoyed || 0}}
{{'Spent' | translate}} {{$ctrl.yearHolidays.hoursEnjoyed || 0}}
{{'of' | translate}} {{$ctrl.yearHolidays.totalHours || 0}} {{'hours' | translate}}
</div>
</div>
@ -66,7 +66,7 @@
order="businessFk DESC"
limit="5">
<tpl-item>
<div>#{{businessFk}}</div>
<div>#{{businessFk}}</div>
<div class="text-caption text-secondary">
{{started | date: 'dd/MM/yyyy'}} - {{ended ? (ended | date: 'dd/MM/yyyy') : 'Indef.'}}
</div>
@ -87,17 +87,17 @@
<vn-chip>
<vn-avatar class="festive">
</vn-avatar>
<span translate>Festive</span>
<span translate>Festive</span>
</vn-chip>
<vn-chip>
<vn-avatar class="today">
</vn-avatar>
<span translate>Current day</span>
<span translate>Current day</span>
</vn-chip>
</div>
</div>
</vn-side-menu>
<vn-confirm
<vn-confirm
vn-id="confirm"
message="This item will be deleted"
question="Are you sure you want to continue?">

View File

@ -1,7 +1,7 @@
<vn-crud-model
vn-id="model"
url="WorkerTimeControls/filter"
filter="::$ctrl.filter"
filter="::$ctrl.filter"
data="$ctrl.hours">
</vn-crud-model>
<vn-card class="vn-pa-lg vn-w-lg">
@ -16,7 +16,7 @@
{{::weekday.dated | date: 'MMMM'}}
</span>
</div>
<vn-chip
<vn-chip
title="{{::weekday.event.name}}"
ng-class="{invisible: !weekday.event}">
<vn-avatar
@ -66,7 +66,7 @@
</vn-td>
</vn-tr>
<vn-tr>
<vn-td center ng-repeat="weekday in $ctrl.weekDays">
<vn-td center ng-repeat="weekday in $ctrl.weekDays">
<vn-icon-button
icon="add_circle"
vn-tooltip="Add time"
@ -78,27 +78,43 @@
</vn-table>
</vn-card>
<vn-button-bar class="vn-pa-xs vn-w-lg">
<vn-button
label="Satisfied"
<vn-button-bar ng-show="$ctrl.state" class="vn-w-lg">
<vn-button
label="Satisfied"
disabled="$ctrl.state == 'CONFIRMED'"
ng-if="$ctrl.isHimSelf"
ng-click="$ctrl.isSatisfied()">
</vn-button>
<vn-button
label="Not satisfied"
<vn-button
label="Not satisfied"
disabled="$ctrl.state == 'REVISE'"
ng-if="$ctrl.isHimSelf"
ng-click="reason.show()">
</vn-button>
<vn-button
label="Reason"
ng-if="$ctrl.reason && ($ctrl.isHimSelf || $ctrl.isHr)"
ng-click="reason.show()">
</vn-button>
<vn-button
label="Resend"
ng-click="sendEmailConfirmation.show()"
class="right"
vn-tooltip="Resend email of this week to the user"
ng-show="::$ctrl.isHr">
</vn-button>
</vn-button-bar>
<vn-side-menu side="right">
<div class="vn-pa-md">
<div class="totalBox" style="text-align: center;">
<h6 translate>Hours</h6>
<vn-label-value
label="Week total"
<vn-label-value
label="Week total"
value="{{$ctrl.weekTotalHours}} h.">
</vn-label-value>
<vn-label-value
label="Finish at"
<vn-label-value
label="Finish at"
value="{{$ctrl.getFinishTime()}}">
</vn-label-value>
</div>
@ -106,6 +122,8 @@
vn-id="calendar"
class="vn-pt-md"
ng-model="$ctrl.date"
format-week="$ctrl.formatWeek($element)"
on-move="$ctrl.getMailStates($date)"
has-events="$ctrl.hasEvents($day)">
</vn-calendar>
</div>
@ -166,15 +184,31 @@
vn-id="reason"
on-accept="$ctrl.isUnsatisfied()">
<tpl-body>
<vn-textarea
label="Reason"
ng-model="$ctrl.reason"
required="true"
rows="3">
</vn-textarea>
<div class="reasonDialog">
<vn-textarea
label="Reason"
ng-model="$ctrl.reason"
disabled="!$ctrl.isHimSelf"
rows="5"
required="true">
</vn-textarea>
</div>
</tpl-body>
<tpl-buttons>
<tpl-buttons ng-if="$ctrl.isHimSelf">
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Save</button>
</tpl-buttons>
</vn-dialog>
</vn-dialog>
<vn-dialog
vn-id="sendEmailConfirmation"
on-accept="$ctrl.resendEmail()"
message="Send time control email">
<tpl-body style="min-width: 500px;">
<span translate>Are you sure you want to send it?</span>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>

View File

@ -1,6 +1,7 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
import UserError from 'core/lib/user-error';
class Controller extends Section {
constructor($element, $, vnWeekDays) {
@ -24,12 +25,31 @@ class Controller extends Section {
}
this.date = initialDate;
this.getMailStates(this.date);
}
get isHr() {
return this.aclService.hasAny(['hr']);
}
get isHimSelf() {
const userId = window.localStorage.currentUserWorkerId;
return userId == this.$params.id;
}
get worker() {
return this._worker;
}
get weekNumber() {
return this.getWeekNumber(this.date);
}
set weekNumber(value) {
this._weekNumber = value;
}
set worker(value) {
this._worker = value;
}
@ -68,6 +88,27 @@ class Controller extends Section {
}
this.fetchHours();
this.getWeekData();
}
getWeekData() {
const filter = {
where: {
workerFk: this.$params.id,
year: this._date.getFullYear(),
week: this.getWeekNumber(this._date)
}
};
this.$http.get('WorkerTimeControlMails', {filter})
.then(res => {
const workerTimeControlMail = res.data;
if (!workerTimeControlMail.length) {
this.state = null;
return;
}
this.state = workerTimeControlMail[0].state;
this.reason = workerTimeControlMail[0].reason;
});
}
/**
@ -294,42 +335,56 @@ class Controller extends Section {
this.$.editEntry.show($event);
}
getWeekNumber(currentDate) {
const startDate = new Date(currentDate.getFullYear(), 0, 1);
let days = Math.floor((currentDate - startDate) /
(24 * 60 * 60 * 1000));
return Math.ceil(days / 7);
getWeekNumber(date) {
const tempDate = new Date(date);
let dayOfWeek = tempDate.getDay();
dayOfWeek = (dayOfWeek === 0) ? 7 : dayOfWeek;
const firstDayOfWeek = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate() - (dayOfWeek - 1));
const firstDayOfYear = new Date(tempDate.getFullYear(), 0, 1);
const differenceInMilliseconds = firstDayOfWeek.getTime() - firstDayOfYear.getTime();
const weekNumber = Math.floor(differenceInMilliseconds / (1000 * 60 * 60 * 24 * 7)) + 1;
return weekNumber;
}
isSatisfied() {
const weekNumber = this.getWeekNumber(this.date);
const params = {
workerId: this.worker.id,
year: this.date.getFullYear(),
week: weekNumber,
week: this.weekNumber,
state: 'CONFIRMED'
};
const query = `WorkerTimeControls/updateWorkerTimeControlMail`;
this.$http.post(query, params).then(() => {
this.getMailStates(this.date);
this.getWeekData();
this.vnApp.showSuccess(this.$t('Data saved!'));
});
}
isUnsatisfied() {
const weekNumber = this.getWeekNumber(this.date);
if (!this.reason) throw new UserError(`You must indicate a reason`);
const params = {
workerId: this.worker.id,
year: this.date.getFullYear(),
week: weekNumber,
week: this.weekNumber,
state: 'REVISE',
reason: this.reason
};
const query = `WorkerTimeControls/updateWorkerTimeControlMail`;
this.$http.post(query, params).then(() => {
this.getMailStates(this.date);
this.getWeekData();
this.vnApp.showSuccess(this.$t('Data saved!'));
});
}
changeState(state, reason) {
this.state = state;
this.reason = reason;
this.repaint();
}
save() {
try {
const entry = this.selectedRow;
@ -345,6 +400,77 @@ class Controller extends Section {
this.vnApp.showError(this.$t(e.message));
}
}
resendEmail() {
const timestamp = this.date.getTime() / 1000;
const url = `${window.location.origin}/#!/worker/${this.worker.id}/time-control?timestamp=${timestamp}`;
const params = {
recipient: this.worker.user.emailUser.email,
week: this.weekNumber,
year: this.date.getFullYear(),
url: url,
};
this.$http.post(`WorkerTimeControls/weekly-hour-hecord-email`, params)
.then(() => {
this.vnApp.showSuccess(this.$t('Email sended'));
});
}
getTime(timeString) {
const [hours, minutes, seconds] = timeString.split(':');
return [parseInt(hours), parseInt(minutes), parseInt(seconds)];
}
getMailStates(date) {
const params = {
month: date.getMonth() + 1,
year: date.getFullYear()
};
const query = `WorkerTimeControls/${this.$params.id}/getMailStates`;
this.$http.get(query, {params})
.then(res => {
this.workerTimeControlMails = res.data;
this.repaint();
});
}
formatWeek($element) {
const weekNumberHTML = $element.firstElementChild;
const weekNumberValue = weekNumberHTML.innerHTML;
if (!this.workerTimeControlMails) return;
const workerTimeControlMail = this.workerTimeControlMails.find(
workerTimeControlMail => workerTimeControlMail.week == weekNumberValue
);
if (!workerTimeControlMail) return;
const state = workerTimeControlMail.state;
if (state == 'CONFIRMED') {
weekNumberHTML.classList.remove('revise');
weekNumberHTML.classList.remove('sended');
weekNumberHTML.classList.add('confirmed');
weekNumberHTML.setAttribute('title', 'Conforme');
}
if (state == 'REVISE') {
weekNumberHTML.classList.remove('confirmed');
weekNumberHTML.classList.remove('sended');
weekNumberHTML.classList.add('revise');
weekNumberHTML.setAttribute('title', 'No conforme');
}
if (state == 'SENDED') {
weekNumberHTML.classList.add('sended');
weekNumberHTML.setAttribute('title', 'Pendiente');
}
}
repaint() {
let calendars = this.element.querySelectorAll('vn-calendar');
for (let calendar of calendars)
calendar.$ctrl.repaint();
}
}
Controller.$inject = ['$element', '$scope', 'vnWeekDays'];

View File

@ -5,12 +5,14 @@ describe('Component vnWorkerTimeControl', () => {
let $scope;
let $element;
let controller;
let $httpParamSerializer;
beforeEach(ngModule('worker'));
beforeEach(inject(($componentController, $rootScope, $stateParams, _$httpBackend_) => {
beforeEach(inject(($componentController, $rootScope, $stateParams, _$httpBackend_, _$httpParamSerializer_) => {
$stateParams.id = 1;
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
$scope = $rootScope.$new();
$element = angular.element('<vn-worker-time-control></vn-worker-time-control>');
controller = $componentController('vnWorkerTimeControl', {$element, $scope});
@ -82,6 +84,9 @@ describe('Component vnWorkerTimeControl', () => {
$httpBackend.whenRoute('GET', 'Workers/:id/getWorkedHours')
.respond(response);
$httpBackend.whenRoute('GET', 'WorkerTimeControlMails')
.respond([]);
today.setHours(0, 0, 0, 0);
let weekOffset = today.getDay() - 1;
@ -97,7 +102,6 @@ describe('Component vnWorkerTimeControl', () => {
controller.ended = ended;
controller.getWorkedHours(controller.started, controller.ended);
$httpBackend.flush();
expect(controller.weekDays.length).toEqual(7);
@ -152,5 +156,120 @@ describe('Component vnWorkerTimeControl', () => {
expect(controller.date.toDateString()).toEqual(date.toDateString());
});
});
describe('getWeekData() ', () => {
it(`should make a query an then update the state and reason`, () => {
const today = Date.vnNew();
const response = [
{
state: 'SENDED',
reason: null
}
];
controller._date = today;
$httpBackend.whenRoute('GET', 'WorkerTimeControlMails')
.respond(response);
controller.getWeekData();
$httpBackend.flush();
expect(controller.state).toBe('SENDED');
expect(controller.reason).toBe(null);
});
});
describe('isSatisfied() ', () => {
it(`should make a query an then call three methods`, () => {
const today = Date.vnNew();
jest.spyOn(controller, 'getWeekData').mockReturnThis();
jest.spyOn(controller, 'getMailStates').mockReturnThis();
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$.model = {applyFilter: jest.fn().mockReturnValue(Promise.resolve())};
controller.worker = {id: 1};
controller.date = today;
controller.weekNumber = 1;
$httpBackend.expect('POST', 'WorkerTimeControls/updateWorkerTimeControlMail').respond();
controller.isSatisfied();
$httpBackend.flush();
expect(controller.getMailStates).toHaveBeenCalledWith(controller.date);
expect(controller.getWeekData).toHaveBeenCalled();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('isUnsatisfied() ', () => {
it(`should throw an error is reason is empty`, () => {
let error;
try {
controller.isUnsatisfied();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`You must indicate a reason`);
});
it(`should make a query an then call three methods`, () => {
const today = Date.vnNew();
jest.spyOn(controller, 'getWeekData').mockReturnThis();
jest.spyOn(controller, 'getMailStates').mockReturnThis();
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$.model = {applyFilter: jest.fn().mockReturnValue(Promise.resolve())};
controller.worker = {id: 1};
controller.date = today;
controller.weekNumber = 1;
controller.reason = 'reason';
$httpBackend.expect('POST', 'WorkerTimeControls/updateWorkerTimeControlMail').respond();
controller.isSatisfied();
$httpBackend.flush();
expect(controller.getMailStates).toHaveBeenCalledWith(controller.date);
expect(controller.getWeekData).toHaveBeenCalled();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('resendEmail() ', () => {
it(`should make a query an then call showSuccess method`, () => {
const today = Date.vnNew();
jest.spyOn(controller, 'getWeekData').mockReturnThis();
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$.model = {applyFilter: jest.fn().mockReturnValue(Promise.resolve())};
controller.worker = {id: 1};
controller.worker = {user: {emailUser: {email: 'employee@verdnatura.es'}}};
controller.date = today;
controller.weekNumber = 1;
$httpBackend.expect('POST', 'WorkerTimeControls/weekly-hour-hecord-email').respond();
controller.resendEmail();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('getMailStates() ', () => {
it(`should make a query an then call showSuccess method`, () => {
const today = Date.vnNew();
jest.spyOn(controller, 'repaint').mockReturnThis();
controller.$params = {id: 1};
$httpBackend.expect('GET', `WorkerTimeControls/1/getMailStates?month=1&year=2001`).respond();
controller.getMailStates(today);
$httpBackend.flush();
expect(controller.repaint).toHaveBeenCalled();
});
});
});
});

View File

@ -13,4 +13,10 @@ Entry removed: Fichada borrada
The entry type can't be empty: El tipo de fichada no puede quedar vacía
Satisfied: Conforme
Not satisfied: No conforme
Reason: Motivo
Reason: Motivo
Resend: Reenviar
Email sended: Email enviado
You must indicate a reason: Debes indicar un motivo
Send time control email: Enviar email control horario
Are you sure you want to send it?: ¿Seguro que quieres enviarlo?
Resend email of this week to the user: Reenviar email de esta semana al usuario

View File

@ -14,7 +14,7 @@ vn-worker-time-control {
align-items: center;
justify-content: center;
padding: 4px 0;
& > vn-icon {
color: $color-font-secondary;
padding-right: 1px;
@ -24,8 +24,29 @@ vn-worker-time-control {
.totalBox {
max-width: none
}
}
.reasonDialog{
min-width: 500px;
}
.edit-time-entry {
width: 200px
}
}
.right {
float: right;
}
.confirmed {
color: #97B92F;
}
.revise {
color: #f61e1e;
}
.sended {
color: #d19b25;
}