[page.php]で【固定ページ】を表示中

import React, { useState, useMemo, useEffect, useRef } from ‘react’; import { Users, Search, Plus, MapPin, Calendar, ArrowLeft, History, Sparkles, Send, Loader2, Phone, Map as MapIcon, AlertTriangle, Edit, Save, Briefcase, FileText, Download, Paperclip, Info } from ‘lucide-react’; // — API 設定 — // 有効なAPIキーをここに設定してください。設定されていない場合はプレースホルダーが表示されます。 const apiKey = “”; // — 初期モックデータ — const INITIAL_CUSTOMERS = [ { id: “1”, name: “山田 太郎”, zipCode: “100-0001”, address: “東京都千代田区千代田1-1”, phone: “090-1234-5678”, rank: “優良”, buildDate: “2010-05-20”, manager: “佐藤 健一”, status: “交渉中”, lat: 35.6812, lng: 139.7671, projects: [ { id: “p1”, name: “屋根・外壁塗装工事”, status: “見積提示済”, date: “2024-02-15”, attachment: “御見積書_山田様.pdf” } ], history: [ { id: “h1”, date: “2024-01-10”, note: “定期点検の案内。屋根の色褪せを気にされていた。” } ] }, { id: “2”, name: “鈴木 花子”, zipCode: “530-0001”, address: “大阪府大阪市北区梅田1-1”, phone: “080-9876-5432”, rank: “要注意”, buildDate: “1995-11-01”, manager: “高橋 誠”, status: “未着手”, lat: 34.7024, lng: 135.4959, projects: [], history: [] }, { id: “3”, name: “田中 次郎”, zipCode: “150-0002”, address: “東京都渋谷区渋谷2-2-2”, phone: “03-1111-2222”, rank: “優良”, buildDate: “2020-01-15”, manager: “佐藤 健一”, status: “完了”, lat: 35.6580, lng: 139.7016, projects: [ { id: “p2”, name: “システムキッチン入替”, status: “完工”, date: “2023-10-05”, attachment: “完了報告書.pdf” } ], history: [ { id: “h2”, date: “2023-11-20”, note: “完工後の1ヶ月点検。特に問題なし、満足されている様子。” } ] } ]; const App = () => { const [customers, setCustomers] = useState(INITIAL_CUSTOMERS); const [activeTab, setActiveTab] = useState(‘customers’); // customers, map, projects, history const [view, setView] = useState(‘list’); // list, detail, form const [selectedCustomer, setSelectedCustomer] = useState(null); const [editingCustomer, setEditingCustomer] = useState(null); const [searchTerm, setSearchTerm] = useState(”); const [filterRank, setFilterRank] = useState(‘すべて’); const [isAiLoading, setIsAiLoading] = useState(false); const [aiResponse, setAiResponse] = useState(“”); const [aiModalOpen, setAiModalOpen] = useState(false); const [mapError, setMapError] = useState(false); // 投稿用ステート const [newProject, setNewProject] = useState({ name: ”, status: ‘ヒアリング’, attachment: ” }); const [newHistory, setNewHistory] = useState({ date: new Date().toISOString().split(‘T’)[0], note: ” }); // Google Maps 関連 const mapRef = useRef(null); const googleMapRef = useRef(null); useEffect(() => { if (activeTab !== ‘map’) return; const loadGoogleMaps = () => { // APIキーがない場合は即座にエラー状態にする if (!apiKey) { setMapError(true); return; } if (window.google && window.google.maps) { initMap(); return; } // エラー発生時のコールバック window.gm_authFailure = () => { console.error(“Google Maps API authentication failed (gm_authFailure).”); setMapError(true); }; const script = document.createElement(‘script’); script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&callback=initMap`; script.async = true; script.defer = true; script.onerror = () => { console.error(“Google Maps script load error.”); setMapError(true); }; window.initMap = initMap; document.head.appendChild(script); }; const initMap = () => { if (!mapRef.current) return; try { const mapOptions = { center: { lat: 35.6812, lng: 139.7671 }, zoom: 12, mapTypeControl: false, streetViewControl: false, fullscreenControl: false, styles: [{ featureType: “poi”, elementType: “labels”, stylers: [{ visibility: “off” }] }] }; const map = new window.google.maps.Map(mapRef.current, mapOptions); googleMapRef.current = map; customers.forEach(customer => { const markerColor = customer.status === ‘完了’ ? ‘green’ : customer.status === ‘交渉中’ ? ‘blue’ : ‘grey’; const marker = new window.google.maps.Marker({ position: { lat: customer.lat, lng: customer.lng }, map: map, title: customer.name, icon: `https://maps.google.com/mapfiles/ms/icons/${markerColor}-dot.png` }); const infoWindow = new window.google.maps.InfoWindow({ content: `

