Djangoで検索・ダッシュボード機能を作ってみた。

この記事は、「Djangoを使用した顧客管理システムの作成」の5つ目の記事です。

https://qiita.com/tomo0227/items/cc893ea4e8e6cfb6ad77

いよいよ完成も近くなってきました。今回は、検索機能とダッシュボード機能を作っていきます。
## 検索機能の作成

https://qiita.com/tomo0227/items/7b9934ab4bc36cdcd4e8

今回は、商談履歴(上記の記事で作成)を年月、顧客名、カテゴリで検索できる機能を追加していきたいと思います。
### filters.pyの作成
検索機能を追加するの```django-filter```を使用していきます。
```
pip install django-filter
```
インストールしてない人は、上記でインストールしよう。
```django-filter```を使うと検索条件を短いコードで作成することができ、簡単に検索フォームを作ることができます。今回作成した```filters.py```は```models.py```とかと同じフォルダ```crm_app```に保存します。
さっそく作った```filters.py```を見ていきましょう。
```python:crm_app/filters.py
import django_filters
from .models import Deal

class DealFilter(django_filters.FilterSet):
    year = django_filters.NumberFilter(field_name='date',lookup_expr='year',label='年')
    month = django_filters.NumberFilter(field_name='date',lookup_expr='month',label='月')
    customer_name = django_filters.CharFilter(field_name='customer',lookup_expr='icontains',label='顧客名')
    category = django_filters.ModelChoiceFilter(
        queryset = Category.objects.all(),
        label = 'カテゴリ',
    )
    
    class Meta:
        model = Deal
        fields = ['year','month','customer_name','category']
```
まずは、```djnago-filter```の```NumberFilter```を使用して、数値に対しての検索フィルターを作成しています。```Deal```クラスの```date```フィールドを対象として、年月をフィルタリングしています。引数の```lookup_expr```について、後ほど詳しく説明します。

次に```CharFilter```を使って、文字列に対しての検索フィルターを作成しています。```customer```フィールドを対象とすることで、顧客名でフィルタリングしています。

最後に```ModelChoiceFilter```を使って、カテゴリで検索ができるようにしています。```queryset```で```Category```モデルのすべてのオブジェクトを取得し、プルダウン形式(選択式)で選択できるフィルターを作成しています。

### filters.pyの適用
では、作成した```filters.py```を```view.py```に適用していきます。
```python:crm_app/views.py
from .filters import DealFilter
...
#商談履歴の閲覧
class DealList(View):
    def get(self,request):
        sort_by = request.GET.get('sort', 'date')  # デフォルトは商談日でソート
        deals = Deal.objects.all().order_by(sort_by)
        
        deal_filter = DealFilter(request.GET,queryset=deals)#フィルタ適用
        filtered_deals = deal_filter.qs
        
        
        paginator = Paginator(filtered_deals,10) #1ページに10件
        page = request.GET.get('page')
        paginated_deals = paginator.get_page(page)
        
        
        context = {
            'deal_filter':deal_filter,
            'deals':paginated_deals,#フィルタ後のデータのみを渡す
        }
        return render(request,'crm_app/deal_list.html',context) 
```
まずは、```sort_by```変数を作成して、ソート(並び替え)をしていきます。```sort_by```はURLパラメータに```sort```が設定されていたら、それを採用し、なければ```date```でソートするというものです。

次に```DealFilter```を使用して、検索フォームに入力された内容に対して、```Deal```モデル内でフィルタリングを行います。
```deal_filter = DealFilter(request.GET,queryset=deals)```は、GETリクエストの内容を基にフィルタリングしています。取得した```deals```オブジェクト内に対しての検索フォームを表しています。フィルタリングを行ったら、```.qs```を使用して、フィルタリング後のデータだけ取得します。

ページネーションに渡す値をフィルタリング後のものに変え、検索フォームである```deal_filter```とフィルタリング後の値をテンプレートに渡しています。

