0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

弱々エンジニアの生成AIを用いた開発 #5-4 (フロントエンド開発 Googleアカウント認証~ダッシュボードの型枠)

Posted at

前回のおさらい

前回やったこと

  • ログイン画面UI作成
  • ユーザ登録UI作成
  • Firebase Authenticationへユーザ登録の確認

今回やること

  • Googleアカウントを使用した認証
  • ダッシュボード画面の型枠作成

image.png

Googleアカウントを使用した認証

これについては、Grok3に

ユーザー登録画面の方でGoogleアカウント認証を実装

と入力してみました
結果出てきたソースコードがこれです
(Diff差分表示にしました)

Singup.js
import React, { useState } from 'react';
- import { createUserWithEmailAndPassword } from 'firebase/auth';
+ import { createUserWithEmailAndPassword, signInWithPopup, GoogleAuthProvider } from 'firebase/auth';
import { auth } from '../../utils/firebase';
import { useNavigate } from 'react-router-dom';
import { TextField, Button, Box, Typography, Alert } from '@mui/material';

const Signup = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const navigate = useNavigate();
+  const provider = new GoogleAuthProvider();

-  const handleSignup = async (e) => {
+  const handleEmailSignup = async (e) => {
    e.preventDefault();
    try {
      await createUserWithEmailAndPassword(auth, email, password);
      navigate('/login');
    } catch (err) {
      setError(err.message);
    }
  };

+  const handleGoogleSignup = async () => {
+    try {
+      await signInWithPopup(auth, provider);
+      navigate('/');
+    } catch (err) {
+      setError(err.message);
+    }
+  };

  return (
    <Box sx={{ maxWidth: 400, mx: 'auto', mt: 8, p: 2 }}>
      <Typography variant="h4" gutterBottom>Sign Up</Typography>
      {error && <Alert severity="error">{error}</Alert>}
-      <form onSubmit={handleSignup}>
+      <form onSubmit={handleEmailSignup}>
        <TextField
          label="Email"
          type="email"
          fullWidth
          margin="normal"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
        <TextField
          label="Password"
          type="password"
          fullWidth
          margin="normal"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
        <Button type="submit" variant="contained" fullWidth sx={{ mt: 2 }}>
          Sign Up with Email
        </Button>
      </form>
+      <Button
+        variant="outlined"
+        fullWidth
+        sx={{ mt: 2 }}
+        onClick={handleGoogleSignup}
+      >
+        Sign Up with Google
+      </Button>
      <Button onClick={() => navigate('/login')} sx={{ mt: 1 }}>
        Already have an account? Login
      </Button>
    </Box>
  );
};

export default Signup;

割と少ない修正でGoogle認証できるんですね…
いやぁ、Firebaseさまさまです

とりあえず、実装して動かしてみました

      ∧_∧
     (・ω・ ) カタカタカタカタ
   _| ̄ ̄||_))_
 /旦|――||// /|
 | ̄ ̄ ̄ ̄ ̄| ̄| . |
 |_____|三|/

npm start ...っと

image.png


こんな感じの画面になりました とりあえずGoogleアカウントでログインしてみるか…

おぉ、他アプリでも見るGoogleアカウントを選ぶ画面が出てきて選択できる…
そして、選んで連携ってすると

image.png

ログインできたっぽいですね
Firebase側の画面も見てみましょう

image.png

おー… ちゃんと登録されてる…
すげぇ、こんな簡単に実装できるんや
おっとアブナイ、アカウントは消し消しっと…
※ユーザ登録画面だけではなく、ログイン画面からもGoogleアカウントで入れるように
修正しました。

とりあえず、ログイン画面、ユーザ登録画面、Firebase認証及び登録
ここまでは何とか実装してローカルで動作確認できました

ダッシュボードの型枠作成

ある意味今回の肝ともいえる部分です
BI、分析画面側は別に準備するって画面遷移してたけど、正直どうしようか迷ってます
ま、まぁ、今までもAI君に任せてきてるから一回そのまま作りたいかな…?

とりあえずダッシュボードに関するコードを生成してもらおうと思います

Chart.jsでのBIダッシュボードを作成

以下は生成AI(Grok3)での出力です
※同じ内容を出力している所もあるので、一部を抜粋

2.3 Firestoreデータ取得

src/utils/data.js
import { collection, getDocs } from 'firebase/firestore';
import { db } from './firebase';

