1
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-7 (フロントエンド開発 工事一覧修正~データ入力確認)

Posted at

私的所要の為、一旦更新しなくなります。
10月終わりくらいまでは多分無理かな…

前回やったこと

  • 工事一覧画面作成(エラー解消と同時に)
  • 経費入力画面作成

今回やること

  • 工事一覧の工期開始終了の表示修正、会計基準の英語表示を日本語表示へ
  • 工事詳細の所に入力した経費一覧が欲しい(できれば検索できるようにしたい)
  • 追加した情報について、反映されることの確認

※工程表については別の機能として実装します

遷移図に関して、修正する項目は変わらないので省略

とりあえず、今回やることについて
Grok君にプロンプトでお願いしてみようと思います

工事一覧ページの工期開始と終了が InvalidDateと表示される
工期開始、終了はYYYY/MM/DD 形式で表示するように変更
データを保持する際もYYYY/MM/DD形式に変更
会計基準の表示を英語ではなく日本語に変更

工事詳細ページに、現在までに入力した経費の一覧を表示したい

生成AI(Grok3)による出力です(例によって、変更点中心に抽出)

フォルダ構成

C:\projects\cbit
├── src
│   ├── components
│   │   └── auth
│   │       ├── Login.js (Unchanged)
│   │       └── Signup.js (Unchanged)
│   ├── pages
│   │   ├── Home.js (Unchanged)
│   │   ├── ProjectList.js (Modified)        // 日付表示をYYYY/MM/DDに
│   │   ├── ProjectDetail.js (Modified)      // 経費一覧表示を追加
│   │   ├── ProjectAdd.js (Unchanged)
│   │   └── ProjectExpenseAdd.js (Unchanged)
│   ├── utils
│   │   ├── firebase.js (Unchanged)
│   │   ├── data.js (Modified)               // 文字列形式の日付に対応
│   │   └── addSampleData.js (Modified)      // テストデータを文字列形式に
│   ├── App.js (Unchanged)
├── .env
├── package.json

addSampleData.js (Modified)

addSampleData.js
const { getFirestore, collection, addDoc, setDoc, doc, getDocs, query, where, deleteDoc, Timestamp } = require('firebase/firestore');
const { initializeApp } = require('firebase/app');
require('dotenv').config();

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_APP_ID,
};

console.log('Firebase Config:', firebaseConfig);

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

const testData = [
  {
    name: "工事A",
    department: "土木部",
    contractAmountExTax: 100000000,
    contractAmountIncTax: 110000000,
    budget: 90000000,
    client: "株式会社サンプル",
    location: "東京都中央区",
    startDate: Timestamp.fromDate(new Date(2025, 0, 1)),
    endDate: Timestamp.fromDate(new Date(2025, 11, 31)),
    accountingStandard: "completion",
    progress: 80,
    expenses: [
      {
        occurrenceDate: Timestamp.fromDate(new Date(2025, 1, 1)),
        amount: 20000000,
        description: "外注費",
      },
      {
        occurrenceDate: Timestamp.fromDate(new Date(2025, 2, 1)),
        amount: 15000000,
        description: "材料費",
      },
    ],
  },
  {
    name: "工事B",
    department: "建築部",
    contractAmountExTax: 80000000,
    contractAmountIncTax: 88000000,
    budget: 70000000,
    client: "建設興業株式会社",
    location: "大阪府大阪市",
    startDate: Timestamp.fromDate(new Date(2025, 3, 1)),
    endDate: Timestamp.fromDate(new Date(2025, 9, 31)),
    accountingStandard: "progress",
    progress: 50,
  },
];

async function addTestData() {
  try {
    for (const project of testData) {
      if (!project.name || !project.department || !project.contractAmountIncTax || !project.startDate || !project.endDate || !project.accountingStandard) {
        console.error(`Invalid project data for ${project.name}: missing required fields`);
        continue;
      }

      const projectData = {
        name: project.name,
        department: project.department,
        contractAmountExTax: Number(project.contractAmountExTax) || 0,
        contractAmountIncTax: Number(project.contractAmountIncTax) || 0,
        budget: Number(project.budget) || 0,
        client: project.client || '',
        location: project.location || '',
        startDate: project.startDate,
        endDate: project.endDate,
        accountingStandard: project.accountingStandard,
        progress: Number(project.progress) || 0,
      };

      const projectsRef = collection(db, 'projects');
      const q = query(projectsRef, where('name', '==', project.name));
      const querySnapshot = await getDocs(q);

      let projectRef;
      if (!querySnapshot.empty) {
        projectRef = doc(db, 'projects', querySnapshot.docs[0].id);
        await setDoc(projectRef, projectData);
        console.log(`Updated project: ${project.name} with ID: ${projectRef.id}`);
      } else {
        projectRef = await addDoc(projectsRef, projectData);
        console.log(`Added project: ${project.name} with ID: ${projectRef.id}`);
      }

      const expensesRef = collection(db, 'projects', projectRef.id, 'expenses');
      const existingExpenses = await getDocs(expensesRef);
      for (const expenseDoc of existingExpenses.docs) {
        await deleteDoc(doc(db, 'projects', projectRef.id, 'expenses', expenseDoc.id));
      }
      console.log(`Cleared existing expenses for ${project.name}`);

      if (project.expenses && project.expenses.length > 0) {
        for (const expense of project.expenses) {
          if (!expense.occurrenceDate || !expense.amount || !expense.description) {
            console.error(`Invalid expense data for ${project.name}: missing required fields`);
            continue;
          }
          await addDoc(expensesRef, {
            occurrenceDate: expense.occurrenceDate,
            amount: Number(expense.amount) || 0,
            description: expense.description,
          });
          console.log(`Added expense for ${project.name}: ${expense.description}`);
        }
      }
    }
    console.log('All data processed successfully');
  } catch (error) {
    console.error('Error processing data:', error);
  }
}

