Tickets load level

This commit is contained in:
Jérémy Munsch 2021-08-01 13:44:08 +02:00
parent 46d77f5303
commit efabaa4cf3
12 changed files with 414 additions and 243 deletions

View file

@ -4,3 +4,39 @@
- ~~parse de @user + envoie de mail si un seul user est trouvé~~
- permettre de modifier son mot de passe ? (je crois que ça n'as pas été mis exprès)
- ~~parser les liens http~~
Algorithme charge de travail ticket
delais 7 jours 168h
travaillé 5 jours 37h
temps tickets 1 jours 7.4h
personnes 4 (TpP) 3.7h / semaine * prs
délais moyen ticket (Dmt) 0.75h
délais max attente pour 1 ticket (Dam) 7jours
niveaux de charge 4: - faible
- stable
- élevée
- saturation
états tickets(Et) : - nouveaux (non traités)
- en attente (charge pendante)
- traités
Algo : linéaire
TpP : 3.7h
P : 4
Et(nvx) * Dam
Nv Charge = (P * TpP) / (Et(nvx) * Dam) - (P * TpP) / (Et(nvx) * DmT) - (Et(att) * DmT)
Si nv Charge < 0 => saturation

35
app/Enums/LoadLevel.php Normal file
View file

@ -0,0 +1,35 @@
<?php
namespace App\Enums;
use Kwaadpepper\Enum\BaseEnum;
/**
* @method static self weak()
* @method static self stable()
* @method static self high()
* @method static self overload()
*/
class LoadLevel extends BaseEnum
{
protected static function values(): array
{
return [
'weak' => 0,
'stable' => 1,
'high' => 2,
'overload' => 3
];
}
protected static function labels(): array
{
return [
'weak' => trans('Faible'),
'stable' => trans('Stable'),
'high' => trans('Élevée'),
'overload' => trans('Surcharge')
];
}
}

View file

@ -2,6 +2,9 @@
namespace App\Lib;
use App\Enums\LoadLevel;
use Illuminate\Support\Facades\View;
class Utils
{
public static function plainToHtml(string $text)
@ -39,4 +42,69 @@ class Utils
$m = [];
return \preg_match('/(\.png|\.jpg|\.jpeg|\.gif)$/', $url, $m) === 1;
}
/**
* delais 7 jours 168h
* travaillé 5 jours 37h
* temps tickets 1 jours 7.4h
*
* personnes 4 (TpP) 3.7h / semaine * prs
*
* délais moyen ticket (Dmt) 0.75h
*
* délais max attente pour 1 ticket (Dam) 7jours
*
* niveaux de charge 4: - faible
* - stable
* - élevée
* - saturation
*
* états tickets(Et) : - nouveaux (non traités)
* - en attente (charge pendante)
* - traités
*
* Algo : linéaire
*
* TpP : 3.7h
* P : 4
*
* Et(nvx) * Dam
*
* Nv Charge = (P * TpP) / (Et(nvx) * Dam) - (P * TpP) / (Et(nvx) * DmT) - (Et(att) * DmT)
*
*
* Si nv Charge < 0 => saturation
*
* @return void
*/
public static function computeWorkload(): LoadLevel
{
$Tnew = View::shared('ticketsNew');
$Tprogress = View::shared('ticketsInProgress');
$Twaiting = View::shared('ticketsWaiting');
$worked = config('tickets.workedHoursInAWeek');
$workedDays = config('tickets.workedDaysInAWeek');
$morningWorkTime = ($worked / $workedDays / 2); // in hours
$workingDevsPerWeek = config('tickets.workingDevsPerWeek'); // Devs handlings tickets per week
$timePerWeekDev = $workingDevsPerWeek * $morningWorkTime; // Worktime per week to handle tickets
$avgTimeForTicket = config('tickets.avgTimeForTicket'); // 45min for a ticket
$todoListDelay = $Tnew * $avgTimeForTicket; // The time needed to handle new ticket (average)
$waitingListDelay = $Twaiting * $avgTimeForTicket; // The time needed to handle new ticket (average)
$progressListDelay = $Tprogress * $avgTimeForTicket; // The time needed to handle new ticket (average)
$totalTicketsDelay = $todoListDelay + $progressListDelay + $waitingListDelay;
$ETAForAnewTicket = $totalTicketsDelay / $timePerWeekDev; // in weeks
switch ((int)round($ETAForAnewTicket)) {
case LoadLevel::weak()->value:
case LoadLevel::stable()->value:
case LoadLevel::high()->value:
return LoadLevel::make((int)round($ETAForAnewTicket));
default:
return LoadLevel::overload();
}
}
}