export const fetchProjectData = async () => {
  const querySnapshot = await getDocs(collection(db, 'projects'));
  const projects = querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
  return projects;
};

2.4 BIダッシュボード(ホーム画面)

src/pages/Home.js
import React, { useEffect, useState } from 'react';
import { Bar, Line, Pie } from 'react-chartjs-2';
import { Chart as ChartJS, registerables } from 'chart.js';
import { auth } from '../utils/firebase';
import { signOut } from 'firebase/auth';
import { useNavigate } from 'react-router-dom';
import { Box, Typography, Button, Grid } from '@mui/material';
import { fetchProjectData } from '../utils/data';

ChartJS.register(...registerables);

const Home = () => {
  const [projects, setProjects] = useState([]);
  const navigate = useNavigate();

  useEffect(() => {
    const loadData = async () => {
      const data = await fetchProjectData();
      setProjects(data);
    };
    loadData();
  }, []);

  const handleLogout = async () => {
    await signOut(auth);
    navigate('/login');
  };

  // 工事データ例: [{ id, name, budget, progress, costs: { labor, material, equipment } }, ...]
  const budgetData = {
    labels: projects.map(p => p.name),
    datasets: [{
      label: '予算 (万円)',
      data: projects.map(p => p.budget / 10000),
      backgroundColor: 'rgba(75, 192, 192, 0.6)',
    }],
  };

  const progressData = {
    labels: projects.map(p => p.name),
    datasets: [{
      label: '進捗 (%)',
      data: projects.map(p => p.progress),
      borderColor: 'rgba(54, 162, 235, 1)',
      fill: false,
    }],
  };

  const costData = {
    labels: ['人件費', '材料費', '設備費'],
    datasets: projects.map(p => ({
      label: p.name,
      data: [p.costs?.labor / 10000, p.costs?.material / 10000, p.costs?.equipment / 10000],
      backgroundColor: ['rgba(255, 99, 132, 0.6)', 'rgba(54, 162, 235, 0.6)', 'rgba(255, 206, 86, 0.6)'],
    })),
  };

  return (
    <Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2 }}>
      <Typography variant="h4" gutterBottom>Construction BI Dashboard</Typography>
      <Button variant="contained" onClick={handleLogout} sx={{ mb: 2 }}>
        Logout
      </Button>
      <Grid container spacing={3}>
        <Grid item xs={12} md={6}>
          <Typography variant="h6">予算</Typography>
          <Bar data={budgetData} options={{ responsive: true, plugins: { legend: { position: 'top' } } }} />
        </Grid>
        <Grid item xs={12} md={6}>
          <Typography variant="h6">進捗</Typography>
          <Line data={progressData} options={{ responsive: true, plugins: { legend: { position: 'top' } } }} />
        </Grid>
        <Grid item xs={12}>
          <Typography variant="h6">原価内訳</Typography>
          <Pie data={costData} options={{ responsive: true, plugins: { legend: { position: 'right' } } }} />
        </Grid>
      </Grid>
    </Box>
  );
};

export default Home;

2.6 Firestoreデータ構造コレクション

ドキュメント例:json
{
  "name": "工事A",
  "budget": 50000000,
  "progress": 75,
  "costs": {
    "labor": 20000000,
    "material": 15000000,
    "equipment": 10000000
  }
}

こんな感じで出力されたので実装してみます

 ...実装中...

ダッシュボード画面
image.png

いや、円グラフでけぇ…
進捗の所、ポイントしか表示されてねぇ…
予算は表示されてるけど、使用済み予算との対比がねぇ…

とかいろいろあるので、これも指摘して修正してみます

ダッシュボードについて
予算グラフに対して使用済み予算を追加
進捗グラフはプログレスバー形式に変更
原価グラフは原価使用に対する割合をグラフ内に表示するように変更
原価グラフのパーセンテージはポップアップではなく、グラフ内に記載
原価グラフだけ大きすぎる、他と大きさを合わせる
工事Bのデータも載せたい
売上(予定)と原価総合計を今期業績予定として頭に追加

以下は生成AI(Grok3)での出力です
※同じ内容を出力している所もあるので、一部を抜粋(変更点はDiffにて表示)

2. Firestoreデータ構造

  • コレクション:projects
  • ドキュメント例(「工事A」と「工事B」、salesを追加):
