]> _ Git - fluidbook-toolbox.git/commitdiff
wait #7607 @12
authorVincent Vanwaelscappel <vincent@cubedesigners.com>
Tue, 8 Jul 2025 17:30:17 +0000 (19:30 +0200)
committerVincent Vanwaelscappel <vincent@cubedesigners.com>
Tue, 8 Jul 2025 17:30:17 +0000 (19:30 +0200)
app/Http/Controllers/Admin/ExtranetTotalsController.php [new file with mode: 0644]
app/Jobs/ProcessTotals.php
app/helpers.php
resources/views/extranet/totals.blade.php [new file with mode: 0644]
resources/views/vendor/backpack/base/inc/sidebar_content.blade.php
routes/web.php

diff --git a/app/Http/Controllers/Admin/ExtranetTotalsController.php b/app/Http/Controllers/Admin/ExtranetTotalsController.php
new file mode 100644 (file)
index 0000000..023e5e9
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+
+class ExtranetTotalsController extends Controller
+{
+    protected function index()
+    {
+        $data = cache('extranet_totals');
+        $currentYear = date('Y');
+
+        $chartRevenue = [];
+        $chartRevenuePrevision = [];
+        foreach ($data['years'] as $year => $amount) {
+            $chartRevenue[] = ['year' => $year, 'amount' => $amount];
+            $chartRevenuePrevision[] = ['year', $year, 'amount' => $currentYear == $year ? $data['pendingProjects'] : ($year == $currentYear - 1 ? 0 : ("NaN"))];
+        }
+
+        return view('extranet.totals', ['currentYear'=>$currentYear,'chart_revenue' => $chartRevenue, 'chart_revenue_prevision' => $chartRevenuePrevision, 'data' => $data]);
+    }
+}
index 97de642db56e20be0a7a0a4bbf03ad77473e3003..2782e28034a43561758b4aa1f889755528af8e54 100644 (file)
@@ -7,12 +7,11 @@ use App\Models\FluidbookQuote;
 use App\Models\User;
 use Cubedesigners\UserDatabase\Permissions;
 use Illuminate\Support\Facades\Artisan;