addTestData();

data.js (Modified)

data.js
import { collection, getDocs, addDoc, getDoc, doc } from 'firebase/firestore';
import { db } from './firebase';
import { Timestamp } from 'firebase/firestore';

export const fetchProjectData = async () => {
  try {
    const querySnapshot = await getDocs(collection(db, 'projects'));
    const projects = querySnapshot.docs
      .map(doc => ({ id: doc.id, ...doc.data() }))
      .filter(project => {
        return (
          typeof project.name === 'string' &&
          project.name &&
          typeof project.department === 'string' &&
          project.department &&
          typeof project.contractAmountIncTax === 'number' &&
          project.startDate instanceof Timestamp &&
          project.endDate instanceof Timestamp &&
          typeof project.accountingStandard === 'string' &&
          ['completion', 'progress'].includes(project.accountingStandard)
        );
      })
      .map(project => ({
        ...project,
        client: project.client || '',
        location: project.location || '',
        budget: project.budget || 0,
        contractAmountExTax: project.contractAmountExTax || 0,
        progress: Number(project.progress) || 0,
      }));
    return projects;
  } 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()) {
      const project = { id: docSnap.id, ...docSnap.data() };
      if (
        typeof project.name !== 'string' ||
        !project.name ||
        typeof project.department !== 'string' ||
        !project.department ||
        typeof project.contractAmountIncTax !== 'number' ||
        !(project.startDate instanceof Timestamp) ||
        !(project.endDate instanceof Timestamp) ||
        !['completion', 'progress'].includes(project.accountingStandard)
      ) {
        throw new Error('無効な工事データ');
      }
      return {
        ...project,
        client: project.client || '',
        location: project.location || '',
        budget: project.budget || 0,
        contractAmountExTax: project.contractAmountExTax || 0,
        progress: Number(project.progress) || 0,
      };
    } 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,
      budget: projectData.budget || 0,
      client: projectData.client || '',
      location: projectData.location || '',
      startDate: projectData.startDate,
      endDate: projectData.endDate,
      accountingStandard: projectData.accountingStandard,
      progress: Number(projectData.progress) || 0,
    });
    console.log('工事データを追加しました');
  } catch (error) {
    console.error('データ追加エラー:', error);
  }
};

export const addExpense = async (projectId, expenseData) => {
  try {
    const expenseRef = collection(db, 'projects', projectId, 'expenses');
    await addDoc(expenseRef, {
      occurrenceDate: expenseData.occurrenceDate,
      amount: expenseData.amount || 0,
      description: expenseData.description,
    });
    console.log('経費を追加しました');
  } catch (error) {
    console.error('経費追加エラー:', error);
  }
};

export const getTotalExpenses = async (projectId) => {
  try {
    const expenseRef = collection(db, 'projects', projectId, 'expenses');
    const querySnapshot = await getDocs(expenseRef);
    if (querySnapshot.empty) return 0;
    return querySnapshot.docs.reduce((sum, doc) => sum + (doc.data().amount || 0), 0);
  } catch (error) {
    console.error('経費合算エラー:', error);
    return 0;
  }
};

export const getCostBreakdown = async (projectId) => {
  try {
    const expenseRef = collection(db, 'projects', projectId, 'expenses');
    const querySnapshot = await getDocs(expenseRef);
    const breakdown = {
      outsourcing: 0,
      labor: 0,
      material: 0,
      other: 0,
    };
    querySnapshot.forEach(doc => {
      const expense = doc.data();
      const amount = Number(expense.amount) || 0;
      switch (expense.description) {
        case '外注費':
          breakdown.outsourcing += amount;
          break;
        case '労務費':
          breakdown.labor += amount;
          break;
        case '材料費':
          breakdown.material += amount;
          break;
        case 'その他':
          breakdown.other += amount;
          break;
        default:
          break;
      }
    });
    return breakdown;
  } catch (error) {
    console.error('原価内訳取得エラー:', error);
    return { outsourcing: 0, labor: 0, material: 0, other: 0 };
  }
};

export const fetchExpenses = async (projectId) => {
  try {
    const expenseRef = collection(db, 'projects', projectId, 'expenses');
    const querySnapshot = await getDocs(expenseRef);
    const expenses = querySnapshot.docs
      .map(doc => ({ id: doc.id, ...doc.data() }))
      .filter(expense => (
        expense.occurrenceDate instanceof Timestamp &&
        typeof expense.amount === 'number' &&
        typeof expense.description === 'string'
      ))
      .sort((a, b) => a.occurrenceDate.toDate() - b.occurrenceDate.toDate()); // 発生日でソート
    return expenses;
  } catch (error) {
    console.error('経費一覧取得エラー:', error);
    return [];
  }
};

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, CircularProgress } from '@mui/material';
import { fetchProjectData, getTotalExpenses } from '../utils/data';

// 会計基準を日本語に変換
const translateAccountingStandard = (standard) => {
  switch (standard) {
    case 'completion':
      return '完成基準';
    case 'progress':
      return '進捗基準';
    default:
      return standard || '-';
  }
};