project1
    "name": "工事A",
    "budget": 50000000,
    "usedBudget": 35000000,
    "progress": 75,
    "sales": 60000000,
    "costs": {
        "labor": 20000000,
        "material": 10000000,
        "outsourcing": 5000000
    }

+ project2
+     "name": "工事B",
+     "budget": 80000000,
+     "usedBudget": 48000000,
+     "progress": 60,
+     "sales": 95000000,
+     "costs": {
+         "labor": 25000000,
+         "material": 15000000,
+         "outsourcing": 8000000
+     }

3. コード実装3.1 依存ライブラリ

package.json
{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "^5.0.1",
    "firebase": "^12.0.0",
    "@mui/material": "^5.11.0",
    "@emotion/react": "^11.10.0",
    "@emotion/styled": "^11.10.0",
    "chart.js": "^4.2.0",
    "react-chartjs-2": "^5.2.0",
    "react-router-dom": "^6.26.1",
+    "chartjs-plugin-datalabels": "^2.2.0"
  }
}

3.4 BIダッシュボード(改良版)

src/pages/Home.js
import React, { useEffect, useState } from 'react';
+ import { Bar, Pie } from 'react-chartjs-2';
- import { Bar, Line, Pie } from 'react-chartjs-2';
import { Chart as ChartJS, registerables } from 'chart.js';
+ import ChartDataLabels from 'chartjs-plugin-datalabels';
import { auth } from '../utils/firebase';
import { signOut } from 'firebase/auth';
import { useNavigate } from 'react-router-dom';
+ import { Box, Typography, Button, Grid, Paper } from '@mui/material';
- import { Box, Typography, Button, Grid } from '@mui/material';
import { fetchProjectData } from '../utils/data';

+ ChartJS.register(...registerables, ChartDataLabels);
- ChartJS.register(...registerables);

const Home = () => {
  const [projects, setProjects] = useState([]);
  const navigate = useNavigate();

  useEffect(() => {
    const loadData = async () => {
      const data = await fetchProjectData();
      setProjects(data);
    };
    loadData();
  }, []);

  const handleLogout = async () => {
    await signOut(auth);
    navigate('/login');
  };

+  // 今期業績予定(売上合計、原価合計)
+  const totalSales = projects.reduce((sum, p) => sum + (p.sales || 0), 0) / 10000;
+  const totalCosts = projects.reduce((sum, p) => {
+    const projectCost = (p.costs?.labor || 0) + (p.costs?.material || 0) + (p.costs?.outsourcing || 0);
+    return sum + projectCost;
+  }, 0) / 10000;

+  // 予算グラフ(予算と使用済み予算:左Y軸、使用率:右Y軸)
  const budgetData = {
    labels: projects.map(p => p.name),
    datasets: [
      {
        label: '予算 (万円)',
        data: projects.map(p => (p.budget || 0) / 10000),
        backgroundColor: 'rgba(75, 192, 192, 0.6)',
+        yAxisID: 'y',
      },
+     {
+        label: '使用済み予算 (万円)',
+        data: projects.map(p => (p.usedBudget || 0) / 10000),
+        backgroundColor: 'rgba(255, 99, 132, 0.6)',
+        yAxisID: 'y',
+      },
+      {
+        label: '予算使用率 (%)',
+        data: projects.map(p => p.budget ? ((p.usedBudget || 0) / p.budget * 100).toFixed(1) : 0),
+        backgroundColor: 'rgba(255, 206, 86, 0.6)',
+        yAxisID: 'y1',
+      },
+    ],
+  };

+  const budgetOptions = {
+    responsive: true,
+    maintainAspectRatio: false,
+    plugins: {
+      legend: { position: 'top' },
+    },
+    scales: {
+      y: {
+        type: 'linear',
+        display: true,
+        position: 'left',
+        title: { display: true, text: '金額 (万円)' },
+      },
+      y1: {
+        type: 'linear',
+        display: true,
+        position: 'right',
+        max: 100,
+        min: 0,
+        title: { display: true, text: '予算使用率 (%)' },
+        grid: { drawOnChartArea: false },
+      },
+    },
  };

+  // 進捗グラフ(プログレスバー形式)
  const progressData = {
    labels: projects.map(p => p.name),
    datasets: [
      {
        label: '進捗 (%)',
+        data: projects.map(p => p.progress || 0),
+        backgroundColor: 'rgba(54, 162, 235, 0.6)',
-        data: projects.map(p => p.progress),
-        borderColor: 'rgba(54, 162, 235, 1)',
-        fill: false,
      },
    ],
  };

+  // 原価グラフ(金額のみ、datelabels無効化)
  const costData = {
    labels: ['人件費', '材料費', '外注費'],
    datasets: projects.map(p => ({
      label: p.name,
      data: [
+        p.costs?.labor / 10000 || 0,
+        p.costs?.material / 10000 || 0,
+        p.costs?.outsourcing / 10000 || 0,
-     data: [p.costs?.labor / 10000, p.costs?.material / 10000, p.costs?.equipment / 10000],
      ],
      backgroundColor: ['rgba(255, 99, 132, 0.6)', 'rgba(54, 162, 235, 0.6)', 'rgba(255, 206, 86, 0.6)'],
    })),
  };

+  const costOptions = {
+    responsive: true,
+    maintainAspectRatio: false,
+    plugins: {
+      legend: { position: 'right' },
+      datalabels: { display: false }, // 使用率パーセンテージを非表示
+    },
+  };

+  const chartHeight = 250; // グラフの高さを統一

  return (
    <Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2 }}>
      <Typography variant="h4" gutterBottom>Construction BI Dashboard</Typography>
      <Button variant="contained" onClick={handleLogout} sx={{ mb: 2 }}>
        Logout
      </Button>
