前回のおさらい
前回やったこと
- Googleアカウントを使用した認証
- ダッシュボード画面の型枠作成
今回やること
- 工事一覧画面作成(登録についても追加?)
- 工事詳細画面作成
画面作成していると、色々見えてくる部分もやっぱりありますね
ログイン権限設定によって、ダッシュボードからの表示にするか
管理者は工事管理からなのか、作業者レベルだと工事詳細で工程管理辺りなのか?とか
正直、権限レベルでってのは少し考えてたけどフローとかを考えられてなかったので
後々機能追加でやっていくしかなさそうかなー
とりあえず、今回作る部分について考えます
- 工事一覧
見せる情報としては工事名、部署、売上予定金額、担当者名、工事着工日
工事完成日(竣工日と引渡日を分けるか?)あたりが見えれば大丈夫かなと
できれば、今期完成工事と繰越工事でも分けて見られるようにしたいかなぁ
と、なると決算期なんかの会社情報も必要になるか…
- 工事詳細
工事一覧の工事単位のドリルダウン or 工事名をリンクにして工事詳細ページに飛ばす
内容としては、その工事が持っている概要と現状原価と工程進捗管理かな?
概要:売上、実行予算、予定粗利
現状:総原価、外注費、労務費、材料費、その他
進捗:工程表ベースでネットワークチャート?ガントチャート?
とりあえず、予定としてはこんな感じで考えておいて
一旦AIに全振りしてみたらどうなるのか尋ねてみますか
工事一覧~工事詳細の画面を作成して
生成AI(Grok3)での出力です (追加及び変更点のみ抜き出してます)
フォルダ構成
\projects\cbit
├── src
│ ├── components
│ │ └── auth
│ │ ├── Login.js (Modified) // 新規登録ボタン追加
│ │ └── Signup.js (New) // ユーザ登録画面追加
│ ├── pages
│ │ ├── Home.js (Modified) // 工事一覧ボタン追加
│ │ ├── ProjectList.js (New) // 工事一覧画面追加
│ │ └── ProjectDetail.js (New) // 工事詳細画面追加
│ ├── utils
│ │ ├── firebase.js (Unchanged)
│ │ └── data.js (Modified) // fetchProjectById とデータ構造対応追加
│ ├── App.js (Modified) // ルーティングに /projects, /projects/:id, /signup 追加
├── .env
├── package.json (Modified)
package.json(Modified)
{
"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",
"react-firebase-hooks": "^5.1.1" // Added
}
}
App.js(Modified)
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { auth } from './utils/firebase';
import { useAuthState } from 'react-firebase-hooks/auth'; // Added
import Home from './pages/Home';
import Login from './components/auth/Login';
import Signup from './components/auth/Signup'; // Added
import ProjectList from './pages/ProjectList'; // Added
import ProjectDetail from './pages/ProjectDetail'; // Added
const App = () => {
const [user, loading] = useAuthState(auth); // Added
if (loading) return <div>読み込み中...</div>; // Added
return (
<Router>
<Routes>
<Route path="/login" element={user ? <Home /> : <Login />} /> {/* Modified: 認証ガード追加 */}
<Route path="/signup" element={user ? <Home /> : <Signup />} /> {/* Added */}
<Route path="/" element={user ? <Home /> : <Login />} /> {/* Modified: 認証ガード追加 */}
<Route path="/projects" element={user ? <ProjectList /> : <Login />} /> {/* Added */}
<Route path="/projects/:id" element={user ? <ProjectDetail /> : <Login />} /> {/* Added */}
</Routes>
</Router>
);
};
export default App;
Login.js(Modified)
import React, { useState } from 'react';
import { signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth';
import { auth, googleProvider } from '../../utils/firebase';
import { useNavigate } from 'react-router-dom';
import { Box, TextField, Button, Typography, Paper } from '@mui/material';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const handleEmailLogin = async () => {
try {
await signInWithEmailAndPassword(auth, email, password);
navigate('/');
} catch (err) {
setError('ログインに失敗しました。メールまたはパスワードを確認してください。');
}
};
const handleGoogleLogin = async () => {
try {
await signInWithPopup(auth, googleProvider);
navigate('/');
} catch (err) {
setError('Googleログインに失敗しました。もう一度お試しください。');
}
};
return (
<Box sx={{ maxWidth: 400, mx: 'auto', mt: 8, p: 2 }}>
<Paper sx={{ p: 3 }}>
<Typography variant="h5" gutterBottom>ログイン</Typography>
{error && <Typography color="error">{error}</Typography>}
<TextField
label="メールアドレス"
fullWidth
margin="normal"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<TextField
label="パスワード"
type="password"
fullWidth
margin="normal"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<Button
variant="contained"
fullWidth
sx={{ mt: 2 }}
onClick={handleEmailLogin}
>
メールでログイン
</Button>
<Button
variant="outlined"
fullWidth
sx={{ mt: 2 }}
onClick={handleGoogleLogin}
>
Googleアカウントでログイン
</Button>
<Button
variant="text"
fullWidth
sx={{ mt: 2 }}
onClick={() => navigate('/signup')} // Added
>
新規登録
</Button>
</Paper>
</Box>
);
};
export default Login;
Signup.js(Modify)
import React, { useState } from 'react';
import { createUserWithEmailAndPassword } from 'firebase/auth';
import { auth } from '../../utils/firebase';
import { useNavigate } from 'react-router-dom';
import { Box, TextField, Button, Typography, Paper } from '@mui/material';
const Signup = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const navigate = useNavigate();
const handleSignup = async () => {
if (password !== confirmPassword) {
setError('パスワードが一致しません');
return;
}
try {
await createUserWithEmailAndPassword(auth, email, password);
navigate('/');
} catch (err) {
setError('登録に失敗しました。メールアドレスが既に使用されているか、パスワードが弱い可能性があります。');
}
};
return (
<Box sx={{ maxWidth: 400, mx: 'auto', mt: 8, p: 2 }}>
<Paper sx={{ p: 3 }}>
<Typography variant="h5" gutterBottom>新規登録</Typography>
{error && <Typography color="error">{error}</Typography>}
<TextField
label="メールアドレス"
fullWidth
margin="normal"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<TextField
label="パスワード"
type="password"
fullWidth
margin="normal"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<TextField
label="パスワード(確認)"
type="password"
fullWidth
margin="normal"
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
/>
<Button
variant="contained"
fullWidth
sx={{ mt: 2 }}
onClick={handleSignup}
>
登録
</Button>
<Button
variant="text"
fullWidth
sx={{ mt: 2 }}
onClick={() => navigate('/login')}
>
ログイン画面に戻る
</Button>
</Paper>
</Box>
);
};
export default Signup;
data.js(Modified)
import { collection, getDocs, addDoc, getDoc, doc } from 'firebase/firestore';
import { db } from './firebase';
export const fetchProjectData = async () => {
try {
const querySnapshot = await getDocs(collection(db, 'projects'));
return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
} catch (error) {
console.error('データ取得エラー:', error);
return [];
}
};
export const fetchProjectById = async (id) => { // Added
try {
const docRef = doc(db, 'projects', id);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
return { id: docSnap.id, ...docSnap.data() };
} else {
throw new Error('工事が見つかりません');
}
} catch (error) {
console.error('工事詳細取得エラー:', error);
return null;
}
};
export const addSampleData = async () => {
try {
await addDoc(collection(db, 'projects'), {
name: '工事B',
budget: 80000000,
usedBudget: 48000000,
progress: 60,
sales: 95000000,
costs: { labor: 25000000, material: 15000000, outsourcing: 8000000 },
});
console.log('工事Bデータを追加しました');
} catch (error) {
console.error('データ追加エラー:', error);
}
};
Home.js(Modified)
import React, { useEffect, useState } from 'react';
import { Bar, 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 { fetchProjectData, addSampleData } from '../utils/data';
ChartJS.register(...registerables, ChartDataLabels);
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 handleAddSampleData = async () => {
await addSampleData();
const data = await fetchProjectData();
setProjects(data);
};
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;
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)',
},
{
label: '使用済み予算 (万円)',
data: projects.map(p => (p.usedBudget || 0) / 10000),
backgroundColor: 'rgba(255, 99, 132, 0.6)',
},
],
};
const budgetOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' },
},
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: { display: true, text: '金額 (万円)' },
},
},
};
const progressData = {
labels: projects.map(p => p.name),
datasets: [
{
label: '進捗 (%)',
data: projects.map(p => p.progress || 0),
backgroundColor: 'rgba(54, 162, 235, 0.6)',
},
],
};
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,
],
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, mr: 2 }}>
Logout
</Button>
<Button variant="outlined" onClick={handleAddSampleData} sx={{ mb: 2, mr: 2 }}>
工事Bデータを追加
</Button>
<Button variant="outlined" onClick={() => navigate('/projects')} sx={{ mb: 2 }}> {/* Added */}
工事一覧
</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>
</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>
</Grid>
<Grid item xs={12}>
<Typography variant="h6">原価内訳</Typography>
<Box sx={{ height: chartHeight }}>
<Pie data={costData} options={costOptions} />
</Box>
</Grid>
</Grid>
</Box>
);
};
export default Home;
ProjectList.js(New)
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button } from '@mui/material';
import { fetchProjectData } from '../utils/data';
const ProjectList = () => {
const [projects, setProjects] = useState([]);
const navigate = useNavigate();
useEffect(() => {
const loadData = async () => {
const data = await fetchProjectData();
setProjects(data);
};
loadData();
}, []);
return (
<Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2 }}>
<Typography variant="h4" gutterBottom>工事一覧</Typography>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>工事名</TableCell>
<TableCell align="right">予算 (万円)</TableCell>
<TableCell align="right">使用済み予算 (万円)</TableCell>
<TableCell align="right">進捗 (%)</TableCell>
<TableCell align="right">売上 (万円)</TableCell>
<TableCell align="right">詳細</TableCell>
</TableRow>
</TableHead>
<TableBody>
{projects.map(project => (
<TableRow key={project.id}>
<TableCell>{project.name}</TableCell>
<TableCell align="right">{(project.budget / 10000).toFixed(1)}</TableCell>
<TableCell align="right">{(project.usedBudget / 10000).toFixed(1)}</TableCell>
<TableCell align="right">{project.progress}</TableCell>
<TableCell align="right">{(project.sales / 10000).toFixed(1)}</TableCell>
<TableCell align="right">
<Button variant="outlined" onClick={() => navigate(`/projects/${project.id}`)}>
詳細
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
};
export default ProjectList;
ProjectDetail.js(New)
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Box, Typography, Paper, Button, Grid } from '@mui/material';
import { fetchProjectById } from '../utils/data';
const ProjectDetail = () => {
const { id } = useParams();
const [project, setProject] = useState(null);
const navigate = useNavigate();
useEffect(() => {
const loadProject = async () => {
const data = await fetchProjectById(id);
setProject(data);
};
loadProject();
}, [id]);
if (!project) return <Typography>読み込み中...</Typography>;
return (
<Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2 }}>
<Typography variant="h4" gutterBottom>{project.name} の詳細</Typography>
<Button variant="outlined" onClick={() => navigate('/projects')} sx={{ mb: 2 }}>
工事一覧に戻る
</Button>
<Paper sx={{ p: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<Typography variant="h6">基本情報</Typography>
<Typography>予算: {(project.budget / 10000).toFixed(1)}万円</Typography>
<Typography>使用済み予算: {(project.usedBudget / 10000).toFixed(1)}万円</Typography>
<Typography>進捗: {project.progress}%</Typography>
<Typography>売上(予定): {(project.sales / 10000).toFixed(1)}万円</Typography>
</Grid>
<Grid item xs={12} sm={6}>
<Typography variant="h6">原価内訳</Typography>
<Typography>人件費: {(project.costs?.labor / 10000).toFixed(1)}万円</Typography>
<Typography>材料費: {(project.costs?.material / 10000).toFixed(1)}万円</Typography>
<Typography>外注費: {(project.costs?.outsourcing / 10000).toFixed(1)}万円</Typography>
<Typography>原価合計: {((project.costs?.labor + project.costs?.material + project.costs?.outsourcing) / 10000).toFixed(1)}万円</Typography>
</Grid>
</Grid>
</Paper>
</Box>
);
};
export default ProjectDetail;
以上、ここまで生成AI(Grok3)での出力です
とりあえず、また実装っと…
∧_∧
(・ω・ ) カタカタカタカタ
_| ̄ ̄||_))_
/旦|――||// /|
| ̄ ̄ ̄ ̄ ̄| ̄| . |
|_____|三|/
npm start ...っと
ん… エラー?
あ、package.jsonの中身変えてたんだった…
npm install してから
npm start っと
なるほど
ホームから工事一覧ページへはボタンで遷移にしたみたいですね
工事一覧は、まぁ件数とかあれだけどフィルタ機能つけないとだめだよなぁ
- フィルタ内容
- 工事金額(数値型 以上、未満)
- 部署
- 完成工事(日付 以前、以降)
- 繰越工事(工事の完成日が、期末日を超える条件)
- 進行基準、完成基準
後、部署情報を追加して部署毎にまとめたり集計できるようにしないとなぁ
というかやっぱりデータ設計しないとちゃんと表示させたいデータとか
入力値そのまま持ってくるのか計算して持ってくるのかとか設計が足りてないなぁ
詳細の画面については、元々作るために何のデータを入力させるのか
そこに進捗を入力させるとして、どのような入力形式にさせるのかとか
色々考えていかないとだめですね
まぁ、画面のプロトタイプ作りたかったから先に手を出したのですが
データ設計(せめてどのデータを集計して、出力させるのか…?)をしてから
画面詳細した方が手戻り少なくなりそう
まぁ、結局 後々設計変更とか言われる箇所なんでしょうけども
ちょっと長くなりそうですが
一応生成AIに修正プロンプトを投げてみます
1.工事データBの追加は別画面で行うので、Home.jsから削除
2.工事一覧に工事追加へのボタンを追加
工事追加ページを新規作成
工事で入力させるデータは
・工事名(※)
・部署(※)
・請負金額(税抜)
・請負金額(税込)
・発注者
・工事場所
・工期開始(カレンダー入力)(※)
・工期終了(カレンダー入力)(※)
・会計基準(ラジオボタンで ・完成基準 ・進行基準)(※)
を入力させる。
(※)が付いた所は必須入力
入力したデータをFireStoreへjson形式で保存する。
3.工事一覧に表示させるデータは工事追加ページで追加した情報をもとに表示させる。
4.工事一覧にフィルター機能を追加
このプロンプトで出力した結果が下記です
生成AI(Grok3)での出力です (先ほどの出力からの修正点で抜き出しています)
フォルダ構成
\projects\cbit
├── src
│ ├── components
│ │ └── auth
│ │ ├── Login.js (Unchanged)
│ │ └── Signup.js (Unchanged)
│ ├── pages
│ │ ├── Home.js (Modified) // 工事Bデータ追加ボタン削除
│ │ ├── ProjectList.js (Modified) // 工事追加ボタン、フィルター機能追加、表示項目更新
│ │ ├── ProjectDetail.js (Unchanged)
│ │ └── ProjectAdd.js (New) // 工事追加ページ追加
│ ├── utils
│ │ ├── firebase.js (Unchanged)
│ │ └── data.js (Modified) // 新規工事データ保存関数更新
│ ├── App.js (Modified) // /projects/add ルート追加
├── .env
├── package.json (Modified)
package.json (Modified)
{
"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",
"react-firebase-hooks": "^5.1.1",
"react-datepicker": "^7.3.0" // Added
}
}
App.js (Modified)
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { auth } from './utils/firebase';
import { useAuthState } from 'react-firebase-hooks/auth';
import Home from './pages/Home';
import Login from './components/auth/Login';
import Signup from './components/auth/Signup';
import ProjectList from './pages/ProjectList';
import ProjectDetail from './pages/ProjectDetail';
import ProjectAdd from './pages/ProjectAdd'; // Added
const App = () => {
const [user, loading] = useAuthState(auth);
if (loading) return <div>読み込み中...</div>;
return (
<Router>
<Routes>
<Route path="/login" element={user ? <Home /> : <Login />} />
<Route path="/signup" element={user ? <Home /> : <Signup />} />
<Route path="/" element={user ? <Home /> : <Login />} />
<Route path="/projects" element={user ? <ProjectList /> : <Login />} />
<Route path="/projects/:id" element={user ? <ProjectDetail /> : <Login />} />
<Route path="/projects/add" element={user ? <ProjectAdd /> : <Login />} /> {/* Added */}
</Routes>
</Router>
);
};
export default App;
data.js (Modified)
import { collection, getDocs, addDoc, getDoc, doc } from 'firebase/firestore';
import { db } from './firebase';
export const fetchProjectData = async () => {
try {
const querySnapshot = await getDocs(collection(db, 'projects'));
return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
} catch (error) {
console.error('データ取得エラー:', error);
return [];
}
};
export const fetchProjectById = async (id) => {
try {
const docRef = doc(db, 'projects', id);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
return { id: docSnap.id, ...docSnap.data() };
} else {
throw new Error('工事が見つかりません');
}
} catch (error) {
console.error('工事詳細取得エラー:', error);
return null;
}
};
export const addProject = async (projectData) => {
try {
await addDoc(collection(db, 'projects'), {
name: projectData.name,
department: projectData.department,
contractAmountExTax: projectData.contractAmountExTax || 0,
contractAmountIncTax: projectData.contractAmountIncTax || 0,
client: projectData.client || '',
location: projectData.location || '',
startDate: projectData.startDate,
endDate: projectData.endDate,
accountingStandard: projectData.accountingStandard,
});
console.log('工事データを追加しました');
} catch (error) {
console.error('データ追加エラー:', error);
}
};
Home.js (Modified)
import React, { useEffect, useState } from 'react';
import { Bar, 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 { fetchProjectData } from '../utils/data';
ChartJS.register(...registerables, ChartDataLabels);
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.contractAmountIncTax || 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;
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)',
},
{
label: '使用済み予算 (万円)',
data: projects.map(p => (p.usedBudget || 0) / 10000),
backgroundColor: 'rgba(255, 99, 132, 0.6)',
},
],
};
const budgetOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' },
},
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: { display: true, text: '金額 (万円)' },
},
},
};
const progressData = {
labels: projects.map(p => p.name),
datasets: [
{
label: '進捗 (%)',
data: projects.map(p => p.progress || 0),
backgroundColor: 'rgba(54, 162, 235, 0.6)',
},
],
};
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,
],
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, mr: 2 }}>
Logout
</Button>
<Button variant="outlined" onClick={() => navigate('/projects')} sx={{ mb: 2 }}>
工事一覧
</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>
</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>
</Grid>
<Grid item xs={12}>
<Typography variant="h6">原価内訳</Typography>
<Box sx={{ height: chartHeight }}>
<Pie data={costData} options={costOptions} />
</Box>
</Grid>
</Grid>
</Box>
);
};
export default Home;
ProjectList.js (Modified)
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button, TextField } from '@mui/material';
import { fetchProjectData } from '../utils/data';
const ProjectList = () => {
const [projects, setProjects] = useState([]);
const [filteredProjects, setFilteredProjects] = useState([]);
const [filters, setFilters] = useState({ name: '', department: '', client: '' });
const navigate = useNavigate();
useEffect(() => {
const loadData = async () => {
const data = await fetchProjectData();
setProjects(data);
setFilteredProjects(data);
};
loadData();
}, []);
useEffect(() => {
const filtered = projects.filter(project =>
project.name.toLowerCase().includes(filters.name.toLowerCase()) &&
project.department.toLowerCase().includes(filters.department.toLowerCase()) &&
project.client.toLowerCase().includes(filters.client.toLowerCase())
);
setFilteredProjects(filtered);
}, [filters, projects]);
const handleFilterChange = (e) => {
const { name, value } = e.target;
setFilters(prev => ({ ...prev, [name]: value }));
};
return (
<Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2 }}>
<Typography variant="h4" gutterBottom>工事一覧</Typography>
<Button variant="outlined" onClick={() => navigate('/projects/add')} sx={{ mb: 2 }}>
工事追加
</Button>
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
<TextField
label="工事名で検索"
name="name"
value={filters.name}
onChange={handleFilterChange}
size="small"
/>
<TextField
label="部署で検索"
name="department"
value={filters.department}
onChange={handleFilterChange}
size="small"
/>
<TextField
label="発注者で検索"
name="client"
value={filters.client}
onChange={handleFilterChange}
size="small"
/>
</Box>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>工事名</TableCell>
<TableCell align="right">部署</TableCell>
<TableCell align="right">請負金額(税込) (万円)</TableCell>
<TableCell align="right">発注者</TableCell>
<TableCell align="right">工期開始</TableCell>
<TableCell align="right">詳細</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredProjects.map(project => (
<TableRow key={project.id}>
<TableCell>{project.name}</TableCell>
<TableCell align="right">{project.department}</TableCell>
<TableCell align="right">{(project.contractAmountIncTax / 10000).toFixed(1)}</TableCell>
<TableCell align="right">{project.client}</TableCell>
<TableCell align="right">{project.startDate ? new Date(project.startDate).toLocaleDateString('ja-JP') : '-'}</TableCell>
<TableCell align="right">
<Button variant="outlined" onClick={() => navigate(`/projects/${project.id}`)}>
詳細
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
};
export default ProjectList;
ProjectAdd.js (New)
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Typography, Paper, Button, TextField, Radio, RadioGroup, FormControlLabel, FormControl, FormLabel } from '@mui/material';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { addProject } from '../utils/data';
const ProjectAdd = () => {
const [formData, setFormData] = useState({
name: '',
department: '',
contractAmountExTax: '',
contractAmountIncTax: '',
client: '',
location: '',
startDate: null,
endDate: null,
accountingStandard: 'completion',
});
const [error, setError] = useState('');
const navigate = useNavigate();
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async () => {
if (!formData.name || !formData.department || !formData.startDate || !formData.endDate || !formData.accountingStandard) {
setError('必須項目(工事名、部署、工期開始、工期終了、会計基準)を入力してください');
return;
}
try {
await addProject({
...formData,
contractAmountExTax: parseFloat(formData.contractAmountExTax) || 0,
contractAmountIncTax: parseFloat(formData.contractAmountIncTax) || 0,
});
navigate('/projects');
} catch (err) {
setError('工事データの追加に失敗しました');
}
};
return (
<Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2 }}>
<Typography variant="h4" gutterBottom>工事追加</Typography>
<Button variant="outlined" onClick={() => navigate('/projects')} sx={{ mb: 2 }}>
工事一覧に戻る
</Button>
<Paper sx={{ p: 3 }}>
{error && <Typography color="error">{error}</Typography>}
<TextField
label="工事名 (必須)"
name="name"
value={formData.name}
onChange={handleChange}
fullWidth
margin="normal"
required
/>
<TextField
label="部署 (必須)"
name="department"
value={formData.department}
onChange={handleChange}
fullWidth
margin="normal"
required
/>
<TextField
label="請負金額(税抜) (円)"
name="contractAmountExTax"
type="number"
value={formData.contractAmountExTax}
onChange={handleChange}
fullWidth
margin="normal"
/>
<TextField
label="請負金額(税込) (円)"
name="contractAmountIncTax"
type="number"
value={formData.contractAmountIncTax}
onChange={handleChange}
fullWidth
margin="normal"
/>
<TextField
label="発注者"
name="client"
value={formData.client}
onChange={handleChange}
fullWidth
margin="normal"
/>
<TextField
label="工事場所"
name="location"
value={formData.location}
onChange={handleChange}
fullWidth
margin="normal"
/>
<Box sx={{ mt: 2 }}>
<Typography>工期開始 (必須)</Typography>
<DatePicker
selected={formData.startDate}
onChange={date => setFormData(prev => ({ ...prev, startDate: date }))}
dateFormat="yyyy/MM/dd"
customInput={<TextField fullWidth margin="normal" required />}
/>
</Box>
<Box sx={{ mt: 2 }}>
<Typography>工期終了 (必須)</Typography>
<DatePicker
selected={formData.endDate}
onChange={date => setFormData(prev => ({ ...prev, endDate: date }))}
dateFormat="yyyy/MM/dd"
customInput={<TextField fullWidth margin="normal" required />}
/>
</Box>
<FormControl component="fieldset" sx={{ mt: 2 }} required>
<FormLabel component="legend">会計基準 (必須)</FormLabel>
<RadioGroup
name="accountingStandard"
value={formData.accountingStandard}
onChange={handleChange}
row
>
<FormControlLabel value="completion" control={<Radio />} label="完成基準" />
<FormControlLabel value="progress" control={<Radio />} label="進行基準" />
</RadioGroup>
</FormControl>
<Button
variant="contained"
fullWidth
sx={{ mt: 2 }}
onClick={handleSubmit}
>
追加
</Button>
</Paper>
</Box>
);
};
export default ProjectAdd;
ここまでが修正内容となっていました
とりあえず、修正して…
依存関係も変わっていたので、npm install から再度実行っと
npm start ...
お、今度はコンパイルエラー無かったー
けど、ダッシュボードから工事一覧に行く際、エラー吐くな…
これ、工事一覧で出力するデータを登録するデータをベースにしたせいで
初期データが入ってないな?
初期データをサンプルで準備してもらうか…
/)
///)
/,.=゙''"/
/ i f ,.r='"-‐'つ____ こまけぇこたぁいいんだよ!!
/ / _,.-‐'~/⌒ ⌒\
/ ,i ,二ニ⊃( >). (<)\
/ ノ il゙フ::::::⌒(__人__)⌒::::: \
,イ「ト、 ,!,!| |r┬-| |
/ iトヾヽ_/ィ"\ `ー'´ /
いや、いっそのこと工事一覧に表示させるデータを修正するのと
経費入力のページを作ってもらって、経費は入力したデータの積上げで
計算するようにしてもらおう。
ちょっと長くなりすぎたので、一旦今回はここまでで。
次回は、工事一覧ページの改良及び経費入力ページ対応から始めます