はじめに
Python学習を始めて、累計143時間になりました。
私は約11年前、広島風お好み焼きの職人として、
岩手・関東・関西・九州など、全国の大手百貨店の催事場や地下催事場で実演販売をしていました。
売り子さんの雇用、限られた開催期間で赤字を出さないための材料発注、販売数の見積もりなど、
経営者として利益と損益を考えながら現場を走り回っていました。
現在、Flaskを使ってベーカリー向けの商品売上管理アプリを個人開発しています。
このアプリでは、主に次の流れで店舗の売上を管理できます。
- 月ごとの商品メニューを登録する
- 当日の商品別売上個数を入力する
- 売上ランキングやグラフを確認する
- Gemini APIを使ってAIから改善提案を受ける
機能自体はすでに動作していました。
しかし、実際に自分がパン屋の店長になったつもりで画面を操作してみると、細かな違和感がいくつも見つかりました。
今回行ったのは、単なる配色変更ではありません。
店長が画面を見たときに迷わないか
誤操作や勘違いが起きないか
次に何をすればよいか自然に分かるか
というユーザー視点から、文言・配色・画面導線・入力状態の表示を見直しました。
私は以前、百貨店でお好み焼きの実演販売を行っていました。
自分で商品を作り、セールストークを考え、接客し、お客様に販売する。
その経験と、以前学んだWebデザインの感覚を組み合わせながら、今回は「老若男女、誰が見ても使いやすいアプリ」を目指してUIを改善しました。
今回の改善は、見た目を華やかにすることではなく、店長が迷う原因を事前に取り除くことを目的としています。
開発しているアプリ
今回改善したのは、Flaskで開発しているベーカリー向けの商品売上管理アプリです。
主な技術構成は次のとおりです。
- Python
- Flask
- PostgreSQL
- SQLAlchemy
- HTML
- CSS
- JavaScript
- Gemini API
- Docker
- Render
アプリの基本的な画面構成は次の4画面です。
- トップページ
- 商品登録完了ページ
- 日次売上入力ページ
- 売上分析ダッシュボード
改善前に感じた違和感
1. トップページに未来の年が表示されていた
トップページでは、その月に販売する商品メニューを登録します。
しかし、2026年時点で年選択のプルダウンを開くと、未来の2027年まで表示されていました。
2027年
2026年
最初は、データベース内に2027年の商品を登録してしまったのではないかと考えました。
確認したところ、原因はデータベースではなく、HTML側で2027年を選択肢として直接記述していたことでした。
商品メニューの登録画面は、基本的に現在の営業年度の商品を作成する画面です。
そのため、トップページでは今年だけを選択できるように変更しました。
一方、売上分析ダッシュボードは過去の結果を確認する画面です。
こちらは、現在年から過去の年だけを表示するように変更しました。
{% for y in range(current_year, current_year - 4, -1) %}
2026年の場合は、次のように表示されます。
2026年
2025年
2024年
2023年
未来の売上結果はまだ存在しないため、分析画面に未来年を表示する必要はありません。
小さな違和感ですが、利用者から見ると、
2027年を選ぶと何が表示されるのだろう?
という疑問につながります。
エラーが起きてから対応するのではなく、利用者が疑問を感じる前に矛盾を取り除きました。
2. 上書き更新なのにボタンが「保存する」だった
日次売上入力ページでは、商品ごとに当日の販売個数を入力します。
バックエンドの処理は、すでに次のようになっていました。
existing = DailySales.query.filter_by(
product_id=int(product_id),
date=sale_date
).first()
if existing:
existing.quantity = qty_int
else:
sale = DailySales(
product_id=int(product_id),
date=sale_date,
quantity=qty_int
)
db.session.add(sale)
同じ商品・同じ日付のデータがすでに存在する場合は、登録済みの個数を新しい個数で上書きします。
たとえば、すでに14個と登録されている商品へ17個を入力した場合は、次の結果になります。
14個 → 17個
次のように加算されるわけではありません。
14個 + 17個 = 31個
この日次売上入力は加算方式ではありません。
すでに14個が登録されている状態で17個を入力すると、14 + 17 = 31ではなく、登録値が14 → 17へ上書き更新されます。
しかし、画面上のボタンは次の文言でした。
💾 保存する
この文言だけでは、利用者は次のどちらなのか判断できません。
- 入力した個数を新規追加する
- 現在の個数を入力値で上書きする
内部処理が正しくても、画面の説明と処理内容が一致していなければ、利用者の勘違いにつながります。
そこでボタンを次の文言へ変更しました。
<button type="submit" class="btn-submit">
💾 本日の売上個数を更新する
</button>
成功メッセージも、同じ考え方で変更しました。
{% if success %}
<div class="success-msg">
✅ 本日の売上個数を更新しました!
</div>
{% endif %}
「保存」ではなく「更新」と明記することで、操作結果を理解しやすくしました。
内部処理が正しくても、画面上の文言と処理内容が一致していなければ、利用者の勘違いや誤操作につながります。
今回は「保存する」を「本日の売上個数を更新する」へ変更し、上書き処理であることを明確にしました。
3. 今日何回入力したか分からなくなる可能性があった
日次売上入力ページは、1日に何度でも開くことができます。
店舗の営業終了後に一度だけ入力するとは限りません。
たとえば、次のような運用も考えられます。
- 途中経過として14個を入力する
- その後さらに商品が売れる
- 再び入力ページを開く
- 最終結果の17個へ修正する
改善前の画面では、入力ページを開いても、現在データベースに何個登録されているのか分かりませんでした。
これでは店長が、
今日、この商品はもう入力しただろうか?
14個は登録済みだっただろうか?
今から17個を入れたら31個になるのだろうか?
と迷う可能性があります。
そこで、商品名の下に現在の登録済み個数を表示することにしました。
高級食パン
🟢 本日の登録済み:14個
さらに、入力欄にも現在値を最初から表示します。
入力欄:14
利用者が17へ変更して更新すると、データベースの値が14から17へ置き換わります。
本日の登録済み個数を取得する
指定日の売上データを、商品IDと売上個数の辞書に変換する関数を追加しました。
def _get_today_sales_map(target_date):
"""指定日の商品別売上個数を辞書で返す。"""
sales = DailySales.query.filter_by(date=target_date).all()
return {
sale.product_id: sale.quantity
for sale in sales
}
返されるデータは次のような形式です。
{
1: 14,
2: 8,
3: 0
}
キーが商品ID、値がその日の登録済み売上個数です。
inputルートからテンプレートへ渡す
日次売上入力ルートでは、GETとPOSTの両方で当日の売上データをテンプレートへ渡します。
@app.route("/input", methods=["GET", "POST"])
def input_sales():
today = datetime.date.today()
if request.method == "POST":
date_str = request.form.get("date")
sale_date = datetime.date.fromisoformat(date_str)
product_ids = request.form.getlist("product_id")
quantities = request.form.getlist("quantity")
logger.info(
f"Sales data submission received for date: {sale_date}"
)
for product_id, quantity in zip(product_ids, quantities):
if quantity.strip() == "":
continue
try:
qty_int = int(float(quantity))
except ValueError:
logger.warning(
"Invalid quantity format skipped: "
f"product_id={product_id}, quantity={quantity}"
)
continue
if qty_int < 0:
logger.warning(
"Negative quantity skipped: "
f"product_id={product_id}, quantity={qty_int}"
)
continue
existing = DailySales.query.filter_by(
product_id=int(product_id),
date=sale_date
).first()
if existing:
existing.quantity = qty_int
else:
sale = DailySales(
product_id=int(product_id),
date=sale_date,
quantity=qty_int
)
db.session.add(sale)
db.session.commit()
logger.info(
f"Sales data successfully committed for date: {sale_date}"
)
return render_template(
"input.html",
success=True,
products=_get_current_products(),
today=today,
today_sales=_get_today_sales_map(today)
)
return render_template(
"input.html",
products=_get_current_products(),
today=today,
today_sales=_get_today_sales_map(today)
)
POST後にもデータベースから最新値を取得し直しています。
そのため、更新後の画面には、上書きされた最新の売上個数が表示されます。
テンプレートへ現在値を表示する
input.htmlの商品入力部分を次のように変更しました。
{% for product in products %}
<div class="product-row">
<input
type="hidden"
name="product_id"
value="{{ product.id }}"
>
<div class="product-info">
<span class="product-name">
{{ product.name }}
</span>
<span class="registered-quantity">
🟢 本日の登録済み:
{{ today_sales.get(product.id, 0) }}個
</span>
</div>
<span class="product-price">
¥{{ product.price }}
</span>
<input
type="number"
name="quantity"
class="qty-input"
min="0"
value="{{ today_sales.get(product.id, 0) }}"
required
>
</div>
{% endfor %}
登録データが存在しない場合は、get()の第2引数によって0が表示されます。
{{ today_sales.get(product.id, 0) }}
同じ値を入力欄のvalueにも設定しています。
value="{{ today_sales.get(product.id, 0) }}"
これで、利用者は画面を開いた時点で現在の登録状況を把握できます。
文字を緑色にしなかった理由
当初は、「本日の登録済み」の文字を緑色にする案も考えました。
しかし、色を変更すると、背景色や画面環境によって視認性が低下する可能性があります。
そこで、文字色は既存の濃い色を維持し、緑色の丸だけを目印として使用しました。
<span class="registered-quantity">
🟢 本日の登録済み:
{{ today_sales.get(product.id, 0) }}個
</span>
CSSでは、文字色を親要素から継承しています。
.input-page .registered-quantity {
color: inherit;
font-size: 13px;
font-weight: bold;
line-height: 1.4;
}
色だけで状態を伝えるのではなく、次の3つを組み合わせています。
- 緑色の丸
- 「本日の登録済み」という文言
- 太字
色の違いが認識しにくい利用者でも、文言を読めば意味が伝わります。
色分けは操作を見つけやすくする補助として使用しています。
色だけで意味を伝えず、アイコン・具体的な文言・ボタンの形も併用することで、色の見え方やIT経験に左右されにくい画面を目指しました。
商品同士が詰まって見える問題も改善
登録済み個数を追加したことで、商品名・登録済み数・価格・入力欄が一つの行に集まり、商品同士が詰まって見えるようになりました。
そこで、商品ごとの余白と区切り線を追加しました。
.input-page .product-row {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 18px;
padding-bottom: 14px;
border-bottom: 1px solid #f3ecde;
}
商品名と登録済み個数は、一つの情報グループとして縦に並べます。
.input-page .product-info {
display: flex;
flex: 1;
flex-direction: column;
gap: 4px;
min-width: 0;
}
これにより、一商品ごとの境界が分かりやすくなりました。
画面間の導線を改善
改善前は、各画面から次に行う操作への移動が分かりにくい部分がありました。
そこで、店舗業務の流れに合わせて画面間のリンクを追加しました。
商品メニューを登録する
↓
日次売上を入力する
↓
売上分析・ランキングを見る
日次売上入力ページには、ダッシュボードへのボタンを追加しました。
<a
href="{{ url_for('dashboard') }}"
class="btn-submit btn-dashboard-link"
>
📊 お店の健康診断書(売上分析・ランキング)を見る
</a>
トップページへ戻るリンクも、文字だけのリンクではなく、押せる場所が分かりやすいボタンへ変更しました。
<a
href="{{ url_for('index') }}"
class="input-home-link"
>
🍞 トップページに戻る
</a>
ダッシュボードにも、日次入力ページとトップページへの導線を追加しました。
<div class="dashboard-actions">
<a
href="{{ url_for('input_sales') }}"
class="dashboard-action dashboard-input-link"
>
📝 日次売上を入力する
</a>
<a
href="{{ url_for('index') }}"
class="dashboard-action dashboard-home-link"
>
🍞 トップページに戻る
</a>
</div>
どの画面からでも、次の業務へ移りやすい構成にしました。
ボタンの色に役割を持たせる
今回、ボタンの色も画面ごとにバラバラに設定するのではなく、操作の役割ごとに固定しました。
緑 :商品メニューの登録
はちみつ色 :日次売上の入力・更新
いちご色 :売上分析・ランキング
淡いクリーム色 :AIへの質問
白 :トップページへ戻る
商品登録はピスタチオ色
商品メニューを登録するボタンには、ピスタチオや葉野菜をイメージした緑色を使用しました。
.btn-menu-register {
background-color: #7fa45a;
color: #ffffff;
}
.btn-menu-register:hover {
background-color: #688c47;
}
日次入力ははちみつ色
日次売上入力には、パンやはちみつを連想する色を使用しました。
.input-page .btn-submit {
background-color: #c9a063;
color: #ffffff;
}
売上分析はいちご色
売上分析ダッシュボードへのボタンには、いちごジャムのような赤色を使用しました。
.input-page .btn-dashboard-link {
background-color: #e85d5d;
color: #ffffff;
}
.input-page .btn-dashboard-link:hover {
background-color: #cf4646;
}
ベーカリーらしさを保ちながら、日次入力とは異なる操作だと分かる色にしました。
AIへの質問は淡いクリーム色
日次入力ページの「今日のひとことを聞く」と、ダッシュボードの「詳しいアドバイスを聞く」は、どちらもAIへ質問する操作です。
そのため、同じ淡いクリーム色へ統一しました。
.dashboard-page .btn-ai-advice {
background-color: #f7eed6;
color: #8b5a1a;
}
.dashboard-page .btn-ai-advice:hover {
background-color: #ebdcb9;
}
色を固定することで、別の画面へ移動しても、同じ色のボタンは同じ種類の操作だと判断しやすくなります。
ただし、色だけに頼らず、アイコンと具体的な文言も併用しています。
CSSをHTMLから分離した
今回の改善では、各HTMLファイル内に書かれていたCSSを、static/style.cssへ移動しました。
sales_data_app/
├── app.py
├── templates/
│ ├── index.html
│ ├── input.html
│ ├── dashboard.html
│ └── success.html
└── static/
└── style.css
各HTMLでは、次のようにCSSファイルを読み込んでいます。
<link
rel="stylesheet"
href="{{ url_for('static', filename='style.css') }}"
>
さらに、ページごとのスタイルが別画面へ影響しないように、bodyへページ専用クラスを設定しました。
<body class="input-page">
<body class="dashboard-page">
<body class="success-page">
CSSでは、次のようにページクラスを先頭へ付けています。
.input-page .btn-submit {
/* 日次入力ページだけに適用 */
}
.dashboard-page .btn-submit {
/* ダッシュボードだけに適用 */
}
これにより、同じ.btn-submitというクラス名を使用していても、別ページのボタンが意図せず変化するリスクを減らしました。
実際に起きたCSSの影響
CSSを整理する途中で、ダッシュボードの抽出ボタンから角丸が消え、黒い枠が表示されました。
原因は、共通の.btn-submitとダッシュボード専用スタイルの上書き関係でした。
そこで、ダッシュボード側に必要な見た目を明示しました。
.dashboard-page .btn-submit {
width: auto;
margin-top: 0;
padding: 10px 15px;
background-color: #8b5a1a;
color: #ffffff;
border: none;
border-radius: 8px;
outline: none;
box-shadow: none;
cursor: pointer;
}
CSSを共通化するとコードは整理できますが、影響範囲が広がります。
共通スタイルとページ専用スタイルの役割を考えながら調整する必要があると学びました。
CSSを共通化すると重複を減らせますが、同じクラスを使用している別ページにも変更が波及する可能性があります。
共通スタイルとページ専用スタイルの役割を分け、修正後はすべての画面を確認する必要があります。
「老若男女が使いやすい」を判断基準にした
私がアプリ開発で大切にしているコンセプトは、
老若男女、誰が見ても使いやすいアプリ
です。
今回の改善でも、この考え方を判断基準にしました。
- 曖昧な「保存」を、具体的な「更新」へ変える
- 現在の登録済み個数を画面に表示する
- 入力欄へ現在値を最初から入れる
- 未来年など、利用者が迷う選択肢を出さない
- ボタンの色を操作の役割ごとに統一する
- 色だけでなく、アイコンと文言も併用する
- テキストリンクではなく、押せる範囲が分かるボタンにする
- 次の操作へ移動するためのリンクを各画面へ配置する
見た目を派手にすることよりも、利用者が考え込まずに操作できることを優先しました。
お好み焼き実演販売の経験がUI改善に生きた
以前、私は百貨店でお好み焼きの実演販売を行っていました。
商品を自分で作り、セールストークを考え、お客様へ説明し、実際に購入していただく。
外から見ると、単純に次の流れに見えるかもしれません。
売る → 買う
しかし、その間には多くの判断があります。
- どのように見せれば商品へ興味を持ってもらえるか
- どの言葉なら特徴が伝わるか
- お客様が何に迷っているのか
- 次に何を案内すれば購入しやすいか
- 購入後に満足してもらえるか
この感覚は、WebアプリのUIにも共通していると感じました。
機能を配置するだけではなく、
利用者がその表示をどう受け取るか
まで考える必要があります。
今回の「保存」と「更新」の違いも同じです。
開発者は内部処理を知っているため、保存ボタンでも意味を理解できます。
しかし、初めて使う店長は、コードの中身を知りません。
画面に書かれている言葉だけで判断します。
だからこそ、内部処理と画面上の説明を一致させる必要がありました。
トラック運転で身につけた危険予知も生きた
私は現在、トラックドライバーとして働いています。
運転では、
危険が起きてから対応するのではなく、起きる可能性を予測して先に潰す
ことが重要です。
今回のUI改善でも、同じ考え方を使いました。
「今日何個入力したか分からなくなるかもしれない」
↓
本日の登録済み個数を表示する
「17個を入力したら追加されると勘違いするかもしれない」
↓
更新ボタンと現在値表示で上書きだと明確にする
「未来年を選んで迷うかもしれない」
↓
不要な未来年を選択肢から削除する
「次の画面へどこから移動するか迷うかもしれない」
↓
各画面に次の操作ボタンを設置する
利用者から、
これ、どうしたらいいですか?
と言われる前に、迷う可能性を予測して画面上で解決する。
これは、アプリ開発における危険予知だと感じました。
今回学んだこと
今回の改善で最も大きかった学びは、次の点です。
見た目のリファクタリングは、単なる装飾ではありません。
文言・余白・配色・現在値の表示・画面導線も、利用者の迷いや誤操作を防ぐための機能だと学びました。
内部処理が正しく動作していても、利用者にその意味が伝わらなければ、実際の業務では問題になる可能性があります。
コード上の正しさ
+
利用者に伝わる分かりやすさ
=
現場で使えるシステム
今回の改善を通して、ユーザー視点を持つことの重要性を改めて学びました。
今後の改善予定
今後は、さらに次の点を改善していきたいと考えています。
- JavaScriptをHTMLから外部ファイルへ分離する
- CSSをページごとのファイルへ分割する
- 入力エラーの表示をさらに分かりやすくする
- 更新前後の値を記録する履歴機能
- 商品数が増えた場合の一覧性改善
- 店舗や利用者ごとにデータを分離する認証機能
- スマートフォン実機での継続的な表示確認
機能を増やすだけでなく、店長が迷わず使えることを優先して改善していきます。
まとめ
今回は、Flaskで開発しているベーカリー売上管理アプリのUIを、店長目線でリファクタリングしました。
主な改善内容は次のとおりです。
- 不要な未来年の選択肢を削除
- 「保存する」を「本日の売上個数を更新する」へ変更
- 商品ごとに本日の登録済み個数を表示
- 入力欄へ現在値を初期表示
- 商品同士の余白を調整
- 各画面の導線を改善
- 操作の役割ごとにボタン色を統一
- AIボタンの配色を統一
- CSSを外部ファイルへ分離
- ページ専用クラスでCSSの影響範囲を整理
今回の改善は、単なるデザイン変更ではありませんでした。
元お好み焼き職人としての販売経験、Webデザインを学んだ経験、トラック運転で身につけた危険予知。
これらの経験を組み合わせ、
店長が困る前に、迷う原因を画面上から取り除く
というUI改善につなげることができました。
これからも、老若男女が迷わず使える、現場目線のアプリを目指して改善を続けます。
関連リンク
- GitHub
- オンラインデモ
- Qiita
