```react import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; import { Target, Plus, Minus, FileUp, Trash2, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, CheckCircle2, Crosshair, CloudUpload, X, List, ChevronLeft, FolderGit2 } from 'lucide-react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth'; import { getFirestore, collection, doc, setDoc, onSnapshot, deleteDoc } from 'firebase/firestore'; // --- Firebase Initialization --- const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; export default function App() { const [user, setUser] = useState(null); const [sites, setSites] = useState([]); const [view, setView] = useState('list'); // 'list' | 'detail' const [currentSiteId, setCurrentSiteId] = useState(null); // PWA Meta tags injection useEffect(() => { const addMeta = (name, content) => { let meta = document.querySelector(`meta[name="${name}"]`); if (!meta) { meta = document.createElement('meta'); meta.name = name; document.head.appendChild(meta); } meta.content = content; }; addMeta("apple-mobile-web-app-capable", "yes"); addMeta("apple-mobile-web-app-status-bar-style", "black-translucent"); addMeta("viewport", "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"); // Disable scrollbar globally const style = document.createElement('style'); style.innerHTML = ` body { -webkit-tap-highlight-color: transparent; background-color: #f1f5f9; } .no-scrollbar::-webkit-scrollbar { display: none; } input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { opacity: 1; } .font-mono-precision { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } `; document.head.appendChild(style); }, []); // Authentication useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (error) { console.error("Auth Error:", error); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, setUser); return () => unsubscribe(); }, []); // Fetch Sites Data useEffect(() => { if (!user) return; const sitesRef = collection(db, 'artifacts', appId, 'users', user.uid, 'sites'); const unsubscribe = onSnapshot(sitesRef, (snapshot) => { const loadedSites = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); // JS側で降順ソート (Firebaseのインデックス制約を回避) loadedSites.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); setSites(loadedSites); }, (error) => console.error("Fetch Error:", error)); return () => unsubscribe(); }, [user]); const createNewSite = async () => { if (!user) return; const newSite = { id: `site_${Date.now()}`, name: `新規現場 ${new Date().toLocaleDateString()}`, createdAt: Date.now(), xAxisIs: 'N', tolerance: 5.0, perfectThreshold: 1.0, activeColumnId: 1, columns: [ { id: 1, name: '1節-A1', designX: 100.0000, designY: 200.0000, measuredX: 100.0000, measuredY: 200.0000 } ] }; try { await setDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'sites', newSite.id), newSite); setCurrentSiteId(newSite.id); setView('detail'); } catch (e) { console.error("Create Error:", e); } }; const deleteSite = async (siteId, e) => { e.stopPropagation(); if (!user) return; try { await deleteDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'sites', siteId)); } catch (e) { console.error("Delete Error:", e); } }; if (!user) { return
認証中...
; } if (view === 'list') { return (

建方キング Pro

保存済みデータ

{sites.length === 0 && (
データがありません
)} {sites.map(site => (
{ setCurrentSiteId(site.id); setView('detail'); }} className="bg-white p-5 rounded-[2rem] shadow-sm border border-slate-200 flex items-center justify-between active:bg-slate-50 cursor-pointer group transition-all">

{site.name}

{new Date(site.createdAt).toLocaleString()}更新 • {site.columns?.length || 0}点

))}
); } const currentSite = sites.find(s => s.id === currentSiteId); if (!currentSite) { setView('list'); return null; } return ( setView('list')} /> ); } // --- Detail View Component --- function SiteDetail({ site, user, onBack }) { // ローカルステートで編集状態を管理し、変更があれば自動保存する const [localSite, setLocalSite] = useState(site); const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [importText, setImportText] = useState(""); const [stepUnit, setStepUnit] = useState(0.001); const fileInputRef = useRef(null); const saveTimeoutRef = useRef(null); // 親のデータが変更されたらローカルも同期(他のデバイスでの変更検知用) useEffect(() => { setLocalSite(site); }, [site.id]); // ローカルステートが変更されたらFirestoreへ自動保存(Debounce) const updateSite = useCallback((newSiteData) => { setLocalSite(newSiteData); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = setTimeout(async () => { try { await setDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'sites', newSiteData.id), newSiteData); } catch (e) { console.error("Auto-Save Error:", e); } }, 500); // 500msの遅延で自動保存 }, [user.uid]); const activeColumn = useMemo(() => localSite.columns.find(c => c.id === localSite.activeColumnId) || localSite.columns[0], [localSite.columns, localSite.activeColumnId]); const stats = useMemo(() => { if (!activeColumn) return null; const dx = (activeColumn.measuredX - activeColumn.designX) * 1000; const dy = (activeColumn.measuredY - activeColumn.designY) * 1000; let labels = { top: '', bottom: '', right: '', left: '' }; let fixTextX = ""; let fixTextY = ""; if (localSite.xAxisIs === 'N') { labels = { top: '北 (X+)', bottom: '南 (X-)', right: '東 (Y+)', left: '西 (Y-)' }; fixTextX = -dx > 0 ? "北" : "南"; fixTextY = -dy > 0 ? "東" : "西"; } else if (localSite.xAxisIs === 'S') { labels = { top: '南 (X+)', bottom: '北 (X-)', right: '西 (Y+)', left: '東 (Y-)' }; fixTextX = -dx > 0 ? "南" : "北"; fixTextY = -dy > 0 ? "西" : "東"; } else if (localSite.xAxisIs === 'E') { labels = { top: '東 (X+)', bottom: '西 (X-)', right: '南 (Y+)', left: '北 (Y-)' }; fixTextX = -dx > 0 ? "東" : "西"; fixTextY = -dy > 0 ? "南" : "北"; } else if (localSite.xAxisIs === 'W') { labels = { top: '西 (X+)', bottom: '東 (X-)', right: '北 (Y+)', left: '南 (Y-)' }; fixTextX = -dx > 0 ? "西" : "東"; fixTextY = -dy > 0 ? "北" : "南"; } return { dx, dy, fixX: -dx, fixY: -dy, fixTextX, fixTextY, labels, isOk: Math.abs(dx) <= localSite.tolerance && Math.abs(dy) <= localSite.tolerance, isPerfect: Math.abs(dx) <= localSite.perfectThreshold && Math.abs(dy) <= localSite.perfectThreshold }; }, [activeColumn, localSite.xAxisIs, localSite.tolerance, localSite.perfectThreshold]); const formatWithSign = (val, dec = 1) => (val > 0 ? "+" : (val < 0 ? "" : "+")) + val.toFixed(dec); const handleSiteNameChange = (e) => { updateSite({ ...localSite, name: e.target.value }); }; const setXAxisIs = (axis) => { updateSite({ ...localSite, xAxisIs: axis }); }; const setActiveId = (id) => { updateSite({ ...localSite, activeColumnId: id }); }; const updateActiveColumn = (field, value) => { const val = value === "" ? 0 : parseFloat(value); const newColumns = localSite.columns.map(c => c.id === (localSite.activeColumnId || localSite.columns[0].id) ? { ...c, [field]: val } : c); updateSite({ ...localSite, columns: newColumns }); }; const adjustValue = (field, delta) => { const target = activeColumn; if (!target) return; const newVal = (Math.round(target[field] * 10000) + Math.round(delta * 10000)) / 10000; updateActiveColumn(field, newVal); }; const handleFileChange = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => setImportText(event.target.result); reader.readAsText(file, 'Shift-JIS'); // 日本語対応 } }; const handleImport = () => { const lines = importText.split('\n').filter(l => l.trim() !== ""); const newPillars = []; lines.forEach((line, index) => { const parts = line.split(',').map(p => p.trim()); let name, x, y; if (parts[0] === 'A01' && parts.length >= 5) { name = parts[2]; x = parseFloat(parts[3]); y = parseFloat(parts[4]); } else { const spaceParts = line.split(/[\t, ]+/).map(p => p.trim()); if (spaceParts.length >= 3) { name = spaceParts[0]; x = parseFloat(spaceParts[1]); y = parseFloat(spaceParts[2]); } } if (!isNaN(x) && !isNaN(y)) { newPillars.push({ id: Date.now() + index, name: name || `柱-${localSite.columns.length + index + 1}`, designX: x, designY: y, measuredX: x, measuredY: y }); } }); if (newPillars.length > 0) { updateSite({ ...localSite, columns: [...localSite.columns, ...newPillars], activeColumnId: newPillars[0].id }); setImportText(""); setIsImportModalOpen(false); } }; const addColumn = () => { const newCol = { id: Date.now(), name: `柱-${localSite.columns.length+1}`, designX: 0, designY: 0, measuredX: 0, measuredY: 0 }; updateSite({ ...localSite, columns: [...localSite.columns, newCol], activeColumnId: newCol.id }); }; const deleteColumn = (id) => { if (localSite.columns.length <= 1) return; const filtered = localSite.columns.filter(c => c.id !== id); updateSite({ ...localSite, columns: filtered, activeColumnId: (localSite.activeColumnId === id ? filtered[0].id : localSite.activeColumnId) }); }; const exportToSIMA = () => { let content = "Z00,*** 建方キング Pro エクスポート ***,\r\n"; content += `G00, 01,建方実測データ,\r\n`; content += "Z00,----- 座標データ -----\r\n"; content += "A00,\r\n"; localSite.columns.forEach((col, idx) => { content += `A01,${(idx + 1).toString().padStart(5, ' ')}, ${col.name}, ${col.measuredX.toFixed(4)}, ${col.measuredY.toFixed(4)}, 0.0000,\r\n`; }); content += "A99,\r\n"; const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${localSite.name}_実測値_${new Date().getTime()}.sim`; a.click(); URL.revokeObjectURL(url); }; return (
{/* ヘッダー */}