+      <Paper sx={{ p: 2, mb: 3 }}>
+        <Typography variant="h6">今期業績予定</Typography>
+        <Typography>売上予定):{totalSales.toFixed(1)}万円</Typography>
+        <Typography>原価合計{totalCosts.toFixed(1)}万円</Typography>
+        <Typography>粗利益{(totalSales - totalCosts).toFixed(1)}万円</Typography>
+      </Paper>
      <Grid container spacing={3}>
        <Grid item xs={12} md={6}>
          <Typography variant="h6">予算</Typography>
+          <Box sx={{ height: chartHeight }}>
+            <Bar data={budgetData} options={budgetOptions} />
+          </Box>
-          <Bar data={budgetData} options={{ responsive: true, plugins: { legend: { position: 'top' } } }} />
        </Grid>
        <Grid item xs={12} md={6}>
          <Typography variant="h6">進捗</Typography>
+          <Box sx={{ height: chartHeight }}>
+            <Bar
+              data={progressData}
+              options={{
+                responsive: true,
+                maintainAspectRatio: false,
+                indexAxis: 'y',
+                scales: { x: { max: 100, beginAtZero: true } },
+              }}
+            />
+          </Box>
-          <Line data={progressData} options={{ responsive: true, plugins: { legend: { position: 'top' } } }} />
        </Grid>
        <Grid item xs={12}>
          <Typography variant="h6">原価内訳</Typography>
+          <Box sx={{ height: chartHeight }}>
+            <Pie data={costData} options={costOptions} />
+          </Box>
-          <Pie data={costData} options={{ responsive: true, plugins: { legend: { position: 'right' } } }} />
        </Grid>
      </Grid>
    </Box>
  );
};

export default Home;


以上で修正点については完了

      ∧_∧
     (>ω<..) カタカタカタカタ
   _| ̄ ̄||_))_
 /旦|――||// /|
 | ̄ ̄ ̄ ̄ ̄| ̄| . |
 |_____|三|/

Reactはいいなぁ…
npm start したままでファイルを書き換えたら勝手に修正内容を読み込んでくれる…

変更して、ファイルをセーブすると勝手にコンパイルされて
勝手に読み込んで変えてくれました
※一時的にエラーが吐かれる部分(依存関係など)はありましたがホント便利ですねこれ

そして、出力された画面がこちら

image.png

どちらかってーと
担当者レベルのホーム画面のような気がするよーな

もっとホーム画面って(会社レベルなら)

  1. 今年度の業績予想に対しての現在の売上予想、原価率、予想される利益
  2. 各部毎の業績進捗、期末業績予想のみ? or Q毎の確定値からの積上げでの予想?

みたいな感じになりそう…
けどまぁ、とりあえずテストで作ってる段階なので
一旦こんな感じにしといて、また別画面を作成していきます
多分、今回使ったUIの一部は工事担当者レベルに流用かな…

次回は、工事一覧(画面遷移にはなかったけど、工事追加画面)、工事詳細の画面の枠を
とりあえず作っていってみようかと思います

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?