テンプレート以前作成したもの検索フォーム部分を追加します。
```html:templates/crm_app/deal_list.html
...
    <form method="GET">
        {{ deal_filter.form.as_p }}
        <button type="submit">検索</button>
    </form>
...
```
これで、検索機能の完成です。
### 補足：lookup_exprについて
```lookup_expr```とは```django-filter```で検索するとき、どんな条件で検索するかを決めるオプションです。以下が主に使われるものです。
|`lookup_expr` |意味 |
|---|---|
|`exact`|完全一致|
|`iexact`|大文字小文字を無視した一致|
|`contains`|部分一致|
|`icontains`|大文字小文字を無視した部分一致|
|`year`|年での絞り込み|
|`month`|月での絞り込み|
|`day`|日での絞り込み|
|`gt`|より大きい|
|`gte`|以上|
|`lt`|より小さい|
|`lte`|以下|

## ダッシュボードの作成
### 作業内容

1. **基本的なダッシュボード作成**
    - 顧客数、商談数、売上予測（ダミーデータ可）を表示
2. **データ集計**
    - KPIを計算してテンプレートに表示
3. **グラフ表示**
    - `chart.js` などを使用して簡単なグラフを表示

### ダッシュボードのビュー作成
まずは、ビューを作成していきます。ダッシュボートで必要なのは、顧客数、商談数、売上予測(今回はダミーデータを使用)です。それらを用意していきます。
```python:crm_app/views.py
class Dashboard(View):
    def get(self,request):
        #基本データの取得
        customer_count = Customer.objects.count() #顧客数
        deal_count = Deal.objects.count() #商談数
        
        #売上予測(ダミーデータ)
        sales_forecast = sum(random.randint(10000,50000) for _ in range(deal_count)) #仮の売上予測
        
        #月ごとの商談件数(グラフ用データ)
        monthly_deals = (
            Deal.objects
            .extra(select={'month':"strftime('%Y-%m',date)"}) #SQLite用(PostgreSQLなら'date_trunc'
            .values('month')
            .annotate(count=Count('id'))
            .order_by('month')
        )
        
        months = [entry['month'] for entry in monthly_deals]  # x軸(月)
        deal_counts = [entry['count'] for entry in monthly_deals]  # y軸(商談数)
        
        
        context = {
            'customer_count':customer_count,
            'deal_count':deal_count,
            'sales_forecast':sales_forecast,
            'months':months,
            'deal_counts':deal_counts,
        }
        
        return render(request,'crm_app/dashboard.html',context)
```
まずは、```Customer```クラスと```Deal```クラスから顧客数と商談数を取得します。数を数えるのは、```count()```を使用しています。次に売上予測のダミーデータを作成しているのですが、こちらは1商談で```10000~50000```円として、ランダムな数値を割り振っています。

次に```monthly_deals```についてです。こちらは月ごとの商談件数のデータをグラフ用に作成している部分です。
上からそれぞれ、
- ```Deal```モデルを指定
- ```date```フォールドの日付を```YYYY-MM```の形に変形し、```month```という名前の列を作成
- ```month```ごとにデータをグループ化
- 各月ごとの商談数を```id``をカウントすることで取得
- 月ごとに並び替え
を行っています。

次に、```months```と```deal_counts```はx軸とy軸のデータを作成しています。```monthly_deals```からx軸には月ごとのデータを、y軸には商談数のデータを格納している。

最後に、作成したデータをテンプレートに渡しています。
### テンプレートの作成
今回は、```base.html```を使用せずに作成しました。```base.html```が使えないというわけではなく、新しく```javascript```ファイルを使用する関係で、見直すときにわかりやすいかなという理由です。関係ないよって人はぜひ```base.html```を編集してみてください。
```html:templates/crm_app/dashboard.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ダッシュボード</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
    {% load humanize %}
    <div class="container mt-5">
        <h2>ダッシュボード</h2>

        <!-- ✅ KPI セクション -->
        <div class="row text-center">
            <div class="col-md-4">
                <div class="card text-white bg-primary mb-3">
                    <div class="card-header">顧客数</div>
                    <div class="card-body">
                        <h3>{{ customer_count }}</h3>
                    </div>
                </div>
            </div>
            <div class="col-md-4">
                <div class="card text-white bg-success mb-3">
                    <div class="card-header">商談数</div>
                    <div class="card-body">
                        <h3>{{ deal_count }}</h3>
                    </div>
                </div>
            </div>
            <div class="col-md-4">
                <div class="card text-white bg-warning mb-3">
                    <div class="card-header">売上予測</div>
                    <div class="card-body">
                        <h3>¥{{ sales_forecast|intcomma }}</h3>
                    </div>
                </div>
            </div>
        </div>

        <!-- ✅ 商談数の推移グラフ -->
        <div class="card mt-4">
            <div class="card-header">月ごとの商談件数</div>
            <div class="card-body">
                <canvas id="dealChart"></canvas>
            </div>
        </div>
    </div>

    <script>
        var ctx = document.getElementById('dealChart').getContext('2d');
        var dealChart = new Chart(ctx, {
            type: 'bar',
            data: {
                labels: {{ months|safe }},  // x軸
                datasets: [{
                    label: '商談件数',
                    data: {{ deal_counts|safe }},  // y軸
                    backgroundColor: 'rgba(54, 162, 235, 0.6)',
                    borderColor: 'rgba(54, 162, 235, 1)',
                    borderWidth: 1
                }]
            },
            options: {
                scales: {
                    y: { beginAtZero: true }
                }
            }
        });
    </script>