+use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\DB;
 
 class ProcessTotals extends Base
 {
-
-
     protected $projects = [];
     protected $invoices = [];
     protected $invoicesByProject = [];
@@ -23,6 +22,13 @@ class ProcessTotals extends Base
     protected $companyYears = [];
     protected $unpaidYears = [];
     protected $unpaid = [];
+    protected $totalYears = [];
+    protected $totalMonths = [];
+    protected $totalQuarters = [];
+    protected $projects_budget = [];
+    protected $unpaidTotals = [];
+    protected $pendingProjects = 0;
+    protected $totalCategories = [];
     const firstYear = 2006;
 
     protected $lastProject = [];
@@ -31,22 +37,78 @@ class ProcessTotals extends Base
     public function handle()
     {
         $this->processInvoices();
+        $this->processProjects();
         $this->processCompanies();
         $this->processFluidbookCounts();
         Artisan::command('ws:precache', function () {
         });
+
+        cache()->forever('extranet_totals',
+            [
+                'pendingProjects' => $this->pendingProjects,
+                'years' => $this->totalYears,
+                'months' => $this->totalMonths,
+                'quarters' => $this->totalQuarters,
+                'currentYear' => $this->totalYears[date('Y')],
+                'currentYearProjection' => $this->totalYears[date('Y')] + $this->pendingProjects,
+                'unpaid' => $this->unpaidTotals,
+                'categories' => $this->totalCategories,
+            ]
+        );
     }
 
     protected function processInvoices()
     {
+        $d30 = time() - (30 * 3600 * 24);
+        $d90 = time() - (90 * 3600 * 24);
+        $d365 = time() - (365 * 3600 * 24);
+
+        $this->unpaidTotals = ['all' => 0, 'd30' => 0, 'd90' => 0, 'd365' => 0];
+
         foreach (DB::table(self::$_wstable . '.factures')->whereIn('status', [1, 2])->get() as $e) {
+            $year = date('Y', $e->date_creation);
+            $month = date('m', $e->date_creation);
+            $quarter = ceil($month / 3);
             $this->invoices[$e->facture_id] =
-                ['status' => $e->status, 'amount' => $e->total_ht, 'project' => $e->projet, 'year' => date('Y', $e->date_creation), 'paid' => $e->status == 2];
+                ['status' => $e->status, 'amount' => $e->total_ht, 'project' => $e->projet, 'year' => $year, 'month' => $year . '-' . $month, 'quarter' => $year . '-Q' . $quarter, 'paid' => $e->status == 2];
+
+            if ($e->status == 1) {
+                $this->unpaidTotals['all'] += $e->total_ht;
+                if ($e->date_creation < $d30) {
+                    $this->unpaidTotals['d30'] += $e->total_ht;
+                    if ($e->date_creation < $d90) {
+                        $this->unpaidTotals['d90'] += $e->total_ht;
+                        if ($e->date_creation < $d365) {
+                            $this->unpaidTotals['d365'] += $e->total_ht;
+                        }
+                    }
+                }
+            }
+
+            if (!isset($this->totalYears[$year])) {
+                $this->totalYears[$year] = 0;
+            }
+            $this->totalYears[$year] += $e->total_ht;
+            $m = $year . '-' . $month;
+            if (!isset($this->totalMonths[$m])) {
+                $this->totalMonths[$m] = 0;
+            }
+            $this->totalMonths[$m] += $e->total_ht;
+
+            $q = $year . '-Q' . $quarter;
+            if (!isset($this->totalQuarters[$q])) {
+                $this->totalQuarters[$q] = 0;
+            }
+            $this->totalQuarters[$q] += $e->total_ht;
             if (!isset($this->invoicesByProject[$e->projet])) {
                 $this->invoicesByProject[$e->projet] = [];
             }
             $this->invoicesByProject[$e->projet][] = $e->facture_id;
         }
+
+        ksort($this->totalYears);
+        ksort($this->totalMonths);
+        ksort($this->totalQuarters);
     }
 
 
@@ -64,7 +126,6 @@ class ProcessTotals extends Base
         }
 
         foreach (DB::table(self::$_wstable . '.projets')->get() as $e) {
-
             if (!isset($this->companyOfUser[$e->client])) {
                 continue;
             }
@@ -150,4 +211,63 @@ class ProcessTotals extends Base
         }
     }
 
+    protected function processProjects()
+    {
+        $currentYear = date('Y');
+
+        $projectsYears = [];
+        foreach (DB::table(self::$_wstable . '.projets')->get() as $e) {
+            $projectsYears[$e->projet_id] = date('Y', $e->date_creation);
+        }
+
+        foreach (DB::table(self::$_wstable . '.taches')->get() as $e) {
+            if (!isset($this->projects_budget[$e->projet])) {
+                $this->projects_budget[$e->projet] = 0;
+            }
+            if (isset($projectsYears[$e->projet])) {
+                $y = $projectsYears[$e->projet];
+                if (!isset($this->totalCategories[$e->categorie])) {
+                    $this->totalCategories[$e->categorie] = ['total' => 0, 'ratio' => 0, 'years' => []];
+                }
+                if (!isset($this->totalCategories[$e->categorie]['years'][$y])) {
+                    $this->totalCategories[$e->categorie]['years'][$y] = ['amount' => 0, 'ratio' => 0];
+                }
+                $this->totalCategories[$e->categorie]['years'][$y]['amount'] += $e->budget;
+                $this->totalCategories[$e->categorie]['total'] += $e->budget;
+            }
+
+
+            $this->projects_budget[$e->projet] += $e->budget;
+        }
+
+        foreach ($this->totalCategories as $categorie => $c) {
+            if (!$c['total']) {
+                continue;
+            }
+            foreach ($c['years'] as $year => $cc) {
+                $yearTotal = $this->totalYears[$year];
+                if ($year == $currentYear) {
+                    $yearTotal += $this->pendingProjects;
+                }
+                $this->totalCategories[$categorie]['years'][$year]['ratio'] = $cc['amount'] / $yearTotal;
+            }
+        }
+
+        // projects older than 18 month won't be invoices I suppose
+        $limit = time() - (3600 * 24 * 365 * 1.5);
+        foreach (DB::table(self::$_wstable . '.projets')->where('date_creation', '>', $limit)->where('status', 0)->get() as $e) {
+            $alreadyInvoiced = 0;
+            if (isset($this->invoicesByProject[$e->projet_id])) {
+                foreach ($this->invoicesByProject[$e->projet_id] as $invoice_id) {
+                    $alreadyInvoiced += $this->invoices[$invoice_id]['amount'];
+                }
+            }
+
+            $this->pendingProjects += max(0, ($this->projects_budget[$e->projet_id] ?? 0) - $alreadyInvoiced);
+        }
+
+        ksort($this->totalCategories);
+    }
+
+
 }
