iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Modern Web

Laravel Filament 從入門到實戰系列 第 21 篇

Day 20:圖表 Widget,讓庫存與訂單趨勢一眼看懂

  • 分享至 

  • xImage
  •  

Day 19:第一個 Widget,把訂單數字擺上首頁 結尾留下一句提醒,統計數字類型的 Widget 讓管理者一眼看到當下的數字,但這個數字只是當下這個時間點的切片。本月訂單數如果是二十五筆,這二十五筆究竟是比上個月成長還是萎縮,光看這一個數字完全感受不到。

數字知道現在,但看不出來是往上還是往下

具體想像一下這個侷限在管理現場會造成什麼影響。管理者打開後台首頁,看到本月訂單數是二十五筆,這個數字本身不帶任何方向性。它可能是連續成長第三個月的其中一筆紀錄,也可能是剛從谷底回升的第一筆訊號,這兩種情況背後管理者該採取的因應動作完全不同,前者也許該考慮加開產線或增聘客服,後者則可能只是暫時止跌,還不到樂觀的時候。但統計數字本身不會告訴你正站在哪一種情境裡,它只回答了「現在是多少」,回答不了「這是怎麼走到這裡的」。

庫存的情況也是同樣的道理。某個分類的庫存數量看起來還算充足,但如果不知道這個數量是這兩週穩定消耗下來的結果,還是剛進完一批貨的暫時高點,管理者很難判斷該不該提早排下一批進貨。數字停留在原地,變化的過程卻消失了。

今天要做的事情,是在 Day 19 已定案的 Widget 機制之上,補上圖表這一種內容類型,把訂單與庫存資料隨時間變化的樣貌視覺化呈現,讓管理者看得出趨勢,而不只是看到當下的一個數字。

圖表要回答的是趨勢,不是現況

在動手之前,先把統計數字跟圖表這兩種 Widget 類型放在一起比較清楚,這一步能省掉後面很多困惑。直覺上很容易把圖表當成統計數字的另一種畫法,反正都是把資料庫裡的數字端到畫面上,差別只在有沒有畫成曲線。這個直覺低估了兩者的差異。統計數字回答的是「現在是多少」,圖表回答的是「這段時間以來是怎麼變化的」,兩者服務的管理情境不同,統計數字適合快速確認現況,例如打開手機看一眼現在幾點;圖表適合判斷方向,例如看一段時間的體重記錄才知道最近是瘦了還是胖了。統計數字是一張照片,圖表則像是連續播放的影片,影片才看得出動作的方向。

兩者需要的資料形態也完全不同,這是實作上真正的分水嶺。統計數字只需要一個聚合後的單一數值,Order::query()->count() 這類查詢跑出一個結果就結束了。圖表需要的是一系列依時間排列的數值點,例如過去七天,每一天各自對應一筆訂單數,或是過去六個月,每個月各自對應一筆訂單數。這種資料形態的整理,才是圖表 Widget 跟統計數字 Widget 在實作上最大的差異,前者查一次資料庫就夠,後者得把資料依時間分組,變成一組標籤搭配一組數值的陣列。

順帶回答一個常見的疑問,Filament 的圖表 Widget 是不是要自己刻一套繪圖邏輯。答案是不需要,Filament 通常整合現成的圖表函式庫負責實際繪製,開發者要做的主要工作是把資料整理成這個函式庫看得懂的格式,座標軸怎麼畫、線條怎麼描邊,這些前端繪圖細節早就被解決了,不需要重新發明一次。

動手做訂單趨勢圖,近期每天的訂單量變化

先處理訂單趨勢這個示範。跟 Day 19 一樣,透過指令建立骨架,這次帶的參數不同:

php artisan make:filament-widget OrderTrendChart --chart

指令跑完,app/Filament/Widgets 資料夾裡會多出 OrderTrendChart.php,補上依日期分組統計訂單筆數的邏輯:

<?php

namespace App\Filament\Widgets;

use App\Models\Order;
use Filament\Widgets\ChartWidget;

class OrderTrendChart extends ChartWidget
{
    protected static ?string $heading = '近七天訂單趨勢';

    protected function getData(): array
    {
        $days = collect(range(6, 0))->map(
            fn (int $offset) => now()->subDays($offset)->startOfDay()
        );

        $counts = $days->map(function ($day) {
            return Order::query()
                ->whereBetween('created_at', [$day, $day->copy()->endOfDay()])
                ->count();
        });

        return [
            'datasets' => [
                [
                    'label' => '訂單筆數',
                    'data' => $counts->toArray(),
                ],
            ],
            'labels' => $days->map(fn ($day) => $day->format('n/j'))->toArray(),
        ];
    }

    protected function getType(): string
    {
        return 'line';
    }
}

這段程式碼做的事情,是把「依日期分組計算訂單筆數」這個讀者原本就熟悉的聚合查詢寫法,重複跑七次,分別對應近七天的每一天,再把結果整理成 labels 對應 datasets 這種一組標籤搭配一組數值的陣列結構,這正是圖表函式庫看得懂的格式。created_at 沿用的是 Day 19 已經用過的訂單建立時間欄位,沒有引入任何新欄位。getType() 回傳 line,指定用折線圖呈現,訂單量隨天數推進的走勢,用折線最容易看出方向。