</body>
</html>
```
まずグラフ表示のために```chart.js```を使用します。ヘッダーのところに```script```で設定します。
まずはKPIセクションについてです。KPIとは「ビジネスの成果を測るための重要な指標」のことで、要するに成果として、この項目を見ようねっていうことです。今回は、「顧客数」「商談数」「売上予測」の3つの指標を表示しています。

次にグラフを表示している部分についてです。グラフは```<canvas id="dealChart"></canvas>```の部分に表示されます。設定しているのが、下の```<script></script>```部分です。

```script```部分は、
**var ctx = document.getElementById(’dealChart’).getContext(’2d’)**：id=”dealChart”の<canvas>要素を取得し、2Dグラフィックスを描写するためのコンテキストを取得。html内に<canvas id=”dealChart”></canvas>の記述が必要。

**var dealChart = new Chart()：** Chartコンストラクタを使用してグラフを生成。引数のtypeはグラフの種類を設定。今回使用したbarは棒グラフ。他にもline:折れ線グラフやpie:円グラフなどがある。

**引数のdata{}**：グラフ表示するデータの設定を行う。引数のlabels:{{ months|safe }}は棒グラフのx軸ラベル。months変数が表示される。

**引数のdatasets**：実際のグラフデータを定義する配列。複数のデータセットを追加することで、複数の系列で表示できる。borderWidthは棒グラフの太さを指定する。

**options:{}**：グラフ表示のオプションを設定。scalesは軸の設定。今回はy軸を設定している。beginAtZero:trueは目盛りが０から始めるように設定している。

となっています。
完成したページがこちらです。
![crmシステム_dashboard.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830132/83022bf9-a5b7-4a00-91fd-cc7b65c341be.jpeg)

一件しか追加してないですが、しっかりと表示できていますね。
```urls.py```の部分は割愛してますが、今までと同じように設定するだけです。

これで、顧客管理システム完成です。まだフォームとかの見た目気を使ってない部分多いですので、改良してみると楽しいかもしれません。

1つ目：Djangoを使用した顧客管理システムの作成
https://qiita.com/tomo0227/items/cc893ea4e8e6cfb6ad77
2つ目：顧客管理システムのモデル作成
https://qiita.com/tomo0227/items/11f6a262ee1da183fd70
3つ目：顧客管理システムのCRUD機能の実装
https://qiita.com/tomo0227/items/054c974e104f81ea82db
4つ目：商談履歴管理システムの作成
https://qiita.com/tomo0227/items/7b9934ab4bc36cdcd4e8
5つ目：Djangoで検索・ダッシュボード機能を作ってみた。
https://qiita.com/tomo0227/items/a86f7bf038e87be11892