現場のX軸プラス(上)の方角を指定

{['N','S','E','W'].map(d => { const labels = { 'N':'北','S':'南','E':'東','W':'西' }; return ( ); })}
{/* モニター */}
updateActiveColumn('name', e.target.value)} className="text-2xl font-black text-slate-800 italic tracking-tighter truncate w-full text-center outline-none bg-slate-50 rounded-xl py-1 focus:ring-2 focus:ring-blue-400" />
{/* ラベル回転 */}
{stats?.labels.top}
{stats?.labels.bottom}
{stats?.labels.right}
{stats?.labels.left}
{stats && (
)}

X軸ズレ (上下方向)

localSite.tolerance ? 'text-red-500' : 'text-blue-600'}`}>{formatWithSign(stats?.dx || 0, 1)}mm

Y軸ズレ (左右方向)

localSite.tolerance ? 'text-red-500' : 'text-blue-600'}`}>{formatWithSign(stats?.dy || 0, 1)}mm

{/* 指示パネル */}
{stats?.isPerfect ? '完全合格' : (stats?.isOk ? '精度内 (ゼロへ追い込み中)' : '建ち直し指示が必要です')}
{stats?.isPerfect ? (

固定OK

) : (
{Math.abs(stats?.fixX || 0) > 0.0001 && (
{stats?.fixX > 0 ? : }

{stats?.fixTextX}へ

{Math.abs(stats?.fixX || 0).toFixed(1)}mm

)}
{Math.abs(stats?.fixY || 0) > 0.0001 && (
{stats?.fixY > 0 ? : }

{stats?.fixTextY}へ

{Math.abs(stats?.fixY || 0).toFixed(1)}mm

)}
)}
{/* 実測入力 */}