index 89d02a3979cc35a74ea22353c218f067f180c535..53bd158f1a1eebb139107fd0962458614920bb31 100644 (file)
@@ -23,7 +23,6 @@ if (!function_exists('dddump')) {
 }
 
 
-
 if (!function_exists('us_path')) {
     function us_path($path = '')
     {
@@ -32,8 +31,28 @@ if (!function_exists('us_path')) {
 }
 
 if (!function_exists('us_protected_path')) {
-    function us_protected_path($path='')
+    function us_protected_path($path = '')
     {
         return us_path('protected/' . $path);
     }
 }
+
+if (!function_exists('format_ke')) {
+    function format_ke($number)
+    {
+        if (!$number) {
+            return '-';
+        }
+        return round($number / 1000) . ' K€';
+    }
+}
+
+if (!function_exists('format_pct')) {
+    function format_pct($number)
+    {
+        if (!$number) {
+            return '';
+        }
+        return number_format($number * 100, 2, '.', ',') . ' %';
+    }
+}
diff --git a/resources/views/extranet/totals.blade.php b/resources/views/extranet/totals.blade.php
new file mode 100644 (file)
index 0000000..3ff814b
--- /dev/null
@@ -0,0 +1,214 @@
+{{-- __('!! Extranet') --}}
+@extends(backpack_view('blank'))
+
+@php
+    $categories = array(0 => __('Non défini'), 1 => __('Gestion de projet'),
+        2 => __('Design Web'), 3 => __('Design Industriel'), 4 => __('Print'),
+        5 => __('Newsletter'), 6 => __('Développement PHP'), 7 => __('Développement Flash'),
+        8 => __('Fluidbook'), 9 => __('Formation'), 10 => __('Administratif'), 11 => __('Divers'),
+        12 => __('Intégration HTML'), 13 => __('Motion design'), 14 => __('Design graphique'),
+        15 => __('Bandeaux de pub'), 16 => __('Applications mobiles'), 17 => __('Prise de vue photo/vidéo'), 18 => __('Hébergement'));
+@endphp
+
+@section('content')
+    <table id="crudTable"
+           class="bg-white table table-striped table-hover nowrap rounded shadow-xs border-xs mt-2 dataTable dtr-inline">
+        <thead>
+        <tr role="row">
+            <th colspan="3"><h3
+                    style="position: relative;left:-10px">{{__('Détails de l\'année :year',['year'=>date('Y')])}}</h3>
+            </th>
+            <th>{{__('Impayés')}}</th>
+            <th>{{format_ke($data['unpaid']['all'])}}</th>
+            <th></th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr>
+            <td></td>
+            <td>{{__('Projets en cours')}}</td>
+            <td>{{format_ke($data['pendingProjects'])}}</td>
+            <td>{{__('Impayés de plus de 30 jours')}}</td>
+            <td>{{format_ke($data['unpaid']['d30'])}}</td>
+            <td></td>
+        </tr>
+        <tr>
+            <td></td>
+            <td>{{__('Chiffre d\'affaire')}}</td>
+            <td>{{format_ke($data['currentYear'])}}</td>
+            <td>{{__('Impayés de plus de 90 jours')}}</td>
+            <td>{{format_ke($data['unpaid']['d90'])}}</td>
+            <td></td>
+        </tr>
+        <tr>
+            <td></td>
+            <td>{{__('Prévision de chiffre d\'affaire')}}</td>
+            <td>{{format_ke($data['currentYearProjection'])}}</td>
+            <td>{{__('Impayés de plus d\'un an')}}</td>
+            <td>{{format_ke($data['unpaid']['d365'])}}</td>
+            <td></td>
+        </tr>
+        </tbody>
+    </table>
+
+    <h3>{{__('Évolution du chiffre d\'affaire')}}</h3>
+    <div style="height: 300px; ">
+        <canvas id="revenue-chart"></canvas>
+    </div>
+
+    <table id="crudTable"
+           class="bg-white table table-striped table-hover nowrap rounded shadow-xs border-xs mt-2 dataTable dtr-inline">
+        <thead>
+        <tr role="row">
+            <th colspan="4"><h3
+                    style="position: relative;left:-10px">{{__('Chiffre d\'affaire trimestriel')}}</h3>
+            </th>
+            <th>T1</th>
+            <th>T2</th>
+            <th>T3</th>
+            <th>T4</th>
+            <th><em>{{__('Total')}}</em></th>
+            <th></th>
+        </tr>
+        </thead>
+        <tbody>
+        @for($y=$currentYear;$y>=2009;$y--)
+            <tr>
+                <td></td>
+                <td colspan="3"><strong>{{$y}}</strong></td>
+                @for($i=1;$i<=4;$i++)
+                    <td>{{format_ke($data['quarters'][$y.'-Q'.$i]??0)}}</td>
+                @endfor
+                <td><em>{{format_ke($data['years'][$y])}}</em></td>
+                <td></td>
+            </tr>
+        @endfor
+        </tbody>
+    </table>
+
+    <h3>{{__('Chiffre d\'affaire mensuel')}}</h3>
+    <table id="crudTable"
+           class="bg-white table table-striped table-hover nowrap rounded shadow-xs border-xs mt-2 dataTable dtr-inline"
+           style="table-layout: fixed;">
+        <thead>
+        <tr role="row">
+            <th style="width: 1px"></th>
+            <th style="width: 7.14%;">{{__('Année')}}</th>
+            <th style="width: 7.14%;">{{__('Janvier')}}</th>
+            <th style="width: 7.14%;">{{__('Février')}}</th>
+            <th style="width: 7.14%;">{{__('Mars')}}</th>
+            <th style="width: 7.14%;">{{__('Avril')}}</th>
+            <th style="width: 7.14%;">{{__('Mai')}}</th>
+            <th style="width: 7.14%;">{{__('Juin')}}</th>
+            <th style="width: 7.14%;">{{__('Juillet')}}</th>
+            <th style="width: 7.14%;">{{__('Août')}}</th>
+            <th style="width: 7.14%;">{{__('Septembre')}}</th>
+            <th style="width: 7.14%;">{{__('Octobre')}}</th>
+            <th style="width: 7.14%;">{{__('Novembre')}}</th>
+            <th style="width: 7.14%;">{{__('Décembre')}}</th>
+            <th style="width: 7.14%;"><em>{{__('Total')}}</em></th>
+        </tr>
+        </thead>
+        <tbody>
+        @for($y=$currentYear;$y>=2009;$y--)
+            <tr>
+                <td></td>
+                <td><strong>{{$y}}</strong></td>
+                @for($i=1;$i<=12;$i++)
+                    <td>{{format_ke($data['months'][$y.'-'.($i>=10?$i:'0'.$i)]??0)}}</td>
+                @endfor
+                <td><em>{{format_ke($data['years'][$y])}}</em></td>
+            </tr>
+        @endfor
+        </tbody>
+    </table>
+
+    <h3>{{__('Répartition du chiffre d\'affaire par catégorie')}}</h3>
+    <table id="crudTable"
+           class="bg-white table table-striped table-hover nowrap rounded shadow-xs border-xs mt-2 dataTable dtr-inline"
+           style="table-layout: fixed;">
+        <thead>
+        <tr role="row">
+            <th style="width: 1px"></th>
+            <th style="width: 7.14%;">{{__('Catégorie')}}</th>
+            @for($y=$currentYear-11;$y<=$currentYear;$y++)
+                <th style="width: 7.14%;">{{$y}}</th>
+            @endfor
+            <th style="width: 7.14%;"><em>{{__('Total')}}</em></th>
+        </tr>
+        </thead>
+        <tbody>
+        @foreach($categories as $c=>$label)
+            <tr>
+                <td></td>
+                <td><strong>{{$label}}</strong></td>
+                @for($y=$currentYear-11;$y<=$currentYear;$y++)
+                    <td>{{format_ke($data['categories'][$c]['years'][$y]['amount']??0)}}<br>{{format_pct($data['categories'][$c]['years'][$y]['ratio']??0)}}</td>
+                @endfor
+                <td>{{format_ke($data['categories'][$c]['total']??0)}}</td>
+                <td></td>
+            </tr>
+        @endforeach
+        </tbody>
+    </table>
+@endsection
+
+@push('after_scripts')
+    {{-- Charting library --}}
+    <script src=" https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js "></script>
+    <script>
+        const revenue = @json($chart_revenue);
+        const prevision = @json($chart_revenue_prevision);
+
+        Chart.defaults.font.family = "Source Sans Pro";
+        //=== Render Chart
+        new Chart(
+            document.getElementById('revenue-chart'),
+            {
+                type: 'line',
+                data: {
+                    labels: revenue.map(row => row.year),
+                    datasets: [
+                        {
+                            label: "{!! __('Chiffre d\'affaire annuel')!!}",
+                            data: revenue.map(row => row.amount),
+                            cubicInterpolationMode: 'monotone',
+                            tension: 0.1,
+                            fill: 'origin',
+                        },
+                        {
+                            label: "{!! __('Prévision')!!}",
+                            data: prevision.map(row => row.amount),
+                            cubicInterpolationMode: 'monotone',
+                            tension: 0.1,
+                            fill: 'stack',
+                        }
+                    ]
+
+                },
+                options: {
+                    responsive: true,
+                    maintainAspectRatio: false,
+                    maxBarThickness: 20, // Prevent bars being ridiculously wide when there isn't much data
+                    scales: {
+                        x: {stacked: true},
+                        y: {
+                            type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance
+                            position: 'left',
+                            stacked: true,
+                            min: 0,
+                        },
+                    },
+                    plugins: {
+                        tooltip: {
+                            callbacks: {
+                                label: (item) =>
+                                    `${item.dataset.label}: ${item.formattedValue} €`,
+                            },
+                        },
+                    },
+                }
+            }
+        );
+    </script>
+@endpush
index 68ed51fc1ccd7947aad7c0fa2d11581c35d63922..1797ffbe5bf967355cb2b48a93ffca2f8418f5cb 100644 (file)
         </ul>
     </li>
 @endcan
+@canany(['extranet:totals'])
+    <li {!! sidebarState('extranet') !!}><a class='nav-link nav-dropdown-toggle' href='#'><i
+                class='nav-icon las la-piggy-bank'></i>{{__('Extranet')}}</a>
+        <ul class='nav-dropdown-items'>
+            @can('extranet:totals')
+                <li class='nav-item'><a class='nav-link' href='{{ backpack_url('extranet/totals') }}'><i
+                            class='nav-icon las la-chart-bar'></i>
+                        <span>{{__('Chiffres')}}</span></a></li>
+            @endcan
+        </ul>
+    </li>
+@endcan
 
 @canany(['team-leave:read','team-overtime:read','extranet:manage_emails'])
     <li {!! sidebarState('team') !!}><a class='nav-link nav-dropdown-toggle' href='#'><i
index 7199e38dc284cfae0d911d959331496620de5d6b..68baf6ee5156d62fbfb3dea1c9e201cf870732c4 100644 (file)
@@ -27,6 +27,7 @@ Route::group([
     Route::get('tasks/countUnread', 'TasksController@countUnread');
     Route::match(['get'], 'storage/{path?}', 'StorageController@storage')->where(['path' => '.*']);
     Route::delete('tasks/notification/{id}', 'TasksController@deleteNotification');
+    Route::get('extranet/totals', 'ExtranetTotalsController@index');
 });
 
 Route::group([