${customer.name} 様

${customer.address}

` }); marker.addListener(‘click’, () => { infoWindow.open(map, marker); setTimeout(() => { const btn = document.getElementById(`btn-detail-${customer.id}`); if (btn) { btn.onclick = () => { setSelectedCustomer(customer); setView(‘detail’); setActiveTab(‘customers’); }; } }, 100); }); }); } catch (e) { console.error(“Map initialization failed:”, e); setMapError(true); } }; loadGoogleMaps(); return () => { // クリーンアップ delete window.initMap; delete window.gm_authFailure; }; }, [activeTab, customers]); const callGemini = async (prompt) => { setIsAiLoading(true); setAiModalOpen(true); setAiResponse(“”); let retries = 0; while (retries < 5) { try { const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], systemInstruction: { parts: [{ text: "住宅営業のプロとしてアドバイスしてください。" }] } }) }); if (!response.ok) throw new Error('API request failed'); const data = await response.json(); setAiResponse(data.candidates?.[0]?.content?.parts?.[0]?.text || "回答を取得できませんでした。"); setIsAiLoading(false); return; } catch (error) { retries++; await new Promise(res => setTimeout(res, Math.pow(2, retries) * 1000)); } } setIsAiLoading(false); setAiResponse(“AIとの通信に失敗しました。APIキーを確認してください。”); }; const calculateAge = (date) => { if (!date) return “不明”; const buildDate = new Date(date); if (isNaN(buildDate)) return “不明”; const age = new Date().getFullYear() – buildDate.getFullYear(); return `${age}年`; }; const filteredCustomers = useMemo(() => { return customers.filter(c => { const matchSearch = c.name.toLowerCase().includes(searchTerm.toLowerCase()) || c.address.toLowerCase().includes(searchTerm.toLowerCase()); const matchRank = filterRank === ‘すべて’ || c.rank === filterRank; return matchSearch && matchRank; }); }, [customers, searchTerm, filterRank]); // 全顧客の案件と履歴をフラットにしたリスト(全体タブ用) const allProjects = useMemo(() => { return customers.flatMap(c => (c.projects || []).map(p => ({ …p, customerName: c.name, customerId: c.id }))) .sort((a, b) => new Date(b.date) – new Date(a.date)); }, [customers]); const allHistory = useMemo(() => { return customers.flatMap(c => (c.history || []).map(h => ({ …h, customerName: c.name, customerId: c.id }))) .sort((a, b) => new Date(b.date) – new Date(a.date)); }, [customers]); const StatusBadge = ({ status }) => { const colors = { ‘完了’: ‘bg-green-100 text-green-700 border-green-200’, ‘完工’: ‘bg-green-100 text-green-700 border-green-200’, ‘交渉中’: ‘bg-blue-100 text-blue-700 border-blue-200’, ‘見積提示済’: ‘bg-blue-100 text-blue-700 border-blue-200’, ‘ヒアリング’: ‘bg-amber-100 text-amber-700 border-amber-200’, ‘未着手’: ‘bg-slate-100 text-slate-600 border-slate-200’, }; const appliedClass = colors[status] || ‘bg-slate-100 text-slate-600 border-slate-200’; return {status}; }; // CSVダウンロード機能 (BOM付きで文字化け防止) const exportCSV = () => { const headers = [‘氏名’, ‘郵便番号’, ‘住所’, ‘電話番号’, ‘ランク’, ‘担当者’, ‘ステータス’]; const rows = filteredCustomers.map(c => [ c.name, c.zipCode, c.address, c.phone, c.rank, c.manager, c.status ]); const csvContent = “data:text/csv;charset=utf-8,\uFEFF” + [headers.join(‘,’), …rows.map(e => e.map(item => `”${item}”`).join(‘,’))].join(‘\n’); const encodedUri = encodeURI(csvContent); const link = document.createElement(“a”); link.setAttribute(“href”, encodedUri); link.setAttribute(“download”, “customer_list.csv”); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; // 投稿アクション const handleAddProject = (customerId) => { if (!newProject.name) return; const projectEntry = { id: Date.now().toString(), …newProject, date: new Date().toISOString().split(‘T’)[0] }; setCustomers(prev => prev.map(c => c.id === customerId ? { …c, projects: [projectEntry, …(c.projects || [])] } : c)); setNewProject({ name: ”, status: ‘ヒアリング’, attachment: ” }); setSelectedCustomer(prev => ({ …prev, projects: [projectEntry, …(prev.projects || [])] })); }; const handleAddHistory = (customerId) => { if (!newHistory.note) return; const historyEntry = { id: Date.now().toString(), …newHistory }; setCustomers(prev => prev.map(c => c.id === customerId ? { …c, history: [historyEntry, …(c.history || [])] } : c)); setNewHistory({ date: new Date().toISOString().split(‘T’)[0], note: ” }); setSelectedCustomer(prev => ({ …prev, history: [historyEntry, …(prev.history || [])] })); }; return (
{/* ナビゲーション */}
{view === ‘detail’ || view === ‘form’ ? ( ) : (

{activeTab === ‘customers’ && <> 顧客マスター} {activeTab === ‘map’ && <> 顧客マップ} {activeTab === ‘projects’ && <> 案件情報} {activeTab === ‘history’ && <> 訪問履歴}

)}
setSearchTerm(e.target.value)} className=”pl-8 pr-4 py-1.5 text-sm border border-slate-200 rounded-full focus:ring-2 focus:ring-indigo-500 outline-none w-32 md:w-48″ />
{activeTab === ‘customers’ && view === ‘list’ && ( <> )}
{/* — タブ: 顧客マップ — */} {activeTab === ‘map’ && (
{mapError ? (

地図を読み込めません

Google Maps APIの設定(ApiProjectMapError)またはAPIキーが未入力のため、地図を表示できません。
※コード内の apiKey 変数に有効なキーを設定してください。

顧客の現在地一覧

{customers.map(c => (
{c.name} 様 {c.address.substring(0, 10)}…
))}
) : (
)} {!mapError && (

Legend

交渉中
完了
未着手
)}
)} {/* — タブ: 顧客マスター (一覧) — */} {activeTab === ‘customers’ && view === ‘list’ && (
{[‘すべて’, ‘優良’, ‘要注意’, ‘訪問不要’].map(r => ( ))}
{filteredCustomers.length > 0 ? filteredCustomers.map(customer => (
{setSelectedCustomer(customer); setView(‘detail’);}} className=”bg-white p-4 rounded-xl shadow-sm border border-slate-200 hover:border-indigo-300 transition cursor-pointer” >
{customer.name[0]}

{customer.name} 様

{customer.address}

ランク: {customer.rank} 築{calculateAge(customer.buildDate)}
)) : (

該当する顧客は見つかりませんでした

)}
)} {/* — タブ: 案件情報 (全顧客横断) — */} {activeTab === ‘projects’ && (

案件タイムライン

全顧客のプロジェクト進捗状況を時系列で表示しています

{allProjects.length === 0 ? (

案件情報はありません

) : (
{allProjects.map(p => (

{ const cust = customers.find(c => c.id === p.customerId); if(cust){ setSelectedCustomer(cust); setView(‘detail’); setActiveTab(‘customers’); } }}>{p.customerName} 様

{p.date}

{p.name}

{p.attachment && ( {p.attachment} )}
))}
)}
)} {/* — タブ: 訪問履歴 (全顧客横断) — */} {activeTab === ‘history’ && (

活動ログ

これまでの訪問記録を時系列で一覧表示しています

{allHistory.length === 0 ? (

訪問履歴はありません

) : (
{allHistory.map(h => (

{ const cust = customers.find(c => c.id === h.customerId); if(cust){ setSelectedCustomer(cust); setView(‘detail’); setActiveTab(‘customers’); } }}>{h.customerName} 様

{h.date}

{h.note}

))}
)}
)} {/* — 顧客詳細画面 (案件・履歴の投稿UI含む) — */} {activeTab === ‘customers’ && view === ‘detail’ && selectedCustomer && (
{/* クイックアクション */}
電話
{/* 基本情報 */}

{selectedCustomer.name} 様

ランク: {selectedCustomer.rank} 築{calculateAge(selectedCustomer.buildDate)}

Address

〒{selectedCustomer.zipCode}
{selectedCustomer.address}

Contact

{selectedCustomer.phone}

担当者

{selectedCustomer.manager}

{/* 案件情報 (投稿セクション) */}

案件情報

{/* 案件投稿フォーム */}
setNewProject({…newProject, name: e.target.value})} className=”col-span-1 md:col-span-2 p-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none” />
setNewProject({…newProject, attachment: e.target.value})} className=”flex-1 p-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none” />
{/* 案件リスト */}
{selectedCustomer.projects && selectedCustomer.projects.length > 0 ? selectedCustomer.projects.map((p) => (

{p.name}

{p.date}
{p.attachment && ( {p.attachment} )}
)) : (

登録された案件はありません

)}
{/* 訪問履歴 (投稿セクション) */}

訪問履歴

{/* 履歴投稿フォーム */}
setNewHistory({…newHistory, date: e.target.value})} className=”w-full md:w-1/3 p-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none” />
{/* 履歴リスト */}
{selectedCustomer.history && selectedCustomer.history.length > 0 ? selectedCustomer.history.map((h) => (

{h.date}

{h.note}

)) : (

登録された訪問履歴はありません

)}
)} {/* — 編集・新規作成フォーム — */} {activeTab === ‘customers’ && view === ‘form’ && editingCustomer && (

{editingCustomer.id.length > 10 ? ‘新規顧客登録’ : ‘顧客情報の編集’}

setEditingCustomer({ …editingCustomer, name: e.target.value })} className=”w-full p-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none” placeholder=”例: 山田 太郎” />
setEditingCustomer({ …editingCustomer, phone: e.target.value })} className=”w-full p-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none” placeholder=”090-0000-0000″ />
setEditingCustomer({ …editingCustomer, zipCode: e.target.value })} className=”w-full p-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none” placeholder=”100-0000″ />
setEditingCustomer({ …editingCustomer, address: e.target.value })} className=”w-full p-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none” placeholder=”東京都…” />
setEditingCustomer({ …editingCustomer, buildDate: e.target.value })} className=”w-full p-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none” />
setEditingCustomer({ …editingCustomer, manager: e.target.value })} className=”w-full p-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none” />
)}
{/* AIモーダル */} {aiModalOpen && (
AI 提案アシスタント
{isAiLoading ? (

提案を考えています…

) : aiResponse}
)}
); }; export default App;
Page Top