const ProjectList = () => {
  const [projects, setProjects] = useState([]);
  const [filteredProjects, setFilteredProjects] = useState([]);
  const [filters, setFilters] = useState({ name: '', department: '', client: '' });
  const [loading, setLoading] = useState(true);
  const navigate = useNavigate();

  useEffect(() => {
    const loadData = async () => {
      setLoading(true);
      const data = await fetchProjectData();
      const projectsWithExpenses = await Promise.all(data.map(async (project) => {
        const totalExpenses = await getTotalExpenses(project.id);
        return { ...project, totalExpenses: totalExpenses || 0 };
      }));
      setProjects(projectsWithExpenses);
      setFilteredProjects(projectsWithExpenses);
      setLoading(false);
    };
    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 }));
  };

  if (loading) {
    return (
      <Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2, textAlign: 'center' }}>
        <CircularProgress />
        <Typography>読み込み中...</Typography>
      </Box>
    );
  }

  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>
              <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.budget / 10000).toFixed(1)}</TableCell>
                <TableCell align="right">{((project.totalExpenses || 0) / 10000).toFixed(1)}</TableCell>
                <TableCell align="right">{project.client}</TableCell>
                <TableCell align="right">{project.location}</TableCell>
                <TableCell align="right">{project.startDate ? project.startDate.toDate().toLocaleDateString('ja-JP').replace(/-/g, '/') : '-'}</TableCell>
                <TableCell align="right">{project.endDate ? project.endDate.toDate().toLocaleDateString('ja-JP').replace(/-/g, '/') : '-'}</TableCell>
                <TableCell align="right">{translateAccountingStandard(project.accountingStandard)}</TableCell>
                <TableCell align="right">
                  <Button variant="outlined" onClick={() => navigate(`/projects/${project.id}`)}>
                    詳細
                  </Button>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TableContainer>
    </Box>
  );
};

export default ProjectList;

ProjectDetail.js (Modified)

ProjectDetail.js
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Box, Typography, Button, CircularProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material';
import { fetchProjectById, getTotalExpenses, getCostBreakdown, fetchExpenses } from '../utils/data';

// 会計基準を日本語に変換
const translateAccountingStandard = (standard) => {
  switch (standard) {
    case 'completion':
      return '完成基準';
    case 'progress':
      return '進捗基準';
    default:
      return standard || '-';
  }
};

const ProjectDetail = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const [project, setProject] = useState(null);
  const [totalExpenses, setTotalExpenses] = useState(0);
  const [costBreakdown, setCostBreakdown] = useState(null);
  const [expenses, setExpenses] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const loadData = async () => {
      setLoading(true);
      const projectData = await fetchProjectById(id);
      if (projectData) {
        const expensesTotal = await getTotalExpenses(id);
        const breakdown = await getCostBreakdown(id);
        const expenseList = await fetchExpenses(id);
        setProject(projectData);
        setTotalExpenses(expensesTotal);
        setCostBreakdown(breakdown);
        setExpenses(expenseList);
      }
      setLoading(false);
    };
    loadData();
  }, [id]);

  if (loading) {
    return (
      <Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2, textAlign: 'center' }}>
        <CircularProgress />
        <Typography>読み込み中...</Typography>
      </Box>
    );
  }

  if (!project) {
    return (
      <Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2 }}>
        <Typography variant="h6">工事が見つかりません</Typography>
        <Button variant="outlined" onClick={() => navigate('/projects')}>
          工事一覧に戻る
        </Button>
      </Box>
    );
  }

  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, mr: 2 }}>
        工事一覧に戻る
      </Button>
      <Button variant="outlined" onClick={() => navigate(`/projects/${id}/expense/add`)} sx={{ mb: 2 }}>
        経費追加
      </Button>
      <Box sx={{ mb: 3 }}>
        <Typography variant="h6">工事詳細</Typography>
        <Typography>部署: {project.department}</Typography>
        <Typography>受注金額税込: {(project.contractAmountIncTax / 10000).toFixed(1)}万円</Typography>
        <Typography>実行予算: {(project.budget / 10000).toFixed(1)}万円</Typography>
        <Typography>使用済み経費: {(totalExpenses / 10000).toFixed(1)}万円</Typography>
        <Typography>発注者: {project.client}</Typography>
        <Typography>工事場所: {project.location}</Typography>
        <Typography>工期開始: {project.startDate ? project.startDate.toDate().toLocaleDateString('ja-JP').replace(/-/g, '/') : '-'}</Typography>
        <Typography>工期終了: {project.endDate ? project.endDate.toDate().toLocaleDateString('ja-JP').replace(/-/g, '/') : '-'}</Typography>
        <Typography>会計基準: {translateAccountingStandard(project.accountingStandard)}</Typography>
        <Typography>進捗: {project.progress}%</Typography>
      </Box>
      <Box sx={{ mb: 3 }}>
        <Typography variant="h6">原価内訳</Typography>
        <Typography>外注費: {(costBreakdown.outsourcing / 10000).toFixed(1)}万円</Typography>
        <Typography>労務費: {(costBreakdown.labor / 10000).toFixed(1)}万円</Typography>
        <Typography>材料費: {(costBreakdown.material / 10000).toFixed(1)}万円</Typography>
        <Typography>その他: {(costBreakdown.other / 10000).toFixed(1)}万円</Typography>
      </Box>
      <Box>
        <Typography variant="h6" gutterBottom>経費一覧</Typography>
        {expenses.length === 0 ? (
          <Typography>経費データがありません</Typography>
        ) : (
          <TableContainer component={Paper}>
            <Table>
              <TableHead>
                <TableRow>
                  <TableCell>発生日</TableCell>
                  <TableCell align="right">金額 (万円)</TableCell>
                  <TableCell align="right">経費概要</TableCell>
                </TableRow>
              </TableHead>
              <TableBody>
                {expenses.map(expense => (
                  <TableRow key={expense.id}>
                    <TableCell>{expense.occurrenceDate ? expense.occurrenceDate.toDate().toLocaleDateString('ja-JP').replace(/-/g, '/') : '-'}</TableCell>
                    <TableCell align="right">{(expense.amount / 10000).toFixed(1)}</TableCell>
                    <TableCell align="right">{expense.description}</TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          </TableContainer>
        )}
      </Box>
    </Box>
  );
};

export default ProjectDetail;

以上が、生成AI(Grok3)による出力でした

とりあえず、修正コードをコピペして、Firestoreのコレクション情報を一旦削除して
テストデータを追加しなおして、実行してみました

修正した画面

image.png
image.png

んー悪くはない、悪くはないんだけど…
具体的な表示スペースの問題とか、経費一覧の表記問題とかは一旦置いといて
とりあえずは修正し、内容的には表示できるようになったかなと

後、よく見たら、会計基準の「進行基準」が「進捗基準」になってるな…
登録するときは英語になってるし…
流石に入出力のデータくらいは合わせた方がいいよな…
というかデータの取り扱いに関しては同期してほしいな…

という事でこの部分だけAI君に投げます

会計基準について、「完成」と「進行」の2種類で取り扱うようにしてください。
データの登録、入出力、表示の時も上記2種に統一してください
工事登録ページ、経費入力ページなども合わせて修正してください。

生成AI(Grok3)による出力です

フォルダ構成

フォルダ構成
C:\projects\cbit
├── src
│   ├── components
│   │   └── auth
│   │       ├── Login.js (Unchanged)
│   │       └── Signup.js (Unchanged)
│   ├── pages
│   │   ├── Home.js (Unchanged)
│   │   ├── ProjectList.js (Modified)        // 会計基準を日本語直接表示
│   │   ├── ProjectDetail.js (Modified)      // 会計基準を日本語直接表示
│   │   ├── ProjectAdd.js (Modified)         // 工事追加フォームを修正
│   │   └── ProjectExpenseAdd.js (Modified)  // 経費追加フォームを修正
│   ├── utils
│   │   ├── firebase.js (Unchanged)
│   │   ├── data.js (Modified)               // 日本語会計基準に対応、addProject, addExpenseを修正
│   │   └── addSampleData.js (Modified)      // テストデータを日本語会計基準に
│   ├── App.js (Modified)                    // ルート確認/追加
├── .env
├── package.json

addSampleData.js (Modified)

addSampleData.js
const { getFirestore, collection, addDoc, setDoc, doc, getDocs, query, where, deleteDoc, Timestamp } = require('firebase/firestore');
const { initializeApp } = require('firebase/app');
require('dotenv').config();

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_APP_ID,
};