寫好之後,同樣放進 app/Filament/Widgets 資料夾,AdminPanelProvider.php 裡 Day 19 已經設定好的 discoverWidgets 會自動掃描到這個新類別,不需要額外註冊。

登入後台首頁驗證,本月訂單數與待處理訂單數這兩張卡片下方,多出一張折線圖,橫軸是近七天的日期,縱軸是當天的訂單筆數。

後台首頁,OrderTrendChart 掛載後畫面,本月訂單數與待處理訂單數卡片下方新增折線圖,橫軸為近七天日期,縱軸為每日訂單筆數

管理者看著這條線,不需要自己在腦中拼湊七個數字的變化,一眼就能看出這七天訂單量是持續往上、原地打轉,還是正在下滑,這正是本月訂單數這張卡片單獨存在時完全給不出的訊息。

動手做庫存分佈圖,各分類的庫存量長條圖

訂單趨勢圖表回答的是「同一個指標隨時間怎麼變化」,接下來這個示範要處理另一種問題,「同一個時間點下,不同分類之間量體差多少」。庫存分佈圖表要呈現的正是這件事,依商品分類分組,統計各分類目前的庫存總量。

php artisan make:filament-widget InventoryByCategoryChart --chart

補上依分類分組加總庫存數量的邏輯:

<?php

namespace App\Filament\Widgets;

use App\Models\Product;
use Filament\Widgets\ChartWidget;

class InventoryByCategoryChart extends ChartWidget
{
    protected static ?string $heading = '各分類庫存量';

    protected function getData(): array
    {
        $stocks = Product::query()
            ->selectRaw('category, sum(stock) as total_stock')
            ->groupBy('category')
            ->pluck('total_stock', 'category');

        return [
            'datasets' => [
                [
                    'label' => '庫存數量',
                    'data' => $stocks->values()->toArray(),
                ],
            ],
            'labels' => $stocks->keys()->toArray(),
        ];
    }

    protected function getType(): string
    {
        return 'bar';
    }
}

category 與 stock 都是商品模組在 Day 04:表單欄位全解析,從文字輸入到日期選擇器 就已經定案的既有欄位,這裡只是換一種分組聚合的方式,把它們依分類加總,整理成同一套 labels 對應 datasets 的格式。getType() 回傳 bar,用長條圖呈現,同一個時間點下,幾個貨架各自堆了多高,長條圖比折線圖更適合這種對比情境。

這裡要特別點出,這張圖表跟前一張訂單趨勢圖表雖然都屬於圖表類型,但回答的問題面向不一樣。訂單趨勢圖表看的是同一個指標在不同時間點各自的樣子,時間是橫軸的主角;庫存分佈圖表看的是同一個時間點下,不同分類各自的樣子,分類才是橫軸的主角。圖表類型本身不是單一用途的工具,會因為資料的分組維度不同,服務到不同的管理問題。

驗證方式跟前一個示範一樣,登入後台首頁,這次會看到第二張圖表出現,橫軸是各個商品分類,縱軸是該分類目前的庫存總量。

後台首頁,InventoryByCategoryChart 掛載後畫面,橫軸為商品分類,縱軸為各分類庫存總量,其中一個分類的長條明顯偏矮

管理者掃過這排長條,哪個分類的庫存量偏低,需要盡快進貨,一眼就能看出來,不用切到商品列表頁逐筆核對數量再自己在心裡加總。這類「哪裡偏低該補貨」的洞察,是單一統計數字沒辦法傳遞的,得靠這種同時攤開多個分類的對比畫面才看得出來。

首頁有數字也有圖了,但誰看到的都一樣

在 Day 19 已定案的 Widget 機制上,今天補上了訂單趨勢與庫存分佈這兩個圖表類型的 Widget。後台首頁現在同時具備統計數字與圖表兩種資訊區塊,管理者看得出現況,也看得出趨勢往哪裡走。

這裡也得誠實面對一個新出現的落差。不管是倉管、業務還是財務角色登入,看到的首頁都是同一套統計數字加圖表組合,訂單趨勢圖表呈現的永遠是全公司的整體資料。不同經銷據點的人登入,圖表裡的訂單走勢也還是同一份全公司彙總數字,沒有人看到專屬於自己據點的版本,這件事今天完全沒有處理。

回頭看看 Day 16:設計一份貼近真實團隊的角色權限矩陣 與 Day 17:多租戶隔離,讓每個經銷據點只看到自己的資料 已經建立的這兩層機制,角色權限矩陣決定了誰能操作哪些 Resource,多租戶隔離決定了每個據點只查得到自己的資料,這兩層機制目前都只作用在 Resource 的操作與資料查詢上,還沒有延伸到 Widget 這個今天才補上的呈現層。今天做出來的兩張圖表,暫時是所有登入者共用的同一套畫面。

誰登入看到誰的儀表板,Widget 排版與權限呼應,是接下來要處理的內容。


上一篇
Day 19:第一個 Widget,把訂單數字擺上首頁
系列文
Laravel Filament 從入門到實戰 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言