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-5 (フロントエンド開発 工事一覧~工事詳細)

Posted at

前回のおさらい

前回やったこと

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

今回やること

  • 工事一覧画面作成(登録についても追加?)
  • 工事詳細画面作成

ここあたりかなと
image.png

画面作成していると、色々見えてくる部分もやっぱりありますね

ログイン権限設定によって、ダッシュボードからの表示にするか
管理者は工事管理からなのか、作業者レベルだと工事詳細で工程管理辺りなのか?とか

正直、権限レベルでってのは少し考えてたけどフローとかを考えられてなかったので
後々機能追加でやっていくしかなさそうかなー

とりあえず、今回作る部分について考えます


  • 工事一覧
    見せる情報としては工事名、部署、売上予定金額、担当者名、工事着工日
    工事完成日(竣工日と引渡日を分けるか?)あたりが見えれば大丈夫かなと
    できれば、今期完成工事と繰越工事でも分けて見られるようにしたいかなぁ
    と、なると決算期なんかの会社情報も必要になるか…

  • 工事詳細
    工事一覧の工事単位のドリルダウン 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)

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",
    "react-firebase-hooks": "^5.1.1" // Added
  }
}

App.js(Modified)

App.js
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)

Login.js
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)

Signup.js
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)

data.js
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)

Home.js
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)

ProjectList.js
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)

ProjectDetail.js
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 っと

追加された画面
image.png

image.png

image.png

なるほど
ホームから工事一覧ページへはボタンで遷移にしたみたいですね
工事一覧は、まぁ件数とかあれだけどフィルタ機能つけないとだめだよなぁ

  • フィルタ内容
    • 工事金額(数値型 以上、未満)
    • 部署
    • 完成工事(日付 以前、以降)
    • 繰越工事(工事の完成日が、期末日を超える条件)
    • 進行基準、完成基準

後、部署情報を追加して部署毎にまとめたり集計できるようにしないとなぁ
というかやっぱりデータ設計しないとちゃんと表示させたいデータとか
入力値そのまま持ってくるのか計算して持ってくるのかとか設計が足りてないなぁ

詳細の画面については、元々作るために何のデータを入力させるのか
そこに進捗を入力させるとして、どのような入力形式にさせるのかとか
色々考えていかないとだめですね

まぁ、画面のプロトタイプ作りたかったから先に手を出したのですが
データ設計(せめてどのデータを集計して、出力させるのか…?)をしてから
画面詳細した方が手戻り少なくなりそう
まぁ、結局 後々設計変更とか言われる箇所なんでしょうけども

ちょっと長くなりそうですが
一応生成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)

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",
    "react-firebase-hooks": "^5.1.1",
    "react-datepicker": "^7.3.0" // Added
  }
}

App.js (Modified)

App.js
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)

data.js
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)

Home.js
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)

ProjectList.js
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)

ProjectAdd.js
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トヾヽ_/ィ"\      `ー'´     /

いや、いっそのこと工事一覧に表示させるデータを修正するのと
経費入力のページを作ってもらって、経費は入力したデータの積上げで
計算するようにしてもらおう。

ちょっと長くなりすぎたので、一旦今回はここまでで。

次回は、工事一覧ページの改良及び経費入力ページ対応から始めます

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?