console.log('Firebase Config:', firebaseConfig);

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

const testData = [
  {
    name: "工事A",
    department: "土木部",
    contractAmountExTax: 100000000,
    contractAmountIncTax: 110000000,
    budget: 90000000,
    client: "株式会社サンプル",
    location: "東京都中央区",
    startDate: Timestamp.fromDate(new Date(2025, 0, 1)),
    endDate: Timestamp.fromDate(new Date(2025, 11, 31)),
    accountingStandard: "完成",
    progress: 80,
    expenses: [
      {
        occurrenceDate: Timestamp.fromDate(new Date(2025, 1, 1)),
        amount: 20000000,
        description: "外注費",
      },
      {
        occurrenceDate: Timestamp.fromDate(new Date(2025, 2, 1)),
        amount: 15000000,
        description: "材料費",
      },
    ],
  },
  {
    name: "工事B",
    department: "建築部",
    contractAmountExTax: 80000000,
    contractAmountIncTax: 88000000,
    budget: 70000000,
    client: "建設興業株式会社",
    location: "大阪府大阪市",
    startDate: Timestamp.fromDate(new Date(2025, 3, 1)),
    endDate: Timestamp.fromDate(new Date(2025, 9, 31)),
    accountingStandard: "進行",
    progress: 50,
  },
];

async function addTestData() {
  try {
    for (const project of testData) {
      if (!project.name || !project.department || !project.contractAmountIncTax || !project.startDate || !project.endDate || !project.accountingStandard) {
        console.error(`Invalid project data for ${project.name}: missing required fields`);
        continue;
      }

      const projectData = {
        name: project.name,
        department: project.department,
        contractAmountExTax: Number(project.contractAmountExTax) || 0,
        contractAmountIncTax: Number(project.contractAmountIncTax) || 0,
        budget: Number(project.budget) || 0,
        client: project.client || '',
        location: project.location || '',
        startDate: project.startDate,
        endDate: project.endDate,
        accountingStandard: project.accountingStandard,
        progress: Number(project.progress) || 0,
      };

      const projectsRef = collection(db, 'projects');
      const q = query(projectsRef, where('name', '==', project.name));
      const querySnapshot = await getDocs(q);

      let projectRef;
      if (!querySnapshot.empty) {
        projectRef = doc(db, 'projects', querySnapshot.docs[0].id);
        await setDoc(projectRef, projectData);
        console.log(`Updated project: ${project.name} with ID: ${projectRef.id}`);
      } else {
        projectRef = await addDoc(projectsRef, projectData);
        console.log(`Added project: ${project.name} with ID: ${projectRef.id}`);
      }

      const expensesRef = collection(db, 'projects', projectRef.id, 'expenses');
      const existingExpenses = await getDocs(expensesRef);
      for (const expenseDoc of existingExpenses.docs) {
        await deleteDoc(doc(db, 'projects', projectRef.id, 'expenses', expenseDoc.id));
      }
      console.log(`Cleared existing expenses for ${project.name}`);

      if (project.expenses && project.expenses.length > 0) {
        for (const expense of project.expenses) {
          if (!expense.occurrenceDate || !expense.amount || !expense.description) {
            console.error(`Invalid expense data for ${project.name}: missing required fields`);
            continue;
          }
          await addDoc(expensesRef, {
            occurrenceDate: expense.occurrenceDate,
            amount: Number(expense.amount) || 0,
            description: expense.description,
          });
          console.log(`Added expense for ${project.name}: ${expense.description}`);
        }
      }
    }
    console.log('All data processed successfully');
  } catch (error) {
    console.error('Error processing data:', error);
  }
}