View file

@ -41,6 +41,7 @@ class AppServiceProvider extends ServiceProvider
View::share('license', $appInfos->license);
View::share('ticketsNew', Ticket::where('status', TicketStatus::new())->count());
View::share('ticketsWaiting', Ticket::where('status', TicketStatus::waiting())->count());
View::share('ticketsInProgress', Ticket::whereNotIn(
'status',
[
@ -49,6 +50,7 @@ class AppServiceProvider extends ServiceProvider
TicketStatus::estimateValidated(),
TicketStatus::toPlan(),
TicketStatus::planned(),
TicketStatus::waiting(),
TicketStatus::production(),
TicketStatus::archived()
]

View file

@ -233,6 +233,7 @@ return [
'CrudType' => App\Enums\CrudType::class,
'Role' => App\Enums\Role::class,
'FileType' => App\Enums\FileType::class,
'LoadLevel' => App\Enums\LoadLevel::class,
],
];

8
config/tickets.php Normal file
View file

@ -0,0 +1,8 @@
<?php
return [
'workedHoursInAWeek' => 37,
'workedDaysInAWeek' => 5,
'workingDevsPerWeek' => 2,
'avgTimeForTicket' => 0.95, // 0.95 heure
];

View file

@ -1,331 +1,331 @@
// Lumen 5.0.2
// Bootswatch
// Variables
@import 'variables';
@import "variables";
$web-font-path: "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,300;0,400;0,700;1,400&display=swap" !default;
@if $web-font-path {
@import url($web-font-path);
@import url($web-font-path);
}
// Mixins
@mixin shadow($width: 4px){
border-style: solid;
border-width: 0 1px $width 1px;
@mixin shadow($width: 4px) {
border-style: solid;
border-width: 0 1px $width 1px;
}
// Navbar
.navbar {
@include shadow();
@include shadow();
@each $color, $value in $theme-colors {
&.bg-#{$color} {
border-color: shade-color($value, 10%);
@each $color, $value in $theme-colors {
&.bg-#{$color} {
border-color: shade-color($value, 10%);
}
}
}
}
// Buttons
.btn {
@include shadow();
text-transform: uppercase;
@include shadow();
text-transform: uppercase;
&:not(.disabled):hover {
margin-top: 1px;
border-bottom-width: 3px;
}
&:not(.disabled):active {
margin-top: 2px;
border-bottom-width: 2px;
@include box-shadow(none);
}
@each $color, $value in $theme-colors {
&-#{$color} {
&:hover,
&:active,
&:focus {
background-color: $value;
}
&,
&:not(.disabled):hover,
&:not(.disabled):active,
&:focus {
border-color: shade-color($value, 10%);
}
&:not(.disabled):hover {
margin-top: 1px;
border-bottom-width: 3px;
}
&:not(.disabled):active {
margin-top: 2px;
border-bottom-width: 2px;
@include box-shadow(none);
}
@each $color, $value in $theme-colors {
&-#{$color} {
&:hover,
&:active,
&:focus {
background-color: $value;
}
&,
&:not(.disabled):hover,
&:not(.disabled):active,
&:focus {
border-color: shade-color($value, 10%);
}
}
}
}
}
[class*="btn-outline"] {
border-top-width: 1px;
border-top-width: 1px;
}
.btn-group-vertical {
.btn + .btn {
&:hover {
margin-top: -1px;
border-top-width: 1px;
}
.btn + .btn {
&:hover {
margin-top: -1px;
border-top-width: 1px;
}
&:active {
margin-top: -1px;
border-top-width: 2px;
&:active {
margin-top: -1px;
border-top-width: 2px;
}
}
}
}
// Typography
.text-secondary {
color: $gray-700 !important;
color: $gray-700 !important;
}
.blockquote-footer {
color: $gray-600;
color: $gray-600;
}
// Forms
.form-control {
box-shadow: inset 0 2px 0 rgba(0, 0, 0, .075);
box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.075);
}
.input-group-sm {
> .input-group-prepend,
> .input-group-append {
.btn {
font-size: $btn-font-size-sm;
> .input-group-prepend,
> .input-group-append {
.btn {
font-size: $btn-font-size-sm;
}
}
}
}
// Navs
.nav {
.open > a,
.open > a:hover,
.open > a:focus {
border-color: transparent;
}
.open > a,
.open > a:hover,
.open > a:focus {
border-color: transparent;
}
}
.nav-tabs {
.nav-link {
color: $body-color;
.nav-link {
color: $body-color;
&,
&.disabled,
&.disabled:hover,
&.disabled:focus {
margin-top: 6px;
border-color: $nav-tabs-border-color;
transition: padding-bottom .2s ease-in-out, margin-top .2s ease-in-out, border-bottom .2s ease-in-out;
&,
&.disabled,
&.disabled:hover,
&.disabled:focus {
margin-top: 6px;
border-color: $nav-tabs-border-color;
transition: padding-bottom 0.2s ease-in-out,
margin-top 0.2s ease-in-out, border-bottom 0.2s ease-in-out;
}
&:not(.disabled):hover,
&:not(.disabled):focus,
&.active {
padding-bottom: add(0.5rem, 6px);
border-bottom-color: transparent;
margin-top: 0;
}
}
&:not(.disabled):hover,
&:not(.disabled):focus,
&.active {
padding-bottom: add(.5rem, 6px);
border-bottom-color: transparent;
margin-top: 0;
&.nav-justified > li {
vertical-align: bottom;
}
}
&.nav-justified > li {
vertical-align: bottom;
}
}
.dropdown-menu {
margin-top: 0;
@include shadow();
border-top-width: 1px;
@include box-shadow(none);
margin-top: 0;
@include shadow();
border-top-width: 1px;
@include box-shadow(none);
}
.breadcrumb {
border-color: shade-color($breadcrumb-bg, 10%);
@include shadow();
border-color: shade-color($breadcrumb-bg, 10%);
@include shadow();
}
.pagination {
> li > a,
> li > span {
position: relative;
top: 0;
@include shadow();
color: $pagination-color;
font-weight: 700;
text-transform: uppercase;
> li > a,
> li > span {
position: relative;
top: 0;
@include shadow();
color: $pagination-color;
font-weight: 700;
text-transform: uppercase;
&:hover,
&:focus {
top: 1px;
border-bottom-width: 3px;
text-decoration: none;
&:hover,
&:focus {
top: 1px;
border-bottom-width: 3px;
text-decoration: none;
}
&:active {
top: 2px;
border-bottom-width: 2px;
}
}
&:active {
top: 2px;
border-bottom-width: 2px;
}
}
> .disabled > a,
> .disabled > span {
&:hover {
top: 0;
@include shadow();
}
> .disabled > a,
> .disabled > span {
&:hover {
top: 0;
@include shadow();
&:active {
top: 0;
@include shadow();
}
}
&:active {
top: 0;
@include shadow();
}
}
}
.pager {
> li > a,
> li > span,
> .disabled > a,
> .disabled > span {
&,
&:hover,
&:active {
border-left-width: 2px;
border-right-width: 2px;
> li > a,
> li > span,
> .disabled > a,
> .disabled > span {
&,
&:hover,
&:active {
border-left-width: 2px;
border-right-width: 2px;
}
}
}
}
// Indicators
.btn-close {
text-decoration: none;
opacity: .4;
text-decoration: none;
opacity: 0.4;
&:hover,
&:focus {
opacity: 1;
}
&:hover,
&:focus {
opacity: 1;
}
}
.alert {
color: $white;
@include shadow();
&-primary {
background-color: $primary;
border-color: shade-color($primary, 10%);
}
&-secondary {
background-color: $secondary;
border-color: shade-color($secondary, 10%);
}
&-success {
background-color: $success;
border-color: shade-color($success, 10%);
}
&-info {
background-color: $info;
border-color: shade-color($info, 10%);
}
&-danger {
background-color: $danger;
border-color: shade-color($danger, 10%);
}
&-warning {
background-color: $warning;
border-color: shade-color($warning, 10%);
}
&-dark {
background-color: $dark;
border-color: shade-color($dark, 10%);
}
&-light {
background-color: $light;
border-color: shade-color($light, 10%);
}
.alert-link {
font-weight: 400;
color: $white;
text-decoration: underline;
}
@include shadow();
&-secondary,
&-light {
&,
a,
.alert-link {
color: $body-color;
&-primary {
background-color: $primary;
border-color: shade-color($primary, 10%);
}
&-secondary {
background-color: $secondary;
border-color: shade-color($secondary, 10%);
}
&-success {
background-color: $success;
border-color: shade-color($success, 10%);
}
&-info {
background-color: $info;
border-color: shade-color($info, 10%);
}
&-danger {
background-color: $danger;
border-color: shade-color($danger, 10%);
}
&-warning {
background-color: $warning;
border-color: shade-color($warning, 10%);
}
&-dark {
background-color: $dark;
border-color: shade-color($dark, 10%);
}
&-light {
background-color: $light;
border-color: shade-color($light, 10%);
}
.alert-link {
font-weight: 400;
color: $white;
text-decoration: underline;
}
&-secondary,
&-light {
&,
a,
.alert-link {
color: $body-color;
}
}
}
}
.badge {
&.bg-secondary,
&.bg-light {
color: $dark;
}
&.bg-secondary,
&.bg-light {
color: $dark;
}
}
// Containers
a.list-group-item {
&-success {
&.active {
background-color: $success;
&-success {
&.active {
background-color: $success;
}
&.active:hover,
&.active:focus {
background-color: shade-color($success, 10%);
}
}
&.active:hover,
&.active:focus {
background-color: shade-color($success, 10%);
}
}
&-warning {
&.active {
background-color: $warning;
}
&-warning {
&.active {
background-color: $warning;
&.active:hover,
&.active:focus {
background-color: shade-color($warning, 10%);
}
}
&.active:hover,
&.active:focus {
background-color: shade-color($warning, 10%);
}
}
&-danger {
&.active {
background-color: $danger;
}
&-danger {
&.active {
background-color: $danger;
&.active:hover,
&.active:focus {
background-color: shade-color($danger, 10%);
}
}
&.active:hover,
&.active:focus {
background-color: shade-color($danger, 10%);
}
}
}
.modal,
.toast {
.btn-close {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>");
}
.btn-close {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>");
}
}

View file

@ -9,48 +9,50 @@ $theme: "lumen" !default;
//
// Color system
//
$lightness: -30%;
$saturation: -9%;
$lightness: -20%;
$saturation: -5%;
@function lowerColor($color) {
@return color.scale($color, $lightness: $lightness, $saturation: $saturation);
}
@function lowerBrightness($color) {
@return color.scale($color, $lightness: $lightness - 20%);
@return color.scale($color, $lightness: $lightness + 10%);
}
$white: #fff !default;
$gray-100: lowerBrightness(#f6f6f6) !default;
$gray-200: lowerBrightness(#f0f0f0) !default;
$white: lowerBrightness(#f1efec) !default;
$gray-100: lowerBrightness(#f8f9fa) !default;
$gray-200: lowerBrightness(#e9ecef) !default;
$gray-300: lowerBrightness(#dee2e6) !default;
$gray-400: lowerBrightness(#ced4da) !default;
$gray-500: lowerBrightness(#adb5bd) !default;
$gray-600: lowerBrightness(#999) !default;
$gray-700: lowerBrightness(#555) !default;
$gray-800: lowerBrightness(#333) !default;
$gray-900: lowerBrightness(#222) !default;
$gray-600: lowerBrightness(#6c757d) !default;
$gray-700: lowerBrightness(#495057) !default;
$gray-800: lowerBrightness(#343a40) !default;
$gray-900: lowerBrightness(#212529) !default;
$black: #000 !default;
$blue: lowerColor(#158cba) !default;
$blue: lowerColor(#337ab7) !default;
$indigo: lowerColor(#6610f2) !default;
$purple: lowerColor(#6f42c1) !default;
$pink: lowerColor(#e83e8c) !default;
$red: lowerColor(#ff4136) !default;
$pink: lowerColor(#B7337A) !default;
$red: lowerColor(#d9534f) !default;
$orange: lowerColor(#fd7e14) !default;
$yellow: lowerColor(#ff851b) !default;
$green: lowerColor(#28b62c) !default;
$yellow: lowerColor(#f0ad4e) !default;
$green: lowerColor(#5cb85c) !default;
$teal: lowerColor(#20c997) !default;
$cyan: lowerColor(#75caeb) !default;
$cyan: lowerColor(#5bc0de) !default;
$primary: $blue !default;
$secondary: $gray-200 !default;
$secondary: $gray-600 !default;
$success: $green !default;
$info: $cyan !default;
$info: $teal !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-100 !default;
$dark: $gray-700 !default;
$dark: $gray-900 !default;
$body-bg: $white;
$min-contrast-ratio: 1.95 !default;

View file

@ -16,6 +16,11 @@ a.col_sort {
max-width: 1340px !important;
}
// background color
body {
background-color: $body-bg;
}
// FOOTER
body {
position: relative;

View file

@ -28,7 +28,7 @@
@endif
<ul class="navbar-nav me-auto">
<li class="nav-item">
<span class="nav-link text-primary fw-bold">{{ __('Tickets') }}</span>
<span class="nav-link text-info fw-bold">{{ __('Tickets') }}</span>
</li>
<li class="nav-item">
<span class="nav-link text-light">{{ __('Nouveau') }}&nbsp;:&nbsp;<span class="text-info">{{ $ticketsNew }}</span></span>
@ -37,9 +37,7 @@
<span class="nav-link text-light">{{ __('En cours') }}&nbsp;:&nbsp;<span class="text-info">{{ $ticketsInProgress }}</span></span>
</li>
<li class="nav-item">
{{-- Mouhahahaha --}}
<span class="nav-link text-light">{{ __('Charge') }}&nbsp;:&nbsp;<span class="text-danger">{{ __('Elevée') }}</span></span>
{{-- <span class="nav-link text-dark">Charge&nbsp;:&nbsp;<span class="text-info">Stable</span></span> --}}
@include('modules.loadLevel')
</li>
</ul>
</div>

View file

@ -0,0 +1,16 @@
<span class="nav-link">{{ __('Charge') }}&nbsp;:&nbsp;
@switch(Utils::computeWorkload())
@case(LoadLevel::weak())
<span class="p-1 rounded bg-dark fw-bold text-light">{{ Utils::computeWorkload()->label }}</span>
@break
@case(LoadLevel::stable())
<span class="p-1 rounded bg-success fw-bold text-dark">{{ Utils::computeWorkload()->label }}</span>
@break
@case(LoadLevel::high())
<span class="p-1 rounded bg-warning fw-bold text-dark">{{ Utils::computeWorkload()->label }}</span>
@break
@case(LoadLevel::overload())
<span class="p-1 rounded bg-danger fw-bold text-dark">{{ Utils::computeWorkload()->label }}</span>
@break
@endswitch
</span>

View file

@ -116,7 +116,7 @@ $userPr = Auth::user()->sites ?? collect([]);
$cls = (!$cls && $ticket->status == TicketStatus::inprogress()) ? 'text-dark' : $cls;
$cls = (!$cls && $ticket->status == TicketStatus::waiting()) ? 'text-white bg-danger' : $cls;
@endphp
<td><span class="{{ $cls }}">{{ $ticket->status->label }}</span></td>
<td><span class="rounded {{ $cls }}">{{ $ticket->status->label }}</span></td>
<td>
{{-- Modal du ticket --}}
<!-- Button trigger modal -->