実測座標入力 (m)

北方向 X軸入力
設計: updateActiveColumn('designX', e.target.value)} className="font-mono-precision bg-slate-50 px-2 py-1 outline-none rounded-lg border font-bold text-slate-700 w-24 text-right" />
updateActiveColumn('measuredX', e.target.value)} className="w-full bg-transparent font-mono-precision font-black text-blue-900 text-3xl outline-none text-center tracking-tighter" />
東方向 Y軸入力
設計: updateActiveColumn('designY', e.target.value)} className="font-mono-precision bg-slate-50 px-2 py-1 outline-none rounded-lg border font-bold text-slate-700 w-24 text-right" />
updateActiveColumn('measuredY', e.target.value)} className="w-full bg-transparent font-mono-precision font-black text-blue-900 text-3xl outline-none text-center tracking-tighter" />
{/* 精度管理ログ */}

精度管理ログ

{localSite.columns.map(col => { const dx = (col.measuredX - col.designX) * 1000; const dy = (col.measuredY - col.designY) * 1000; const isP = Math.abs(dx) <= localSite.perfectThreshold && Math.abs(dy) <= localSite.perfectThreshold; const isActive = localSite.activeColumnId === col.id; return ( setActiveId(col.id)} className={`transition-all cursor-pointer ${isActive ? 'border-l-[12px] border-l-blue-600 bg-blue-50' : 'hover:bg-slate-50'}`}> ); })}
点名設計座標(m)ズレ(mm)
{col.name}
X:{col.designX.toFixed(4)}
Y:{col.designY.toFixed(4)}
localSite.tolerance ? 'text-red-500' : (isP ? 'text-green-600' : 'text-blue-600')}`}> X:{formatWithSign(dx, 1)}
localSite.tolerance ? 'text-red-500' : (isP ? 'text-green-600' : 'text-blue-600')}`}> Y:{formatWithSign(dy, 1)}
{/* SIMAインポートモーダル */} {isImportModalOpen && (

SIMAデータ読込

fileInputRef.current.click()} className="border-4 border-dashed border-slate-200 rounded-[2.5rem] p-16 flex flex-col items-center justify-center gap-8 bg-slate-50 active:bg-slate-100 transition-all cursor-pointer group shadow-inner">

{importText ? "読込完了" : "ファイルを指定"}

)}
); } ```