addTestData();

data.js (Modified)

data.js
import { collection, getDocs, addDoc, getDoc, doc } from 'firebase/firestore';
import { db } from './firebase';
import { Timestamp } from 'firebase/firestore';

export const fetchProjectData = async () => {
  try {
    const querySnapshot = await getDocs(collection(db, 'projects'));
    const projects = querySnapshot.docs
      .map(doc => ({ id: doc.id, ...doc.data() }))
      .filter(project => {
        return (
          typeof project.name === 'string' &&
          project.name &&
          typeof project.department === 'string' &&
          project.department &&
          typeof project.contractAmountIncTax === 'number' &&
          project.startDate instanceof Timestamp &&
          project.endDate instanceof Timestamp &&
          typeof project.accountingStandard === 'string' &&
          ['完成', '進行'].includes(project.accountingStandard)
        );
      })
      .map(project => ({
        ...project,
        client: project.client || '',
        location: project.location || '',
        budget: project.budget || 0,
        contractAmountExTax: project.contractAmountExTax || 0,
        progress: Number(project.progress) || 0,
      }));
    return projects;
  } 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()) {
      const project = { id: docSnap.id, ...docSnap.data() };
      if (
        typeof project.name !== 'string' ||
        !project.name ||
        typeof project.department !== 'string' ||
        !project.department ||
        typeof project.contractAmountIncTax !== 'number' ||
        !(project.startDate instanceof Timestamp) ||
        !(project.endDate instanceof Timestamp) ||
        !['完成', '進行'].includes(project.accountingStandard)
      ) {
        throw new Error('無効な工事データ');
      }
      return {
        ...project,
        client: project.client || '',
        location: project.location || '',
        budget: project.budget || 0,
        contractAmountExTax: project.contractAmountExTax || 0,
        progress: Number(project.progress) || 0,
      };
    } else {
      throw new Error('工事が見つかりません');
    }
  } catch (error) {
    console.error('工事詳細取得エラー:', error);
    return null;
  }
};

export const addProject = async (projectData) => {
  try {
    // 日付をTimestampに変換
    const startDate = projectData.startDate instanceof Date
      ? Timestamp.fromDate(projectData.startDate)
      : projectData.startDate;
    const endDate = projectData.endDate instanceof Date
      ? Timestamp.fromDate(projectData.endDate)
      : projectData.endDate;

    // accountingStandardを検証
    if (!['完成', '進行'].includes(projectData.accountingStandard)) {
      throw new Error('無効な会計基準です: 完成 または 進行 を指定してください');
    }

    await addDoc(collection(db, 'projects'), {
      name: projectData.name,
      department: projectData.department,
      contractAmountExTax: Number(projectData.contractAmountExTax) || 0,
      contractAmountIncTax: Number(projectData.contractAmountIncTax) || 0,
      budget: Number(projectData.budget) || 0,
      client: projectData.client || '',
      location: projectData.location || '',
      startDate,
      endDate,
      accountingStandard: projectData.accountingStandard,
      progress: Number(projectData.progress) || 0,
    });
    console.log('工事データを追加しました');
  } catch (error) {
    console.error('データ追加エラー:', error);
    throw error;
  }
};

export const addExpense = async (projectId, expenseData) => {
  try {
    // occurrenceDateをTimestampに変換
    const occurrenceDate = expenseData.occurrenceDate instanceof Date
      ? Timestamp.fromDate(expenseData.occurrenceDate)
      : expenseData.occurrenceDate;

    const expenseRef = collection(db, 'projects', projectId, 'expenses');
    await addDoc(expenseRef, {
      occurrenceDate,
      amount: Number(expenseData.amount) || 0,
      description: expenseData.description,
    });
    console.log('経費を追加しました');
  } catch (error) {
    console.error('経費追加エラー:', error);
    throw error;
  }
};

export const getTotalExpenses = async (projectId) => {
  try {
    const expenseRef = collection(db, 'projects', projectId, 'expenses');
    const querySnapshot = await getDocs(expenseRef);
    if (querySnapshot.empty) return 0;
    return querySnapshot.docs.reduce((sum, doc) => sum + (doc.data().amount || 0), 0);
  } catch (error) {
    console.error('経費合算エラー:', error);
    return 0;
  }
};

