前回のおさらい
前回やったこと
- ログイン画面UI作成
- ユーザ登録UI作成
- Firebase Authenticationへユーザ登録の確認
今回やること
- Googleアカウントを使用した認証
- ダッシュボード画面の型枠作成
Googleアカウントを使用した認証
これについては、Grok3に
ユーザー登録画面の方でGoogleアカウント認証を実装
と入力してみました
結果出てきたソースコードがこれです
(Diff差分表示にしました)
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 ...っと
こんな感じの画面になりました とりあえずGoogleアカウントでログインしてみるか…
おぉ、他アプリでも見るGoogleアカウントを選ぶ画面が出てきて選択できる…
そして、選んで連携ってすると
ログインできたっぽいですね
Firebase側の画面も見てみましょう
おー… ちゃんと登録されてる…
すげぇ、こんな簡単に実装できるんや
おっとアブナイ、アカウントは消し消しっと…
※ユーザ登録画面だけではなく、ログイン画面からもGoogleアカウントで入れるように
修正しました。
とりあえず、ログイン画面、ユーザ登録画面、Firebase認証及び登録
ここまでは何とか実装してローカルで動作確認できました
ダッシュボードの型枠作成
ある意味今回の肝ともいえる部分です
BI、分析画面側は別に準備するって画面遷移してたけど、正直どうしようか迷ってます
ま、まぁ、今までもAI君に任せてきてるから一回そのまま作りたいかな…?
とりあえずダッシュボードに関するコードを生成してもらおうと思います
Chart.jsでのBIダッシュボードを作成
以下は生成AI(Grok3)での出力です
※同じ内容を出力している所もあるので、一部を抜粋
2.3 Firestoreデータ取得
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ダッシュボード(ホーム画面)
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
}
}
こんな感じで出力されたので実装してみます
...実装中...
いや、円グラフでけぇ…
進捗の所、ポイントしか表示されてねぇ…
予算は表示されてるけど、使用済み予算との対比がねぇ…
とかいろいろあるので、これも指摘して修正してみます
ダッシュボードについて
予算グラフに対して使用済み予算を追加
進捗グラフはプログレスバー形式に変更
原価グラフは原価使用に対する割合をグラフ内に表示するように変更
原価グラフのパーセンテージはポップアップではなく、グラフ内に記載
原価グラフだけ大きすぎる、他と大きさを合わせる
工事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 依存ライブラリ
{
"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ダッシュボード(改良版)
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 したままでファイルを書き換えたら勝手に修正内容を読み込んでくれる…
変更して、ファイルをセーブすると勝手にコンパイルされて
勝手に読み込んで変えてくれました
※一時的にエラーが吐かれる部分(依存関係など)はありましたがホント便利ですねこれ
そして、出力された画面がこちら
どちらかってーと
担当者レベルのホーム画面のような気がするよーな
もっとホーム画面って(会社レベルなら)
- 今年度の業績予想に対しての現在の売上予想、原価率、予想される利益
- 各部毎の業績進捗、期末業績予想のみ? or Q毎の確定値からの積上げでの予想?
みたいな感じになりそう…
けどまぁ、とりあえずテストで作ってる段階なので
一旦こんな感じにしといて、また別画面を作成していきます
多分、今回使ったUIの一部は工事担当者レベルに流用かな…
次回は、工事一覧(画面遷移にはなかったけど、工事追加画面)、工事詳細の画面の枠を
とりあえず作っていってみようかと思います