From efabaa4cf3103a5821b8815678bb04fc8d5ff7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Sun, 1 Aug 2021 13:44:08 +0200 Subject: [PATCH] Tickets load level --- IDEAS.md | 36 ++ app/Enums/LoadLevel.php | 35 ++ app/Lib/Utils.php | 68 +++ app/Providers/AppServiceProvider.php | 2 + config/app.php | 1 + config/tickets.php | 8 + resources/sass/_bootswatch.scss | 438 ++++++++++---------- resources/sass/_variables.scss | 40 +- resources/sass/app.scss | 5 + resources/views/layout/nav.blade.php | 6 +- resources/views/modules/loadLevel.blade.php | 16 + resources/views/ticket/index.blade.php | 2 +- 12 files changed, 414 insertions(+), 243 deletions(-) create mode 100644 app/Enums/LoadLevel.php create mode 100644 config/tickets.php create mode 100644 resources/views/modules/loadLevel.blade.php diff --git a/IDEAS.md b/IDEAS.md index a0cddf1..20bfe57 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -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 + diff --git a/app/Enums/LoadLevel.php b/app/Enums/LoadLevel.php new file mode 100644 index 0000000..0ceee0d --- /dev/null +++ b/app/Enums/LoadLevel.php @@ -0,0 +1,35 @@ + 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') + ]; + } +} diff --git a/app/Lib/Utils.php b/app/Lib/Utils.php index 419eaee..a977e4f 100644 --- a/app/Lib/Utils.php +++ b/app/Lib/Utils.php @@ -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(); + } + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 90586d2..e795128 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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() ] diff --git a/config/app.php b/config/app.php index 1c43c85..8b1a654 100644 --- a/config/app.php +++ b/config/app.php @@ -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, ], ]; diff --git a/config/tickets.php b/config/tickets.php new file mode 100644 index 0000000..3e5600e --- /dev/null +++ b/config/tickets.php @@ -0,0 +1,8 @@ + 37, + 'workedDaysInAWeek' => 5, + 'workingDevsPerWeek' => 2, + 'avgTimeForTicket' => 0.95, // 0.95 heure +]; diff --git a/resources/sass/_bootswatch.scss b/resources/sass/_bootswatch.scss index a8748ac..fd73052 100644 --- a/resources/sass/_bootswatch.scss +++ b/resources/sass/_bootswatch.scss @@ -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,"); - } + .btn-close { + background-image: url("data:image/svg+xml,"); + } } diff --git a/resources/sass/_variables.scss b/resources/sass/_variables.scss index 807b2e4..b937c82 100644 --- a/resources/sass/_variables.scss +++ b/resources/sass/_variables.scss @@ -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; diff --git a/resources/sass/app.scss b/resources/sass/app.scss index b6c2081..e8c06d2 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -16,6 +16,11 @@ a.col_sort { max-width: 1340px !important; } +// background color +body { + background-color: $body-bg; +} + // FOOTER body { position: relative; diff --git a/resources/views/layout/nav.blade.php b/resources/views/layout/nav.blade.php index 36482f6..799b266 100644 --- a/resources/views/layout/nav.blade.php +++ b/resources/views/layout/nav.blade.php @@ -28,7 +28,7 @@ @endif diff --git a/resources/views/modules/loadLevel.blade.php b/resources/views/modules/loadLevel.blade.php new file mode 100644 index 0000000..e5e9987 --- /dev/null +++ b/resources/views/modules/loadLevel.blade.php @@ -0,0 +1,16 @@ +{{ __('Charge') }} :  + @switch(Utils::computeWorkload()) + @case(LoadLevel::weak()) + {{ Utils::computeWorkload()->label }} + @break + @case(LoadLevel::stable()) + {{ Utils::computeWorkload()->label }} + @break + @case(LoadLevel::high()) + {{ Utils::computeWorkload()->label }} + @break + @case(LoadLevel::overload()) + {{ Utils::computeWorkload()->label }} + @break + @endswitch + diff --git a/resources/views/ticket/index.blade.php b/resources/views/ticket/index.blade.php index 1a66129..49b1144 100644 --- a/resources/views/ticket/index.blade.php +++ b/resources/views/ticket/index.blade.php @@ -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 - {{ $ticket->status->label }} + {{ $ticket->status->label }} {{-- Modal du ticket --}}