export const getCostBreakdown = async (projectId) => {
  try {
    const expenseRef = collection(db, 'projects', projectId, 'expenses');
    const querySnapshot = await getDocs(expenseRef);
    const breakdown = {
      outsourcing: 0,
      labor: 0,
      material: 0,
      other: 0,
    };
    querySnapshot.forEach(doc => {
      const expense = doc.data();
      const amount = Number(expense.amount) || 0;
      switch (expense.description) {
        case '外注費':
          breakdown.outsourcing += amount;
          break;
        case '労務費':
          breakdown.labor += amount;
          break;
        case '材料費':
          breakdown.material += amount;
          break;
        case 'その他':
          breakdown.other += amount;
          break;
        default:
          break;
      }
    });
    return breakdown;
  } catch (error) {
    console.error('原価内訳取得エラー:', error);
    return { outsourcing: 0, labor: 0, material: 0, other: 0 };
  }
};

export const fetchExpenses = async (projectId) => {
  try {
    const expenseRef = collection(db, 'projects', projectId, 'expenses');
    const querySnapshot = await getDocs(expenseRef);
    const expenses = querySnapshot.docs
      .map(doc => ({ id: doc.id, ...doc.data() }))
      .filter(expense => (
        expense.occurrenceDate instanceof Timestamp &&
        typeof expense.amount === 'number' &&
        typeof expense.description === 'string'
      ))
      .sort((a, b) => a.occurrenceDate.toDate() - b.occurrenceDate.toDate());
    return expenses;
  } catch (error) {
    console.error('経費一覧取得エラー:', error);
    return [];
  }
};

ProjectAdd.js (Modified)

ProjectAdd.js
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Typography, TextField, Button, MenuItem, Select, InputLabel, FormControl, CircularProgress, Alert } from '@mui/material';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { addProject } from '../utils/data';

const ProjectAdd = () => {
  const navigate = useNavigate();
  const [formData, setFormData] = useState({
    name: '',
    department: '',
    contractAmountExTax: '',
    contractAmountIncTax: '',
    budget: '',
    client: '',
    location: '',
    startDate: null,
    endDate: null,
    accountingStandard: '完成',
    progress: '',
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(null);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };

  const handleDateChange = (date, name) => {
    setFormData(prev => ({ ...prev, [name]: date }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setSuccess(null);

    // バリデーション
    if (!formData.name || !formData.department || !formData.contractAmountIncTax || !formData.startDate || !formData.endDate) {
      setError('必須項目を入力してください');
      setLoading(false);
      return;
    }
    if (isNaN(formData.contractAmountIncTax) || isNaN(formData.budget) || isNaN(formData.progress)) {
      setError('金額と進捗は数値を入力してください');
      setLoading(false);
      return;
    }
    if (formData.startDate > formData.endDate) {
      setError('工期終了日は工期開始日より後である必要があります');
      setLoading(false);
      return;
    }

    try {
      await addProject(formData);
      setSuccess('工事データを追加しました');
      setFormData({
        name: '',
        department: '',
        contractAmountExTax: '',
        contractAmountIncTax: '',
        budget: '',
        client: '',
        location: '',
        startDate: null,
        endDate: null,
        accountingStandard: '完成',
        progress: '',
      });
      setTimeout(() => navigate('/projects'), 2000);
    } catch (error) {
      setError('工事データの追加に失敗しました: ' + error.message);
    }
    setLoading(false);
  };

  return (
    <Box sx={{ maxWidth: 600, mx: 'auto', mt: 8, p: 2 }}>
      <Typography variant="h4" gutterBottom>工事追加</Typography>
      {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
      {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
      <form onSubmit={handleSubmit}>
        <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"
          value={formData.contractAmountExTax}
          onChange={handleChange}
          fullWidth
          margin="normal"
          type="number"
        />
        <TextField
          label="受注金額(税込) (円)"
          name="contractAmountIncTax"
          value={formData.contractAmountIncTax}
          onChange={handleChange}
          fullWidth
          margin="normal"
          type="number"
          required
        />
        <TextField
          label="実行予算 (円)"
          name="budget"
          value={formData.budget}
          onChange={handleChange}
          fullWidth
          margin="normal"
          type="number"
        />
        <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={{ mb: 2 }}>
          <Typography>工期開始</Typography>
          <DatePicker
            selected={formData.startDate}
            onChange={(date) => handleDateChange(date, 'startDate')}
            dateFormat="yyyy/MM/dd"
            customInput={<TextField fullWidth required />}
          />
        </Box>
        <Box sx={{ mb: 2 }}>
          <Typography>工期終了</Typography>
          <DatePicker
            selected={formData.endDate}
            onChange={(date) => handleDateChange(date, 'endDate')}
            dateFormat="yyyy/MM/dd"
            customInput={<TextField fullWidth required />}
          />
        </Box>
        <FormControl fullWidth margin="normal">
          <InputLabel>会計基準</InputLabel>
          <Select
            name="accountingStandard"
            value={formData.accountingStandard}
            onChange={handleChange}
            required
          >
            <MenuItem value="完成">完成</MenuItem>
            <MenuItem value="進行">進行</MenuItem>
          </Select>
        </FormControl>
        <TextField
          label="進捗 (%)"
          name="progress"
          value={formData.progress}
          onChange={handleChange}
          fullWidth
          margin="normal"
          type="number"
        />
        <Box sx={{ mt: 2 }}>
          <Button variant="contained" type="submit" disabled={loading}>
            {loading ? <CircularProgress size={24} /> : '追加'}
          </Button>
          <Button variant="outlined" onClick={() => navigate('/projects')} sx={{ ml: 2 }}>
            キャンセル
          </Button>
        </Box>
      </form>
    </Box>
  );
};

export default ProjectAdd;

ProjectExpenseAdd.js (Modified)

ProjectExpenseAdd.js
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Box, Typography, TextField, Button, Select, MenuItem, FormControl, InputLabel, CircularProgress, Alert } from '@mui/material';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { addExpense } from '../utils/data';

const ProjectExpenseAdd = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const [formData, setFormData] = useState({
    occurrenceDate: null,
    amount: '',
    description: '外注費',
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(null);

  // idの検証
  useEffect(() => {
    console.log('Project ID:', id);
    if (!id) {
      setError('プロジェクトIDが無効です');
    }
  }, [id]);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    console.log('Form data updated:', { ...formData, [name]: value });
  };

  const handleDateChange = (date) => {
    setFormData(prev => ({ ...prev, occurrenceDate: date }));
    console.log('Date updated:', date);
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setSuccess(null);

    // バリデーション
    if (!id) {
      setError('プロジェクトIDが無効です');
      setLoading(false);
      return;
    }
    if (!formData.occurrenceDate || !formData.amount || !formData.description) {
      setError('必須項目を入力してください');
      setLoading(false);
      return;
    }
    if (isNaN(formData.amount)) {
      setError('金額は数値を入力してください');
      setLoading(false);
      return;
    }

    try {
      await addExpense(id, formData);
      setSuccess('経費を追加しました');
      setFormData({
        occurrenceDate: null,
        amount: '',
        description: '外注費',
      });
      setTimeout(() => navigate(`/projects/${id}`), 2000);
    } catch (error) {
      setError('経費の追加に失敗しました: ' + error.message);
      console.error('Submit error:', error);
    }
    setLoading(false);
  };

  return (
    <Box sx={{ maxWidth: 600, mx: 'auto', mt: 8, p: 2 }}>
      <Typography variant="h4" gutterBottom>経費追加</Typography>
      {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
      {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
      <form onSubmit={handleSubmit}>
        <Box sx={{ mb: 2 }}>
          <Typography>発生日</Typography>
          <DatePicker
            selected={formData.occurrenceDate}
            onChange={handleDateChange}
            dateFormat="yyyy/MM/dd"
            customInput={<TextField fullWidth required />}
          />
        </Box>
        <TextField
          label="金額 (円)"
          name="amount"
          value={formData.amount}
          onChange={handleChange}
          fullWidth
          margin="normal"
          type="number"
          required
        />
        <FormControl fullWidth margin="normal">
          <InputLabel>経費概要</InputLabel>
          <Select
            name="description"
            value={formData.description}
            onChange={handleChange}
            required
          >
            <MenuItem value="外注費">外注費</MenuItem>
            <MenuItem value="労務費">労務費</MenuItem>
            <MenuItem value="材料費">材料費</MenuItem>
            <MenuItem value="その他">その他</MenuItem>
          </Select>
        </FormControl>
        <Box sx={{ mt: 2 }}>
          <Button variant="contained" type="submit" disabled={loading}>
            {loading ? <CircularProgress size={24} /> : '追加'}
          </Button>
          <Button variant="outlined" onClick={() => navigate(`/projects/${id}`)} sx={{ ml: 2 }}>
            キャンセル
          </Button>
        </Box>
      </form>
    </Box>
  );
};

export default ProjectExpenseAdd;

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, CircularProgress } from '@mui/material';
import { fetchProjectData, getTotalExpenses } from '../utils/data';

const ProjectList = () => {
  const [projects, setProjects] = useState([]);
  const [filteredProjects, setFilteredProjects] = useState([]);
  const [filters, setFilters] = useState({ name: '', department: '', client: '' });
  const [loading, setLoading] = useState(true);
  const navigate = useNavigate();

  useEffect(() => {
    const loadData = async () => {
      setLoading(true);
      const data = await fetchProjectData();
      const projectsWithExpenses = await Promise.all(data.map(async (project) => {
        const totalExpenses = await getTotalExpenses(project.id);
        return { ...project, totalExpenses: totalExpenses || 0 };
      }));
      setProjects(projectsWithExpenses);
      setFilteredProjects(projectsWithExpenses);
      setLoading(false);
    };
    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 }));
  };

  if (loading) {
    return (
      <Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2, textAlign: 'center' }}>
        <CircularProgress />
        <Typography>読み込み中...</Typography>
      </Box>
    );
  }

  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>
              <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.budget / 10000).toFixed(1)}</TableCell>
                <TableCell align="right">{((project.totalExpenses || 0) / 10000).toFixed(1)}</TableCell>
                <TableCell align="right">{project.client}</TableCell>
                <TableCell align="right">{project.location}</TableCell>
                <TableCell align="right">{project.startDate ? project.startDate.toDate().toLocaleDateString('ja-JP').replace(/-/g, '/') : '-'}</TableCell>
                <TableCell align="right">{project.endDate ? project.endDate.toDate().toLocaleDateString('ja-JP').replace(/-/g, '/') : '-'}</TableCell>
                <TableCell align="right">{project.accountingStandard || '-'}</TableCell>
                <TableCell align="right">
                  <Button variant="outlined" onClick={() => navigate(`/projects/${project.id}`)}>
                    詳細
                  </Button>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TableContainer>
    </Box>
  );
};

export default ProjectList;

ProjectDetail.js (Modified)

ProjectDetail.js
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Box, Typography, Button, CircularProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material';
import { fetchProjectById, getTotalExpenses, getCostBreakdown, fetchExpenses } from '../utils/data';

const ProjectDetail = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const [project, setProject] = useState(null);
  const [totalExpenses, setTotalExpenses] = useState(0);
  const [costBreakdown, setCostBreakdown] = useState(null);
  const [expenses, setExpenses] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const loadData = async () => {
      setLoading(true);
      const projectData = await fetchProjectById(id);
      if (projectData) {
        const expensesTotal = await getTotalExpenses(id);
        const breakdown = await getCostBreakdown(id);
        const expenseList = await fetchExpenses(id);
        setProject(projectData);
        setTotalExpenses(expensesTotal);
        setCostBreakdown(breakdown);
        setExpenses(expenseList);
      }
      setLoading(false);
    };
    loadData();
  }, [id]);

  if (loading) {
    return (
      <Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2, textAlign: 'center' }}>
        <CircularProgress />
        <Typography>読み込み中...</Typography>
      </Box>
    );
  }

  if (!project) {
    return (
      <Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2 }}>
        <Typography variant="h6">工事が見つかりません</Typography>
        <Button variant="outlined" onClick={() => navigate('/projects')}>
          工事一覧に戻る
        </Button>
      </Box>
    );
  }

  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, mr: 2 }}>
        工事一覧に戻る
      </Button>
      <Button variant="outlined" onClick={() => navigate(`/projects/${id}/expense/add`)} sx={{ mb: 2 }}>
        経費追加
      </Button>
      <Box sx={{ mb: 3 }}>
        <Typography variant="h6">工事詳細</Typography>
        <Typography>部署: {project.department}</Typography>
        <Typography>受注金額税込: {(project.contractAmountIncTax / 10000).toFixed(1)}万円</Typography>
        <Typography>実行予算: {(project.budget / 10000).toFixed(1)}万円</Typography>
        <Typography>使用済み経費: {(totalExpenses / 10000).toFixed(1)}万円</Typography>
        <Typography>発注者: {project.client}</Typography>
        <Typography>工事場所: {project.location}</Typography>
        <Typography>工期開始: {project.startDate ? project.startDate.toDate().toLocaleDateString('ja-JP').replace(/-/g, '/') : '-'}</Typography>
        <Typography>工期終了: {project.endDate ? project.endDate.toDate().toLocaleDateString('ja-JP').replace(/-/g, '/') : '-'}</Typography>
        <Typography>会計基準: {project.accountingStandard || '-'}</Typography>
        <Typography>進捗: {project.progress}%</Typography>
      </Box>
      <Box sx={{ mb: 3 }}>
        <Typography variant="h6">原価内訳</Typography>
        <Typography>外注費: {(costBreakdown.outsourcing / 10000).toFixed(1)}万円</Typography>
        <Typography>労務費: {(costBreakdown.labor / 10000).toFixed(1)}万円</Typography>
        <Typography>材料費: {(costBreakdown.material / 10000).toFixed(1)}万円</Typography>
        <Typography>その他: {(costBreakdown.other / 10000).toFixed(1)}万円</Typography>
      </Box>
      <Box>
        <Typography variant="h6" gutterBottom>経費一覧</Typography>
        {expenses.length === 0 ? (
          <Typography>経費データがありません</Typography>
        ) : (
          <TableContainer component={Paper}>
            <Table>
              <TableHead>
                <TableRow>
                  <TableCell>発生日</TableCell>
                  <TableCell align="right">金額 (万円)</TableCell>
                  <TableCell align="right">経費概要</TableCell>
                </TableRow>
              </TableHead>
              <TableBody>
                {expenses.map(expense => (
                  <TableRow key={expense.id}>
                    <TableCell>{expense.occurrenceDate ? expense.occurrenceDate.toDate().toLocaleDateString('ja-JP').replace(/-/g, '/') : '-'}</TableCell>
                    <TableCell align="right">{(expense.amount / 10000).toFixed(1)}</TableCell>
                    <TableCell align="right">{expense.description}</TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          </TableContainer>
        )}
      </Box>
    </Box>
  );
};

export default ProjectDetail;

App.js (Modified)

App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import ProjectList from './pages/ProjectList';
import ProjectDetail from './pages/ProjectDetail';
import ProjectAdd from './pages/ProjectAdd';
import ProjectExpenseAdd from './pages/ProjectExpenseAdd';
import Login from './components/auth/Login';
import Signup from './components/auth/Signup';
import { Box, Typography } from '@mui/material';

const NotFound = () => (
  <Box sx={{ maxWidth: 1200, mx: 'auto', mt: 8, p: 2, textAlign: 'center' }}>
    <Typography variant="h4">ページが見つかりません</Typography>
    <Typography>URLを確認してください</Typography>
  </Box>
);

const App = () => {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/login" element={<Login />} />
        <Route path="/signup" element={<Signup />} />
        <Route path="/projects" element={<ProjectList />} />
        <Route path="/projects/add" element={<ProjectAdd />} />
        <Route path="/projects/:id" element={<ProjectDetail />} />
        <Route path="/projects/:id/expense/add" element={<ProjectExpenseAdd />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Router>
  );
};

export default App;

以上、生成AI(Grok3)の出力

修正した結果
image.png

image.png

経費追加後
image.png

工事Cを追加したドキュメント
image.png

ちゃんと追加までできることを確認できました

一旦はこんな感じかな…?

ちょっと、コードが長くなってしまったので
今回はここまでくらいにしておこうかと思います

次回はいちいちデータをFirestore Databaseから削除していたので
工事情報や経費情報を削除できるように修正しておきたいと思います

1
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
1
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?