<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>スーパーロングエクステ在庫管理 (クラウド保存版)</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Noto Sans JP', 'Inter', sans-serif;
}
.btn-focus-ring:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.6);
}
.zero-stock-item { background-color: #f3f4f6; } /* gray-200 */
.low-stock-item { background-color: #fee2e2; } /* red-100 */
button:disabled { opacity: 0.4; cursor: not-allowed; }
.modal-content {
max-height: 60vh;
}
.tab-btn-active {
border-color: #4f46e5;
color: #4f46e5;
}
</style>
</head>
<body class="bg-gray-50 text-gray-800">
<!-- Login Modal -->
<div id="loginModal" class="fixed inset-0 bg-gray-800 bg-opacity-75 flex items-center justify-center z-50">
<div class="bg-white p-8 rounded-lg shadow-2xl text-center w-11/12 max-w-sm">
<h2 class="text-2xl font-bold mb-4">ログイン</h2>
<p class="text-gray-600 mb-6">作業を始める前に、あなたの名前を選んでください。</p>
<select id="staffSelector" class="w-full p-3 border border-gray-300 rounded-lg mb-6 text-lg">
<option value="">名前を選択...</option>
<option value="あつきくん">あつきくん</option>
<option value="いっきくん">いっきくん</option>
<option value="あいちゃん">あいちゃん (店長)</option>
<option value="C1">C1</option>
</select>
<button id="loginBtn" class="w-full bg-indigo-600 text-white font-bold py-3 px-6 rounded-lg hover:bg-indigo-700 transition-colors shadow-md btn-focus-ring" disabled>作業を開始</button>
</div>
</div>
<div id="app" class="hidden container mx-auto p-4 sm:p-6 lg:p-8 max-w-7xl">
<!-- ヘッダー -->
<header class="mb-6 flex justify-between items-start">
<div>
<h1 class="text-2xl sm:text-4xl font-bold text-gray-800">スーパーロングエクステ在庫管理</h1>
<p class="text-gray-500 mt-1">クラウドに自動保存されるため、データは消えません。</p>
</div>
<div id="userInfo" class="text-right text-sm flex-shrink-0"></div>
</header>
<!-- サマリー表示エリア -->
<div class="mb-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
<h3 class="font-bold text-lg text-gray-800 mb-2">在庫サマリー</h3>
<div id="capacity-info" class="text-center">
<p class="text-gray-600">現在のお客様対応可能人数(目安)</p>
<p class="text-3xl font-bold text-indigo-600">計算中...</p>
<p class="text-xs text-gray-500 mt-1">※1人あたり80本使用で計算</p>
</div>
</div>
<div id="weekly-summary-highlight" class="hidden p-4 bg-yellow-100 border-l-4 border-yellow-500 rounded-r-lg shadow-sm">
<!-- Weekly summary injected here -->
</div>
</div>
<!-- 発注日リマインダー -->
<div id="order-reminder" class="hidden mb-6 p-4 bg-purple-100 border-l-4 border-purple-500 text-purple-700 rounded-r-lg">
<div class="flex justify-between items-center">
<div><p class="font-bold">今日は発注の日だよ!</p><p>在庫が少ない商品をチェックして、発注依頼をしよう!</p></div>
<button id="close-reminder-btn" class="text-purple-500 hover:text-purple-800">×</button>
</div>
</div>
<!-- コメント表示エリア -->
<div id="commentSection" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div id="comment-box-personal" class="p-4 bg-blue-100 border-l-4 border-blue-500 text-blue-700 rounded-r-lg"></div>
<div id="comment-box-others" class="p-4 bg-green-100 border-l-4 border-green-500 text-green-700 rounded-r-lg"></div>
</div>
<!-- View Toggler -->
<div class="mb-6 flex justify-center border-b border-gray-200">
<button id="showInventoryBtn" class="px-4 py-3 text-indigo-600 border-b-2 border-indigo-600 font-semibold">在庫リスト</button>
<button id="showWarehouseBtn" class="px-4 py-3 text-gray-500 font-semibold">在庫リスト(倉庫)</button>
<button id="showManagementBtn" class="px-4 py-3 text-gray-500 font-semibold">在庫管理</button>
<button id="showAnalysisBtn" class="px-4 py-3 text-gray-500 font-semibold">データ分析</button>
<button id="showFeedbackBtn" class="px-4 py-3 text-gray-500 font-semibold">試作品フィードバック</button>
</div>
<!-- Inventory View (Store) -->
<div id="inventoryView">
<!-- アクションエリア -->
<div class="flex flex-col sm:flex-row gap-4 mb-6">
<div class="relative flex-grow">
<input type="text" id="searchInput" placeholder="商品名で検索..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"><svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" /></svg></div>
</div>
<button id="confirmOrderBtn" class="bg-purple-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-purple-700 transition-colors shadow-md btn-focus-ring">発注内容を確認&コピー</button>
<button id="itemsArrivedBtn" class="bg-green-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-green-700 transition-colors shadow-md btn-focus-ring">商品到着 (発注リセット)</button>
<button id="openAddModalBtn" class="bg-indigo-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-indigo-700 transition-colors shadow-md btn-focus-ring">新規追加</button>
</div>
<!-- 在庫リスト -->
<div id="inventoryListContainer">
<div class="hidden md:grid md:grid-cols-12 gap-4 px-4 py-2 text-xs text-gray-700 uppercase bg-gray-100 font-bold rounded-t-lg">
<div class="col-span-3">商品名</div><div class="col-span-2 text-center">在庫(袋)</div><div class="col-span-2 text-center">在庫(本)</div><div class="col-span-1 text-center">最低在庫(袋)</div><div class="col-span-1 text-center">発注数(袋)</div><div class="col-span-3 text-center">アクション</div>
</div>
<div id="inventoryList" class="space-y-4 md:space-y-0"></div>
</div>
</div>
<!-- Warehouse View -->
<div id="warehouseView" class="hidden">
<!-- アクションエリア -->
<div class="flex flex-col sm:flex-row gap-4 mb-6">
<div class="relative flex-grow">
<input type="text" id="warehouseSearchInput" placeholder="商品名で検索..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"><svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" /></svg></div>
</div>
</div>
<!-- 在庫リスト -->
<div id="warehouseListContainer">
<div class="hidden md:grid md:grid-cols-12 gap-4 px-4 py-2 text-xs text-gray-700 uppercase bg-gray-100 font-bold rounded-t-lg">
<div class="col-span-3">商品名</div><div class="col-span-2 text-center">在庫(袋)</div><div class="col-span-2 text-center">在庫(本)</div><div class="col-span-1 text-center">最低在庫(袋)</div><div class="col-span-1 text-center">発注数(袋)</div><div class="col-span-3 text-center">アクション</div>
</div>
<div id="warehouseList" class="space-y-4 md:space-y-0"></div>
</div>
</div>
<!-- Management View -->
<div id="managementView" class="hidden">
<div class="mb-4 border-b border-gray-200">
<nav class="flex -mb-px" aria-label="Tabs">
<button id="weeklyReportBtn" class="tab-btn-active w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm">週管理</button>
<button id="monthlyReportBtn" class="w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300">月間管理</button>
</nav>
</div>
<div id="weeklyReportView">
<h2 class="text-xl font-bold mb-4">週次レポート (<span id="weeklyDateRange"></span>)</h2>
<div id="weeklyReportContent" class="bg-white p-4 rounded-lg shadow-md"></div>
</div>
<div id="monthlyReportView" class="hidden">
<h2 class="text-xl font-bold mb-4">月次レポート (<span id="monthlyDateRange"></span>)</h2>
<div id="monthlyReportContent" class="bg-white p-4 rounded-lg shadow-md"></div>
</div>
</div>
<!-- Analysis View -->
<div id="analysisView" class="hidden">
<div class="flex justify-end mb-4">
<button id="resetAnalysisBtn" class="bg-red-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-red-700 transition-colors shadow-md btn-focus-ring">履歴データリセット</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="lg:col-span-2 bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-bold mb-4">全体サマリー</h3>
<div id="analysisSummary" class="grid grid-cols-2 md:grid-cols-4 gap-4 text-center"></div>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-bold mb-2">お客様の動機</h3>
<div id="motivationChartFeedback" class="text-sm text-gray-600 mb-4"></div>
<div class="h-64"><canvas id="motivationChart"></canvas></div>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-bold mb-2">人気の着用場所</h3>
<div id="areaAnalysisList" class="space-y-2"></div>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-bold mb-2">人気色ランキング</h3>
<ol id="popularColorList" class="space-y-2 list-decimal list-inside">
<!-- Ranking will be injected here -->
</ol>
</div>
<div class="lg:col-span-2 bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-bold mb-4">商品別 在庫推移 (過去30日)</h3>
<select id="itemHistorySelector" class="w-full p-2 border border-gray-300 rounded-lg mb-4"></select>
<div id="historyChartFeedback" class="text-sm text-gray-600 mb-4"></div>
<div class="h-64"><canvas id="historyChart"></canvas></div>
</div>
<div class="lg:col-span-2 bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-bold mb-4">お客様の声(原文ママ)</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 max-h-96 overflow-y-auto">
<div>
<h4 class="font-semibold mb-2">つけた後の感想</h4>
<div id="impressionsList" class="space-y-2 text-sm"></div>
</div>
<div>
<h4 class="font-semibold mb-2">結果・仕上がり</h4>
<div id="resultsList" class="space-y-2 text-sm"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Feedback View -->
<div id="feedbackView" class="hidden">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-1 bg-white p-6 rounded-lg shadow-md">
<h3 class="text-xl font-bold mb-4">試作品フィードバック入力</h3>
<form id="feedbackForm" class="space-y-4">
<div><label for="feedbackDate" class="block text-sm font-medium text-gray-700">日付</label><input type="date" id="feedbackDate" required class="mt-1 block w-full p-2 border border-gray-300 rounded-md"></div>
<div><label for="feedbackColor" class="block text-sm font-medium text-gray-700">色</label><select id="feedbackColor" required class="mt-1 block w-full p-2 border border-gray-300 rounded-md"></select></div>
<div class="grid grid-cols-2 gap-4">
<div><label for="feedbackPieces" class="block text-sm font-medium text-gray-700">本数</label><input type="number" id="feedbackPieces" required class="mt-1 block w-full p-2 border border-gray-300 rounded-md"></div>
<div><label for="feedbackPrice" class="block text-sm font-medium text-gray-700">1本の料金</label><select id="feedbackPrice" required class="mt-1 block w-full p-2 border border-gray-300 rounded-md"></select></div>
</div>
<div><label for="feedbackArea" class="block text-sm font-medium text-gray-700">着用場所</label><input type="text" id="feedbackArea" placeholder="例:サイド、バック" class="mt-1 block w-full p-2 border border-gray-300 rounded-md"></div>
<div>
<label for="feedbackMotivation" class="block text-sm font-medium text-gray-700">動機</label>
<select id="feedbackMotivation" class="mt-1 block w-full p-2 border border-gray-300 rounded-md">
<option value="元からつけたかった">元からつけたかった</option>
<option value="来店してから決めた">来店してから決めた</option>
</select>
</div>
<div>
<label for="feedbackMotivationReason" class="block text-sm font-medium text-gray-700">動機の理由</label>
<textarea id="feedbackMotivationReason" rows="2" class="mt-1 block w-full p-2 border border-gray-300 rounded-md" placeholder="(例)インスタで見て、など"></textarea>
</div>
<div><label for="feedbackMixed" class="block text-sm font-medium text-gray-700">ダイヤと混ぜたか</label><select id="feedbackMixed" class="mt-1 block w-full p-2 border border-gray-300 rounded-md"><option value="はい">はい</option><option value="いいえ">いいえ</option></select></div>
<div><label for="feedbackImpressions" class="block text-sm font-medium text-gray-700">つけた後の感想</label><textarea id="feedbackImpressions" rows="3" class="mt-1 block w-full p-2 border border-gray-300 rounded-md"></textarea></div>
<div><label for="feedbackResult" class="block text-sm font-medium text-gray-700">結果(仕上がり)</label><textarea id="feedbackResult" rows="3" class="mt-1 block w-full p-2 border border-gray-300 rounded-md"></textarea></div>
<button type="submit" class="w-full bg-indigo-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-indigo-700 transition-colors shadow-md">フィードバックを保存</button>
</form>
</div>
<div class="lg:col-span-2">
<h3 class="text-xl font-bold mb-4">フィードバック履歴</h3>
<div id="feedbackList" class="space-y-4">
<p>フィードバックがありません。</p>
</div>
</div>
</div>
</div>
</div>
<!-- Modals -->
<div id="addEditModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> <div class="relative top-20 mx-auto p-5 border w-full max-w-md shadow-lg rounded-md bg-white"> <div class="mt-3"> <h3 class="text-lg leading-6 font-medium text-gray-900 text-center" id="modalTitle">在庫の新規追加</h3> <form id="addEditForm" class="mt-4 space-y-4 text-left"> <input type="hidden" id="editItemId"><input type="hidden" id="editListType"> <div> <label for="itemName" class="block text-sm font-medium text-gray-700">商品名 / 色</label> <input type="text" id="itemName" required class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"></div> <div class="grid grid-cols-2 gap-4"> <div> <label id="quantityLabel" for="itemQuantity" class="block text-sm font-medium text-gray-700">在庫数 (袋)</label> <input type="number" id="itemQuantity" required min="0" step="any" class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> </div> <div> <label id="minStockLabel" for="itemMinStock" class="block text-sm font-medium text-gray-700">最低在庫 (袋)</label> <input type="number" id="itemMinStock" required min="0" value="0" class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"> </div> </div> <div class="items-center gap-4 pt-4 flex"> <button id="cancelBtn" type="button" class="w-full justify-center rounded-md border border-gray-300 px-4 py-2 bg-white text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 btn-focus-ring">キャンセル</button> <button id="saveBtn" type="submit" class="w-full justify-center rounded-md border border-transparent px-4 py-2 bg-indigo-600 text-base font-medium text-white shadow-sm hover:bg-indigo-700 btn-focus-ring">保存</button> </div> </form> </div> </div> </div>
<div id="historyModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> <div class="relative top-10 mx-auto p-5 border w-full max-w-lg shadow-lg rounded-md bg-white"> <div class="flex justify-between items-center pb-3 border-b"> <h3 class="text-lg leading-6 font-medium text-gray-900" id="historyModalTitle">変更履歴</h3> <button id="closeHistoryModalBtn" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg> </button> </div> <div id="historyModalContent" class="mt-4 modal-content overflow-y-auto"></div> </div> </div>
<div id="orderSummaryModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> <div class="relative top-20 mx-auto p-5 border w-full max-w-md shadow-lg rounded-md bg-white"> <div class="mt-3"> <h3 class="text-lg leading-6 font-medium text-gray-900 text-center">発注内容の確認</h3> <div class="mt-4 bg-gray-50 p-3 rounded-md max-h-60 overflow-y-auto"> <textarea id="orderTextForCopy" readonly class="w-full h-40 text-sm bg-gray-50 border-0 focus:ring-0 resize-none"></textarea> </div> <div class="items-center gap-4 pt-4 flex"> <button id="copyOrderBtn" type="button" class="w-full justify-center rounded-md border border-transparent px-4 py-2 bg-blue-600 text-base font-medium text-white shadow-sm hover:bg-blue-700 btn-focus-ring">クリップボードにコピー</button> <button id="markAsOrderedBtn" type="button" class="w-full justify-center rounded-md border border-transparent px-4 py-2 bg-purple-600 text-base font-medium text-white shadow-sm hover:bg-purple-700 btn-focus-ring">発注済みにする</button> </div> <button id="closeOrderSummaryBtn" type="button" class="mt-4 w-full justify-center rounded-md border border-gray-300 px-4 py-2 bg-white text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 btn-focus-ring">閉じる</button> </div> </div> </div>
<div id="alertModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50"> <div class="bg-white p-6 rounded-lg shadow-xl w-11/12 max-w-sm text-center"> <h3 id="alertModalTitle" class="text-lg font-bold mb-4"></h3> <p id="alertModalMessage" class="text-gray-700 mb-6"></p> <button id="alertModalCloseBtn" class="w-full bg-indigo-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-indigo-700">閉じる</button> </div> </div>
<div id="confirmModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50"> <div class="bg-white p-6 rounded-lg shadow-xl w-11/12 max-w-sm text-center"> <h3 id="confirmModalTitle" class="text-lg font-bold mb-4"></h3> <p id="confirmModalMessage" class="text-gray-700 mb-6"></p> <div class="flex justify-center gap-4"> <button id="confirmModalNoBtn" class="w-full bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded-lg hover:bg-gray-400">いいえ</button> <button id="confirmModalYesBtn" class="w-full bg-red-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-red-700">はい</button> </div> </div> </div>
<script type="module">
// Firebase モジュールのインポート
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInAnonymously, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, collection, onSnapshot, addDoc, doc, updateDoc, deleteDoc, serverTimestamp, getDocs, writeBatch, query, Timestamp, where, runTransaction } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
// --- グローバル変数 ---
const firebaseConfig = {
apiKey: "AIzaSyAmGcQsqzxOaXr0lweKhz58Mab9uLd4FYU",
authDomain: "superlong-e265f.firebaseapp.com",
projectId: "superlong-e265f",
storageBucket: "superlong-e265f.appspot.com",
messagingSenderId: "117912519488",
appId: "1:117912519488:web:d8052ef7a9a8ddf81f30a5",
measurementId: "G-7X4JHJJ1YY"
};
const appId = 'superlong-ext-inventory-private';
let app, db, auth, userId, inventoryCollection, feedbackCollection, orderLogsCollection;
let allItems = [];
let allFeedback = [];
let unsubscribeInventory, unsubscribeFeedback;
let currentUser = null;
let motivationChartInstance, historyChartInstance;
// --- ログイン処理 ---
function handleLogin() {
const staffSelector = document.getElementById('staffSelector');
const loginModal = document.getElementById('loginModal');
currentUser = staffSelector.value;
if (!currentUser) return;
localStorage.setItem('inventoryStaffName', currentUser);
loginModal.classList.add('hidden');
document.getElementById('app').classList.remove('hidden');
let userDisplayName = currentUser;
if (currentUser === 'あいちゃん') {
userDisplayName = 'あいちゃん (店長)';
}
document.getElementById('userInfo').innerHTML = `<span class="font-semibold">${userDisplayName}</span> としてログイン中`;
initialize();
}
document.addEventListener('DOMContentLoaded', () => {
const staffSelector = document.getElementById('staffSelector');
const loginBtn = document.getElementById('loginBtn');
const savedStaff = localStorage.getItem('inventoryStaffName');
loginBtn.addEventListener('click', handleLogin);
staffSelector.addEventListener('change', () => {
loginBtn.disabled = staffSelector.value === "";
});
if (savedStaff) {
staffSelector.value = savedStaff;
loginBtn.disabled = false;
handleLogin();
}
});
// --- コメント機能 ---
const staff = ['あつきくん', 'いっきくん', 'あいちゃん'];
const comments = {
gentle: {
morning: ["おはよう!今日も一日頑張ろうね。在庫チェック、時間があるときにお願い!", "いつもありがとう!お客様のために、在庫の確認を一緒に頑張ろうね。", "朝の準備、順調?在庫も綺麗だと一日気持ちいいね!", "おはよう!今日も一日、お客様を最高に可愛くしようね!"],
evening: ["今日もお疲れ様!使った分だけ、忘れずに入力してくれたら嬉しいな。", "お疲れ様!最後にサッと入力して、明日の自分を助けてあげよう。", "今日も一日お疲れ様!入力作業、大変だけど明日が楽になるよ。", "いつも丁寧な入力ありがとう。助かってます!"],
general: ["みんなの丁寧な管理のおかげで、お店が回ってるよ。本当にありがとう!", "在庫が綺麗だと、心もスッキリするね!", "この色、最近よく出るね!お客様の好み、わかってきたかも?", "みんなで協力すれば、在庫管理も楽勝だね!", "一つ一つの管理が、お客様の満足に繋がってるよ。いつもありがとう!"]
},
strict: {
morning: ["営業開始前に在庫のズレは必ず修正すること。お客様に迷惑がかかる。", "在庫数が合わないのはプロとして問題。すぐに確認しなさい。", "昨日の最終在庫と今朝の在庫は一致しているか?確認を怠らないように。", "報告・連絡・相談。在庫の異変はすぐに共有すること。"],
evening: ["今日の売上と在庫は合っているか?退勤前に必ず報告すること。", "入力漏れは許されない。今日の業務は今日のうちに完結させなさい。", "退勤前にダブルチェックは基本。明日の自分に責任を持つこと。", "今日の数字は今日のうちに。先延ばしはミスの元。"],
general: ["数字の管理は基本中の基本。常に正確な在庫を把握しておくこと。", "欠品は店の信用問題に直結する。最低在庫数を下回る前に発注を徹底。", "「たぶん大丈夫」は通用しない。数字で語ること。", "プロ意識を持って、在庫と向き合うこと。"]
},
provocative: {
morning: ["まさかとは思うけど、まだ在庫見てないなんてことないよね?お客様、帰っちゃうよ?", "その在庫数、本当に信じていいの?昨日誰かさんが入力忘れてたみたいだけど?", "二日酔い?在庫の数、ちゃんと見えてる?(笑)", "まさかとは思うけど、スマホいじる前に在庫見たよね?"],
evening: ["お疲れー。で、今日の在庫、ちゃんと書いた?まさか忘れてないよね?明日困るのは自分だよ?", "え、もう帰るの?今日の分の入力、終わってる?まさか明日やろうとか思ってない?", "今日の売上、在庫と合わなかったら自腹ね(冗談)", "入力忘れて明日あいちゃんに怒られても知らないからねー。"],
general: ["あれ、この商品また欠品しそうじゃん。人気なのに発注しないとか、さては商売やる気ない?", "在庫管理もできないのに、一人前とか言っちゃう感じ?(笑)", "あれ?この在庫数、前も見たな。デジャブ?それとも入力忘れ?", "その発注数で本当に足りると思ってる?勇者だねぇ。"]
}
};
function displayDynamicComment() {
const commentSection = document.getElementById('commentSection');
if (currentUser === 'C1') {
commentSection.classList.add('hidden');
return;
}
commentSection.classList.remove('hidden');
const commentBoxPersonal = document.getElementById('comment-box-personal');
const commentBoxOthers = document.getElementById('comment-box-others');
const hour = new Date().getHours();
const patterns = ['gentle', 'strict', 'provocative'];
const getRandomComment = (pattern, time) => {
const list = comments[pattern][time];
return list[Math.floor(Math.random() * list.length)];
}
const getTimeOfDay = () => (hour >= 9 && hour < 12) ? 'morning' : (hour >= 17 && hour < 20) ? 'evening' : 'general';
const personalPattern = patterns[Math.floor(Math.random() * patterns.length)];
const personalComment = getRandomComment(personalPattern, getTimeOfDay());
commentBoxPersonal.innerHTML = `<p><strong class="font-semibold">${currentUser}へ:</strong> ${personalComment}</p>`;
const otherStaff = staff.filter(s => s !== currentUser);
if (otherStaff.length > 0) {
const otherPattern = patterns[Math.floor(Math.random() * patterns.length)];
const otherComment = getRandomComment(otherPattern, getTimeOfDay());
const targetStaff = otherStaff[Math.floor(Math.random() * otherStaff.length)];
commentBoxOthers.innerHTML = `<p><strong class="font-semibold">${targetStaff}へ:</strong> ${otherComment}</p>`;
}
}
function checkOrderDay() {
const orderReminder = document.getElementById('order-reminder');
const day = new Date().getDay(); // 0:日曜, 5:金曜, 6:土曜
if (day === 5 || day === 6) {
orderReminder.classList.remove('hidden');
}
}
function formatBagDisplay(quantity) {
const bags = quantity / 12;
if (bags % 1 !== 0) {
return bags.toFixed(1);
}
return bags;
}
// --- 在庫データレンダリング ---
const renderInventory = (listType = 'store') => {
const isStore = listType === 'store';
const container = document.getElementById(isStore ? 'inventoryView' : 'warehouseView');
if (!container) return;
const inventoryList = container.querySelector(isStore ? '#inventoryList' : '#warehouseList');
const searchInput = container.querySelector(isStore ? '#searchInput' : '#warehouseSearchInput');
const loadingState = document.getElementById('loadingState');
const emptyState = document.getElementById('emptyState');
const searchTerm = searchInput.value.toLowerCase();
const currentViewItems = allItems.filter(item => item.name.toLowerCase().includes(searchTerm));
inventoryList.innerHTML = '';
if(loadingState) loadingState.classList.add('hidden');
if(emptyState) emptyState.classList.add('hidden');
if (currentViewItems.length === 0) {
if(emptyState) {
emptyState.classList.remove('hidden');
if (allItems.length > 0) {
emptyState.querySelector('p.font-semibold').textContent = '検索結果がありません';
emptyState.querySelector('p.text-sm').textContent = '別のキーワードで試してください。';
} else {
emptyState.querySelector('p.font-semibold').textContent = '在庫がありません';
emptyState.querySelector('p.text-sm').textContent = '「新規追加」ボタンから最初の在庫を登録しましょう。';
}
}
}
if (listType === 'store') {
let totalPieces = 0;
allItems.forEach(item => {
totalPieces += item.quantity;
});
const capacity = Math.floor(totalPieces / 80);
document.getElementById('capacity-info').innerHTML = `
<p class="text-gray-600">現在のお客様対応可能人数(目安)</p>
<p class="text-3xl font-bold text-indigo-600">${capacity} <span class="text-lg font-medium">人</span></p>
<p class="text-xs text-gray-500 mt-1">※在庫合計 ${totalPieces}本 / 1人80本で計算</p>
`;
}
currentViewItems.forEach((item, index) => {
const quantity = isStore ? (item.quantity || 0) : (item.warehouse_quantity || 0);
const minStock = isStore ? (item.minStock || 0) : (item.warehouse_minStock || 0);
const orderedQuantity = isStore ? (item.orderedQuantity || 0) : (item.warehouse_orderedQuantity || 0);
let rowClass = 'bg-white';
if (quantity === 0) {
rowClass = 'zero-stock-item';
} else if (minStock > 0 && quantity < (minStock * 12)) {
rowClass = 'low-stock-item';
}
let orderStatusHTML = '';
if (isStore && item.lastOrderedDate) {
const orderedDate = item.lastOrderedDate.toDate().toLocaleDateString('ja-JP');
orderStatusHTML = `<div class="text-xs text-green-700">✔ ${orderedDate} 発注済み</div>`;
}
const itemElement = document.createElement('div');
itemElement.className = `md:grid md:grid-cols-12 md:gap-4 p-4 md:p-0 md:border-b border-gray-200 rounded-lg shadow-md md:shadow-none md:rounded-none mb-4 md:mb-0 ${rowClass}`;
const isManager = currentUser === 'あいちゃん';
const bagsDisplay = formatBagDisplay(quantity);
itemElement.innerHTML = `
<div class="md:col-span-3 md:px-6 md:py-4 flex justify-between items-center">
<div><div class="font-bold text-lg md:font-medium md:text-base text-gray-900">${item.name}</div>${orderStatusHTML}</div>
<div class="md:hidden flex flex-col items-center"><button data-id="${item.id}" data-type="${listType}" class="move-up-btn text-gray-400 hover:text-gray-800" ${index === 0 ? 'disabled' : ''}><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path></svg></button><button data-id="${item.id}" data-type="${listType}" class="move-down-btn text-gray-400 hover:text-gray-800" ${index === currentViewItems.length - 1 ? 'disabled' : ''}><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg></button></div>
</div>
<div class="grid grid-cols-2 gap-4 mt-4 md:mt-0 md:col-span-6 md:grid-cols-4">
<div class="text-center md:col-span-1 md:flex md:items-center md:justify-center"><div class="text-sm text-gray-500 md:hidden">在庫(袋)</div><div class="flex items-center justify-center gap-2"><button data-id="${item.id}" data-type="${listType}" data-change="-12" class="quantity-btn bg-red-100 text-red-700 rounded-full p-1 hover:bg-red-200 transition btn-focus-ring">-</button><span class="font-mono text-lg w-10 text-center">${bagsDisplay}</span><button data-id="${item.id}" data-type="${listType}" data-change="12" class="quantity-btn bg-green-100 text-green-700 rounded-full p-1 hover:bg-green-200 transition btn-focus-ring">+</button></div></div>
<div class="text-center md:col-span-1 md:flex md:items-center md:justify-center"><div class="text-sm text-gray-500 md:hidden">在庫(本)</div><span class="font-mono text-lg">${quantity}</span></div>
<div class="text-center md:col-span-1 md:flex md:items-center md:justify-center"><div class="text-sm text-gray-500 md:hidden">最低在庫</div><span class="font-mono text-lg">${minStock}</span></div>
<div class="text-center md:col-span-1 md:flex md:items-center md:justify-center"><div class="text-sm text-gray-500 md:hidden">発注数</div><div class="flex items-center justify-center gap-2"><button data-id="${item.id}" data-type="${listType}" data-change="-1" class="order-quantity-btn bg-gray-200 text-gray-700 rounded-full p-1 hover:bg-gray-300 transition btn-focus-ring">-</button><span class="font-mono text-lg w-10 text-center ${listType === 'store' && item.lastOrderedDate ? 'text-purple-700 font-bold' : ''}">${orderedQuantity || 0}</span><button data-id="${item.id}" data-type="${listType}" data-change="1" class="order-quantity-btn bg-gray-200 text-gray-700 rounded-full p-1 hover:bg-gray-300 transition btn-focus-ring">+</button></div></div>
</div>
<div class="md:col-span-3 md:px-6 md:py-4 flex items-center justify-center gap-2 mt-4 md:mt-0">
<div class="hidden md:flex flex-col"><button data-id="${item.id}" data-type="${listType}" class="move-up-btn text-gray-400 hover:text-gray-800" ${index === 0 ? 'disabled' : ''}><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path></svg></button><button data-id="${item.id}" data-type="${listType}" class="move-down-btn text-gray-400 hover:text-gray-800" ${index === currentViewItems.length - 1 ? 'disabled' : ''}><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg></button></div>
<button data-id="${item.id}" data-type="${listType}" class="history-btn text-blue-600 hover:text-blue-900 transition p-2 rounded-md hover:bg-blue-100">履歴</button>
<button data-id="${item.id}" data-type="${listType}" class="reset-stock-btn text-orange-600 hover:text-orange-900 transition px-2 py-1 text-xs font-bold rounded-md hover:bg-orange-100">リセット</button>
<button data-id="${item.id}" data-type="${listType}" class="edit-btn text-indigo-600 hover:text-indigo-900 transition p-2 rounded-md hover:bg-indigo-100" ${!isManager ? 'disabled' : ''}>編集</button>
<button data-id="${item.id}" data-type="${listType}" class="delete-btn text-red-600 hover:text-red-900 transition p-2 rounded-md hover:bg-red-100" ${!isManager ? 'disabled' : ''}>削除</button>
</div>
`;
inventoryList.appendChild(itemElement);
});
};
const renderFeedback = () => {
const feedbackList = document.getElementById('feedbackList');
feedbackList.innerHTML = '';
if (allFeedback.length === 0) {
feedbackList.innerHTML = '<p class="text-center text-gray-500">フィードバック履歴がありません。</p>';
return;
}
allFeedback.forEach(fb => {
const card = document.createElement('div');
card.className = 'bg-white p-4 rounded-lg shadow-md border';
const fbDate = fb.date.toDate().toLocaleDateString('ja-JP');
const isManager = currentUser === 'あいちゃん';
card.innerHTML = `
<div class="flex justify-between items-start">
<div>
<p class="font-bold text-lg">${fb.color}</p>
<p class="text-sm text-gray-500">${fbDate} by ${fb.staffName}</p>
</div>
<div class="flex gap-2">
${isManager ? `<button data-id="${fb.id}" class="delete-feedback-btn text-red-500 hover:text-red-700 p-1">削除</button>` : ''}
</div>
</div>
<div class="mt-4 grid grid-cols-2 sm:grid-cols-3 gap-4 text-sm">
<div><strong class="block text-gray-500">本数</strong>${fb.pieces}本</div>
<div><strong class="block text-gray-500">1本料金</strong>${fb.price}円</div>
<div><strong class="block text-gray-500">着用場所</strong>${fb.area}</div>
<div><strong class="block text-gray-500">ダイヤと混合</strong>${fb.mixed}</div>
<div class="col-span-2 sm:col-span-1"><strong class="block text-gray-500">動機</strong>${fb.motivation}</div>
</div>
<div class="mt-4 space-y-2">
${fb.motivationReason ? `<p><strong class="text-gray-600">理由:</strong>${fb.motivationReason}</p>` : ''}
<p><strong class="text-gray-600">感想:</strong>${fb.impressions || '記入なし'}</p>
<p><strong class="text-gray-600">結果:</strong>${fb.result || '記入なし'}</p>
</div>
`;
feedbackList.appendChild(card);
});
};
// --- モーダル制御 ---
const openModal = (modalId, item = null, listType = 'store') => {
const modal = document.getElementById(modalId);
if (!modal) return;
if (modalId === 'addEditModal') {
document.getElementById('addEditForm').reset();
document.getElementById('itemMinStock').value = 0;
document.getElementById('editListType').value = listType;
const modalTitle = document.getElementById('modalTitle');
const quantityLabel = document.getElementById('quantityLabel');
const minStockLabel = document.getElementById('minStockLabel');
if (item) { // Edit mode
modalTitle.textContent = listType === 'store' ? '店舗在庫の編集' : '倉庫在庫の編集';
quantityLabel.textContent = listType === 'store' ? '在庫数 (袋)' : '倉庫在庫数 (袋)';
minStockLabel.textContent = listType === 'store' ? '最低在庫 (袋)' : '倉庫最低在庫 (袋)';
document.getElementById('editItemId').value = item.id;
document.getElementById('itemName').value = item.name;
document.getElementById('itemName').readOnly = true; // Don't allow editing name
const quantity = listType === 'store' ? item.quantity : item.warehouse_quantity;
const minStock = listType === 'store' ? item.minStock : item.warehouse_minStock;
document.getElementById('itemQuantity').value = quantity / 12;
document.getElementById('itemMinStock').value = minStock || 0;
} else { // Add mode
modalTitle.textContent = '在庫の新規追加';
quantityLabel.textContent = '在庫数 (袋)';
minStockLabel.textContent = '最低在庫 (袋)';
document.getElementById('editItemId').value = '';
document.getElementById('itemName').readOnly = false;
}
}
modal.classList.remove('hidden');
};
const closeModal = (modalId) => {
const modal = document.getElementById(modalId);
if (modal) modal.classList.add('hidden');
};
const showAlert = (title, message) => {
document.getElementById('alertModalTitle').textContent = title;
document.getElementById('alertModalMessage').textContent = message;
document.getElementById('alertModal').classList.remove('hidden');
};
const openConfirmModal = (title, message, onConfirm) => {
const confirmModal = document.getElementById('confirmModal');
const confirmModalTitle = document.getElementById('confirmModalTitle');
const confirmModalMessage = document.getElementById('confirmModalMessage');
const confirmModalYesBtn = document.getElementById('confirmModalYesBtn');
confirmModalTitle.textContent = title;
confirmModalMessage.textContent = message;
const newYesBtn = confirmModalYesBtn.cloneNode(true);
confirmModalYesBtn.parentNode.replaceChild(newYesBtn, confirmModalYesBtn);
newYesBtn.addEventListener('click', () => {
onConfirm();
closeModal('confirmModal');
});
confirmModal.classList.remove('hidden');
};
// --- Firestore データ操作 ---
const setupFirestoreListeners = () => {
if (unsubscribeInventory) unsubscribeInventory();
inventoryCollection = collection(db, `apps/${appId}/inventory`);
const qInv = query(inventoryCollection);
unsubscribeInventory = onSnapshot(qInv, async (snapshot) => {
if (snapshot.empty && !snapshot.metadata.fromCache) {
await initializeDefaultData();
} else {
const itemsPromises = snapshot.docs.map(async (doc) => ({ id: doc.id, ...doc.data() }));
allItems = await Promise.all(itemsPromises);
allItems.sort((a, b) => a.order - b.order);
renderInventory('store');
renderInventory('warehouse');
populateItemSelector();
populateFeedbackColorSelector();
}
}, (error) => {
console.error("Firestoreデータ取得エラー: ", error);
showAlert("データベースエラー", "在庫データの読み込みに失敗しました。");
});
if (unsubscribeFeedback) unsubscribeFeedback();
feedbackCollection = collection(db, `apps/${appId}/prototype_feedback`);
const qFb = query(feedbackCollection);
unsubscribeFeedback = onSnapshot(qFb, async (snapshot) => {
const isSeeded = localStorage.getItem('feedbackDataSeeded');
if (snapshot.empty && !snapshot.metadata.fromCache && !isSeeded) {
await initializeHistoricalFeedbackData();
} else {
allFeedback = snapshot.docs.map(doc => ({id: doc.id, ...doc.data()}));
allFeedback.sort((a, b) => b.date.toMillis() - a.date.toMillis());
renderFeedback();
displayWeeklySummary();
}
}, (error) => {
console.error("Firestoreデータ取得エラー: ", error);
showAlert("データベースエラー", "フィードバックデータの読み込みに失敗しました。");
});
};
const addEventListeners = () => {
const addEditForm = document.getElementById('addEditForm');
const feedbackForm = document.getElementById('feedbackForm');
const inventoryView = document.getElementById('inventoryView');
const warehouseView = document.getElementById('warehouseView');
const feedbackList = document.getElementById('feedbackList');
const confirmOrderBtn = document.getElementById('confirmOrderBtn');
const copyOrderBtn = document.getElementById('copyOrderBtn');
const markAsOrderedBtn = document.getElementById('markAsOrderedBtn');
const itemsArrivedBtn = document.getElementById('itemsArrivedBtn');
const resetAnalysisBtn = document.getElementById('resetAnalysisBtn');
const itemHistorySelector = document.getElementById('itemHistorySelector');
const openAddModalBtn = document.getElementById('openAddModalBtn');
const cancelBtn = document.getElementById('cancelBtn');
const addEditModal = document.getElementById('addEditModal');
const closeHistoryModalBtn = document.getElementById('closeHistoryModalBtn');
const historyModal = document.getElementById('historyModal');
const closeOrderSummaryBtn = document.getElementById('closeOrderSummaryBtn');
const orderSummaryModal = document.getElementById('orderSummaryModal');
const closeReminderBtn = document.getElementById('close-reminder-btn');
const searchInput = document.getElementById('searchInput');
const warehouseSearchInput = document.getElementById('warehouseSearchInput');
const showInventoryBtn = document.getElementById('showInventoryBtn');
const showWarehouseBtn = document.getElementById('showWarehouseBtn');
const showManagementBtn = document.getElementById('showManagementBtn');
const showAnalysisBtn = document.getElementById('showAnalysisBtn');
const showFeedbackBtn = document.getElementById('showFeedbackBtn');
const managementView = document.getElementById('managementView');
const analysisView = document.getElementById('analysisView');
const feedbackView = document.getElementById('feedbackView');
const alertModalCloseBtn = document.getElementById('alertModalCloseBtn');
const confirmModal = document.getElementById('confirmModal');
const confirmModalNoBtn = document.getElementById('confirmModalNoBtn');
const weeklyReportBtn = document.getElementById('weeklyReportBtn');
const monthlyReportBtn = document.getElementById('monthlyReportBtn');
const weeklyReportView = document.getElementById('weeklyReportView');
const monthlyReportView = document.getElementById('monthlyReportView');
const handleInventoryClick = async (e, listType) => {
const button = e.target.closest('button');
if (!button) return;
const id = button.dataset.id;
if (!id) return;
const itemRef = doc(inventoryCollection, id);
const fieldToUpdate = listType === 'store' ? 'quantity' : 'warehouse_quantity';
const minStockField = listType === 'store' ? 'minStock' : 'warehouse_minStock';
const orderedField = listType === 'store' ? 'orderedQuantity' : 'warehouse_orderedQuantity';
const historyCollectionName = listType === 'store' ? 'history' : 'warehouse_history';
if (button.classList.contains('quantity-btn')) {
const change = Number(button.dataset.change);
try {
await runTransaction(db, async (transaction) => {
const itemDoc = await transaction.get(itemRef);
if (!itemDoc.exists()) throw new Error("商品が見つかりません。");
const currentQuantity = itemDoc.data()[fieldToUpdate] || 0;
const newQuantity = currentQuantity + change;
if (newQuantity < 0) {
return;
}
let updateData = {};
updateData[fieldToUpdate] = newQuantity;
transaction.update(itemRef, updateData);
const historyColRef = collection(itemRef, historyCollectionName);
const newHistoryRef = doc(historyColRef);
transaction.set(newHistoryRef, {
change: change,
newQuantity: newQuantity,
timestamp: serverTimestamp(),
staffName: currentUser
});
});
} catch (error) {
console.error("在庫更新トランザクションエラー: ", error);
showAlert("更新エラー", "在庫の更新に失敗しました。他のユーザーと同時に操作した可能性があります。");
}
} else if (button.classList.contains('reset-stock-btn')) {
const item = allItems.find(i => i.id === id);
if (!item) return;
openConfirmModal(
'在庫リセットの確認',
`本当に「${item.name}」の在庫を0にリセットしますか?この操作は元に戻せません。`,
async () => {
try {
await runTransaction(db, async (transaction) => {
const itemDoc = await transaction.get(itemRef);
if (!itemDoc.exists()) {
throw new Error("商品が見つかりません。");
}
const oldQuantity = itemDoc.data()[fieldToUpdate] || 0;
if (oldQuantity === 0) return;
let updateData = {};
updateData[fieldToUpdate] = 0;
transaction.update(itemRef, updateData);
const historyColRef = collection(itemRef, historyCollectionName);
const newHistoryRef = doc(historyColRef);
transaction.set(newHistoryRef, {
change: -oldQuantity,
newQuantity: 0,
timestamp: serverTimestamp(),
staffName: `在庫リセット (${currentUser})`
});
});
showAlert("成功", `「${item.name}」の在庫をリセットしました。`);
} catch (error) {
console.error("在庫リセットエラー: ", error);
showAlert("エラー", `在庫のリセットに失敗しました: ${error.message}`);
}
}
);
} else if (button.classList.contains('order-quantity-btn')) {
const item = allItems.find(i => i.id === id);
if (!item) return;
const change = Number(button.dataset.change);
const currentOrdered = item[orderedField] || 0;
const newOrderedQuantity = currentOrdered + change;
if (newOrderedQuantity >= 0) {
let updateData = {};
updateData[orderedField] = newOrderedQuantity;
await updateDoc(itemRef, updateData);
}
} else if (button.classList.contains('delete-btn')) {
if (currentUser === 'あいちゃん') await deleteDoc(itemRef);
} else if (button.classList.contains('edit-btn')) {
const item = allItems.find(i => i.id === id);
if (currentUser === 'あいちゃん' && item) openModal('addEditModal', item, listType);
} else if (button.classList.contains('history-btn')) {
const item = allItems.find(i => i.id === id);
if (!item) return;
openModal('historyModal', item);
document.getElementById('historyModalTitle').textContent = `「${item.name}」の変更履歴 (${listType === 'store' ? '店舗' : '倉庫'})`;
const historyCol = collection(itemRef, historyCollectionName);
const historySnapshot = await getDocs(query(historyCol));
const historyModalContent = document.getElementById('historyModalContent');
if (historySnapshot.empty) {
historyModalContent.innerHTML = '<p class="text-center text-gray-500 p-4">変更履歴はありません。</p>';
} else {
historyModalContent.innerHTML = '<ul class="space-y-2"></ul>';
const ul = historyModalContent.querySelector('ul');
const historyData = historySnapshot.docs.map(d => d.data());
historyData.sort((a,b) => b.timestamp.toMillis() - a.timestamp.toMillis());
historyData.forEach(history => {
const date = history.timestamp?.toDate().toLocaleString('ja-JP') || '日付不明';
const changeText = `${history.change > 0 ? '+' : ''}${history.change}本`;
const changeColor = history.change > 0 ? 'text-green-600' : 'text-red-600';
const staffName = history.staffName || '不明';
const li = document.createElement('li');
li.className = 'p-3 bg-gray-50 rounded-md flex justify-between items-center';
li.innerHTML = `<div><span class="font-bold ${changeColor}">${changeText}</span> <span class="text-gray-800">→ 新在庫: ${history.newQuantity}本</span></div><div class="text-xs text-gray-500 text-right"><span>${date}</span><br><span>by ${staffName}</span></div>`;
ul.appendChild(li);
});
}
} else if (button.classList.contains('move-up-btn') || button.classList.contains('move-down-btn')) {
const itemIndex = allItems.findIndex(i => i.id === id);
if (itemIndex === -1) return;
const item = allItems[itemIndex];
const direction = button.classList.contains('move-up-btn') ? -1 : 1;
const otherIndex = itemIndex + direction;
if (otherIndex >= 0 && otherIndex < allItems.length) {
const batch = writeBatch(db);
const otherItemRef = doc(inventoryCollection, allItems[otherIndex].id);
batch.update(itemRef, { order: allItems[otherIndex].order });
batch.update(otherItemRef, { order: item.order });
await batch.commit();
}
}
};
inventoryView.addEventListener('click', (e) => handleInventoryClick(e, 'store'));
warehouseView.addEventListener('click', (e) => handleInventoryClick(e, 'warehouse'));
addEditForm.addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('editItemId').value;
const listType = document.getElementById('editListType').value;
const data = {};
const name = document.getElementById('itemName').value;
const quantity = Number(document.getElementById('itemQuantity').value) * 12;
const minStock = Number(document.getElementById('itemMinStock').value);
if (id) { // Edit mode
if (listType === 'store') {
data.quantity = quantity;
data.minStock = minStock;
} else {
data.warehouse_quantity = quantity;
data.warehouse_minStock = minStock;
}
data.updatedAt = serverTimestamp();
data.updatedBy = currentUser;
await updateDoc(doc(inventoryCollection, id), data);
} else { // Add mode
data.name = name;
data.quantity = quantity;
data.minStock = minStock;
data.order = allItems.length;
data.createdAt = serverTimestamp();
data.warehouse_quantity = 0;
data.warehouse_minStock = 0;
data.warehouse_orderedQuantity = 0;
await addDoc(inventoryCollection, data);
}
closeModal('addEditModal');
});
feedbackForm.addEventListener('submit', async (e) => {
e.preventDefault();
const usedPieces = Number(document.getElementById('feedbackPieces').value);
const usedItemName = document.getElementById('feedbackColor').value;
const itemToUpdate = allItems.find(item => item.name === usedItemName);
if (!itemToUpdate) {
showAlert('エラー', '在庫リストに該当の商品が見つかりません。');
return;
}
if (usedPieces <= 0) {
showAlert('エラー', '本数は1以上の数値を入力してください。');
return;
}
const feedbackData = {
date: Timestamp.fromDate(new Date(document.getElementById('feedbackDate').value)),
color: usedItemName,
pieces: usedPieces,
price: Number(document.getElementById('feedbackPrice').value),
area: document.getElementById('feedbackArea').value,
mixed: document.getElementById('feedbackMixed').value,
motivation: document.getElementById('feedbackMotivation').value,
motivationReason: document.getElementById('feedbackMotivationReason').value,
impressions: document.getElementById('feedbackImpressions').value,
result: document.getElementById('feedbackResult').value,
staffName: currentUser,
createdAt: serverTimestamp()
};
const itemDocRef = doc(inventoryCollection, itemToUpdate.id);
try {
await runTransaction(db, async (transaction) => {
const itemDoc = await transaction.get(itemDocRef);
if (!itemDoc.exists()) {
throw new Error("商品が見つかりません!");
}
const newQuantity = itemDoc.data().quantity - usedPieces;
if (newQuantity < 0) {
throw new Error("在庫が不足しています!");
}
transaction.update(itemDocRef, { quantity: newQuantity });
const historyColRef = collection(itemDocRef, 'history');
const newHistoryRef = doc(historyColRef);
transaction.set(newHistoryRef, {
change: -usedPieces,
newQuantity: newQuantity,
timestamp: serverTimestamp(),
staffName: `フィードバック (${currentUser})`
});
});
await addDoc(feedbackCollection, feedbackData);
feedbackForm.reset();
document.getElementById('feedbackDate').valueAsDate = new Date();
showAlert("成功", "フィードバックを保存し、在庫を更新しました。");
} catch (error) {
console.error("フィードバック保存・在庫更新エラー: ", error);
showAlert("エラー", `処理に失敗しました: ${error.message}`);
}
});
feedbackList.addEventListener('click', async (e) => {
const button = e.target.closest('button.delete-feedback-btn');
if (button && currentUser === 'あいちゃん') {
const id = button.dataset.id;
await deleteDoc(doc(feedbackCollection, id));
}
});
confirmOrderBtn.addEventListener('click', () => {
const itemsToOrder = allItems.filter(item => item.orderedQuantity > 0);
if (itemsToOrder.length === 0) {
showAlert('通知', '発注する商品がありません。発注数を設定してください。');
return;
}
let orderText = "【スーパーロングエクステ発注依頼】\n\n";
itemsToOrder.forEach(item => {
orderText += `・${item.name}: ${item.orderedQuantity}袋\n`;
});
orderText += `\n担当: ${currentUser}`;
document.getElementById('orderTextForCopy').value = orderText;
openModal('orderSummaryModal');
});
copyOrderBtn.addEventListener('click', () => {
document.getElementById('orderTextForCopy').select();
document.execCommand('copy');
copyOrderBtn.textContent = 'コピーしました!';
setTimeout(() => {
copyOrderBtn.textContent = 'クリップボードにコピー';
}, 2000);
});
markAsOrderedBtn.addEventListener('click', async () => {
const itemsToOrder = allItems.filter(item => item.orderedQuantity > 0);
const batch = writeBatch(db);
itemsToOrder.forEach(item => {
const docRef = doc(inventoryCollection, item.id);
batch.update(docRef, { lastOrderedDate: serverTimestamp() });
const orderLogRef = doc(collection(db, `apps/${appId}/order_logs`));
batch.set(orderLogRef, {
itemId: item.id,
name: item.name,
quantityOrdered: item.orderedQuantity,
timestamp: serverTimestamp(),
staffName: currentUser
});
});
await batch.commit();
closeModal('orderSummaryModal');
});
itemsArrivedBtn.addEventListener('click', async () => {
const batch = writeBatch(db);
allItems.forEach(item => {
if (item.lastOrderedDate || (item.orderedQuantity && item.orderedQuantity > 0)) {
const docRef = doc(inventoryCollection, item.id);
batch.update(docRef, {
lastOrderedDate: null,
orderNote: "",
orderedQuantity: null
});
}
});
await batch.commit();
});
resetAnalysisBtn.addEventListener('click', async () => {
const batch = writeBatch(db);
const snapshot = await getDocs(feedbackCollection);
snapshot.forEach(doc => {
batch.delete(doc.ref);
});
localStorage.removeItem('feedbackDataSeeded'); // Allow re-seeding
await batch.commit();
});
itemHistorySelector.addEventListener('change', (e) => {
if (e.target.value) {
renderHistoryChart(e.target.value);
} else {
if(historyChartInstance) historyChartInstance.destroy();
document.getElementById('historyChartFeedback').textContent = '商品を選択して在庫の推移を確認します。';
}
});
const viewButtons = {
showInventoryBtn: 'inventoryView',
showWarehouseBtn: 'warehouseView',
showManagementBtn: 'managementView',
showAnalysisBtn: 'analysisView',
showFeedbackBtn: 'feedbackView',
};
Object.keys(viewButtons).forEach(btnId => {
document.getElementById(btnId).addEventListener('click', (e) => {
Object.values(viewButtons).forEach(viewId => {
const view = document.getElementById(viewId);
if(view) view.classList.add('hidden');
});
const targetViewId = viewButtons[btnId];
const targetView = document.getElementById(targetViewId);
if(targetView) targetView.classList.remove('hidden');
Object.keys(viewButtons).forEach(b => {
const button = document.getElementById(b);
button.classList.remove('text-indigo-600', 'border-indigo-600');
button.classList.add('text-gray-500');
});
e.target.classList.add('text-indigo-600', 'border-indigo-600');
e.target.classList.remove('text-gray-500');
if (btnId === 'showAnalysisBtn') {
renderAnalysisDashboard();
} else if (btnId === 'showManagementBtn') {
renderManagementView('weekly');
}
});
});
weeklyReportBtn.addEventListener('click', () => {
weeklyReportView.classList.remove('hidden');
monthlyReportView.classList.add('hidden');
weeklyReportBtn.classList.add('tab-btn-active');
monthlyReportBtn.classList.remove('tab-btn-active');
renderManagementView('weekly');
});
monthlyReportBtn.addEventListener('click', () => {
monthlyReportView.classList.remove('hidden');
weeklyReportView.classList.add('hidden');
monthlyReportBtn.classList.add('tab-btn-active');
weeklyReportBtn.classList.remove('tab-btn-active');
renderManagementView('monthly');
});
openAddModalBtn.addEventListener('click', () => openModal('addEditModal', null, 'store'));
cancelBtn.addEventListener('click', () => closeModal('addEditModal'));
addEditModal.addEventListener('click', (e) => { if (e.target === addEditModal) closeModal('addEditModal'); });
closeHistoryModalBtn.addEventListener('click', () => closeModal('historyModal'));
historyModal.addEventListener('click', (e) => { if (e.target === historyModal) closeModal('historyModal'); });
closeOrderSummaryBtn.addEventListener('click', () => closeModal('orderSummaryModal'));
orderSummaryModal.addEventListener('click', (e) => { if(e.target === orderSummaryModal) closeModal('orderSummaryModal'); });
closeReminderBtn.addEventListener('click', () => document.getElementById('order-reminder').classList.add('hidden'));
searchInput.addEventListener('input', () => renderInventory('store'));
warehouseSearchInput.addEventListener('input', () => renderInventory('warehouse'));
alertModalCloseBtn.addEventListener('click', () => closeModal('alertModal'));
confirmModalNoBtn.addEventListener('click', () => closeModal('confirmModal'));
confirmModal.addEventListener('click', (e) => { if(e.target === confirmModal) closeModal('confirmModal'); });
}
const initializeDefaultData = async () => {
const initialMinStock = {'20': 7, 'A17': 4, 'A14': 4, 'A9': 7, '4#': 13, '9#': 7, '17#': 4, '20W': 7, '1A(ハンナオリジナルカラー)': 24};
const predefinedItems = ['S1', '20', 'A17', 'A14', 'A9', 'A4', '9#', '17#', '20#', '22#', '40W', '20W', '1A(ハンナオリジナルカラー)', '4#'];
const uniquePredefinedItems = [...new Set(predefinedItems)];
const batch = writeBatch(db);
uniquePredefinedItems.forEach((name, index) => {
const docRef = doc(collection(db, `apps/${appId}/inventory`));
batch.set(docRef, {
name: name,
quantity: 0,
minStock: initialMinStock[name] || 0,
order: index,
createdAt: serverTimestamp(),
lastOrderedDate: null,
orderNote: "",
orderedQuantity: null,
warehouse_quantity: 0,
warehouse_minStock: 0,
warehouse_orderedQuantity: 0
});
});
await batch.commit();
};
const initializeHistoricalFeedbackData = async () => {
const historicalData = [
{ date: '2025/06/28', staffName: 'あいちゃん', color: '4A', pieces: 60, price: 450, impressions: '表面が切れているため馴染みやすかった' },
{ date: '2025/07/05', staffName: 'いっき', color: '1A', pieces: 70, price: 450, impressions: '地毛が胸下だが、馴染んだ' },
{ date: '2025/07/02', staffName: 'あいちゃん', color: '1A', pieces: 68, price: 450, impressions: '愛/テスト' },
{ date: '2025/07/04', staffName: 'あいちゃん', color: '1A', pieces: 68, price: 450, impressions: '愛/テスト' },
{ date: '2025/07/04', staffName: 'あいちゃん', color: '20W', pieces: 70, price: 450, impressions: 'テスト' },
{ date: '2025/07/05', staffName: 'あいちゃん', color: '4#', pieces: 50, price: 450, impressions: '' },
{ date: '2025/07/06', staffName: 'あいちゃん', color: '1A', pieces: 50, price: 450, impressions: 'ショートカットでもダイヤと半分で馴染んだ' },
{ date: '2025/07/08', staffName: 'あつき', color: '4#', pieces: 30, price: 450, impressions: 'はちうえの方だけ付けた。切るところによっては毛先の重さも軽くなりすぎなくてとても良かった' },
{ date: '2025/07/08', staffName: 'あつき', color: '1A', pieces: 24, price: 450, impressions: 'お客様は髪の量が少なかったのでエクステか地毛か見分けがつかないぐらい自然になった。毛先は10cm以上切った。(ダイヤの毛先まで)かなり重めの仕上がりでよかった' },
{ date: '2025/07/08', staffName: 'あいちゃん', color: '20A', pieces: 40, price: 450, impressions: '本数が前と足りなかったため多めの本数で染める必要があった' },
{ date: '2025/07/08', staffName: 'あつき', color: '14A', pieces: 24, price: 450, impressions: 'はちうえに付けた。お客様は毛先の関係でダイヤと1本交互でつけた。ダイヤの毛先ぐらいで切ると馴染み本数でも重みは出すことは出来た。' },
{ date: '2025/07/09', staffName: 'あつき', color: '4#', pieces: 35, price: 450, impressions: 'はちうえにつけました。状態)ボブ、毛量多い、毛先が少しブリーチ毛色の色落ち、馴染ませカットあり。毛の詰まりがあるおかげで、多少馴染みずらそうなボブやブリーチの色落ちで色が違くてもスーパーロングなら馴染む!' },
{ date: '2025/07/10', staffName: 'いっき', color: '1A', pieces: 80, price: 450, impressions: '普段110~120でつけてた人が80本で満足していた' },
{ date: '2025/07/10', staffName: 'あいちゃん', color: '9A', pieces: 10, price: 450, impressions: '毛量が気になるため少しだけ取り付けた' },
{ date: '2025/07/11', staffName: 'あいちゃん', color: '1A', pieces: 50, price: 450, impressions: 'ダイヤ40Mixで馴染んだ。バイトが染めたのだが塗りが浅く多くのけて目立った' }
];
const batch = writeBatch(db);
historicalData.forEach(data => {
const docRef = doc(collection(db, `apps/${appId}/prototype_feedback`));
const [year, month, day] = data.date.split('/');
const feedbackDate = new Date(year, month - 1, day);
batch.set(docRef, {
date: Timestamp.fromDate(feedbackDate),
staffName: data.staffName,
color: data.color,
pieces: data.pieces,
price: data.price,
impressions: data.impressions,
area: '',
mixed: 'いいえ',
motivation: '来店してから決めた',
motivationReason: '',
result: '',
createdAt: serverTimestamp()
});
});
await batch.commit();
localStorage.setItem('feedbackDataSeeded', 'true');
}
// --- 分析機能 ---
function renderAnalysisDashboard() {
const analysisSummary = document.getElementById('analysisSummary');
const motivationChartFeedback = document.getElementById('motivationChartFeedback');
const areaAnalysisList = document.getElementById('areaAnalysisList');
const impressionsList = document.getElementById('impressionsList');
const resultsList = document.getElementById('resultsList');
const popularColorList = document.getElementById('popularColorList');
if (allFeedback.length === 0) {
analysisSummary.innerHTML = '<p class="col-span-2 md:col-span-4 text-center text-gray-500">分析できるフィードバックデータがありません。</p>';
if (motivationChartInstance) motivationChartInstance.destroy();
areaAnalysisList.innerHTML = '';
impressionsList.innerHTML = '';
resultsList.innerHTML = '';
popularColorList.innerHTML = '';
return;
}
let totalRevenue = 0;
let totalPieces = 0;
const motivationData = { '元からつけたかった': 0, '来店してから決めた': 0 };
const areaData = {};
const colorCounts = {};
let impressionsHTML = '';
let resultsHTML = '';
allFeedback.forEach(fb => {
totalRevenue += fb.price * fb.pieces;
totalPieces += fb.pieces;
motivationData[fb.motivation] = (motivationData[fb.motivation] || 0) + 1;
colorCounts[fb.color] = (colorCounts[fb.color] || 0) + 1;
if(fb.area) {
const areas = fb.area.split(/[,、\s]+/).filter(Boolean);
areas.forEach(area => {
areaData[area] = (areaData[area] || 0) + 1;
});
}
if(fb.impressions) impressionsHTML += `<li class="border-b border-gray-200 pb-1 mb-1">${fb.impressions}</li>`;
if(fb.result) resultsHTML += `<li class="border-b border-gray-200 pb-1 mb-1">${fb.result}</li>`;
});
const customerCount = allFeedback.length;
const avgPrice = customerCount > 0 ? (totalRevenue / customerCount).toFixed(0) : 0;
const avgPieces = customerCount > 0 ? (totalPieces / customerCount).toFixed(1) : 0;
analysisSummary.innerHTML = `
<div class="p-4 bg-gray-50 rounded-lg">
<div class="text-sm text-gray-500">施術人数</div>
<div class="text-2xl font-bold">${customerCount}人</div>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<div class="text-sm text-gray-500">総売上</div>
<div class="text-2xl font-bold">${totalRevenue.toLocaleString()}円</div>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<div class="text-sm text-gray-500">平均単価</div>
<div class="text-2xl font-bold">${avgPrice.toLocaleString()}円</div>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<div class="text-sm text-gray-500">平均本数</div>
<div class="text-2xl font-bold">${avgPieces}本</div>
</div>
`;
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#4b5563'
}
}
},
scales: {
x: {
ticks: { color: '#6b7280' },
grid: { color: '#e5e7eb' }
},
y: {
ticks: { color: '#6b7280' },
grid: { color: '#e5e7eb' }
}
}
};
const motivationCtx = document.getElementById('motivationChart').getContext('2d');
if (motivationChartInstance) motivationChartInstance.destroy();
motivationChartInstance = new Chart(motivationCtx, {
type: 'pie',
data: {
labels: Object.keys(motivationData),
datasets: [{ data: Object.values(motivationData), backgroundColor: ['#6366f1', '#8b5cf6'] }]
},
options: chartOptions
});
const sortedAreas = Object.entries(areaData).sort(([,a],[,b]) => b-a);
areaAnalysisList.innerHTML = sortedAreas.map(entry => `<li class="flex justify-between"><span>${entry[0]}</span> <strong>${entry[1]}件</strong></li>`).join('') || '<p class="text-gray-500">データがありません</p>';
const sortedColors = Object.entries(colorCounts).sort(([, a], [, b]) => b - a);
popularColorList.innerHTML = sortedColors
.map(([color, count]) => `
<li class="border-b border-gray-200 py-1 flex justify-between items-center">
<span>${color}</span>
<span class="font-semibold">${count} 件</span>
</li>
`)
.join('') || '<p class="text-gray-500">データがありません</p>';
impressionsList.innerHTML = `<ul>${impressionsHTML}</ul>` || '<p class="text-gray-500">感想はありません</p>';
resultsList.innerHTML = `<ul>${resultsHTML}</ul>` || '<p class="text-gray-500">結果はありません</p>';
}
async function renderHistoryChart(itemId) {
const historyChartFeedback = document.getElementById('historyChartFeedback');
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const historyCol = collection(db, `apps/${appId}/inventory/${itemId}/history`);
const q = query(historyCol, where('timestamp', '>=', thirtyDaysAgo));
const querySnapshot = await getDocs(q);
const historyData = querySnapshot.docs.map(doc => doc.data());
historyData.sort((a,b) => a.timestamp.toMillis() - b.timestamp.toMillis());
const labels = [];
const data = [];
historyData.forEach(h => {
labels.push(h.timestamp.toDate().toLocaleDateString('ja-JP'));
data.push(h.newQuantity);
});
if (data.length > 1) {
const start = data[0];
const end = data[data.length - 1];
if (end < start) {
historyChartFeedback.textContent = `この商品は減少傾向にあります。発注を検討しましょう。`;
} else if (end > start) {
historyChartFeedback.textContent = `この商品は増加傾向にあります。在庫は十分です。`;
} else {
historyChartFeedback.textContent = `この商品の在庫は安定しています。`;
}
} else {
historyChartFeedback.textContent = '過去30日間のデータが不足しているため、傾向は分析できません。';
}
const ctx = document.getElementById('historyChart').getContext('2d');
if (historyChartInstance) historyChartInstance.destroy();
historyChartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '在庫数 (本)',
data: data,
fill: false,
borderColor: '#4f46e5',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { labels: { color: '#4b5563' } } },
scales: {
x: { ticks: { color: '#6b7280' }, grid: { color: '#e5e7eb' } },
y: { ticks: { color: '#6b7280' }, grid: { color: '#e5e7eb' } }
}
}
});
}
function populateItemSelector() {
const itemHistorySelector = document.getElementById('itemHistorySelector');
itemHistorySelector.innerHTML = '<option value="">商品を選択...</option>';
allItems.forEach(item => {
const option = document.createElement('option');
option.value = item.id;
option.textContent = item.name;
itemHistorySelector.appendChild(option);
});
}
function populateFeedbackColorSelector() {
const feedbackColor = document.getElementById('feedbackColor');
feedbackColor.innerHTML = '<option value="">色を選択...</option>';
allItems.forEach(item => {
const option = document.createElement('option');
option.value = item.name;
option.textContent = item.name;
feedbackColor.appendChild(option);
});
}
function populateSelectWithOptions(selectId, start, end, step, suffix = '') {
const select = document.getElementById(selectId);
select.innerHTML = '';
for (let i = start; i <= end; i += step) {
const option = document.createElement('option');
option.value = i;
option.textContent = `${i}${suffix}`;
select.appendChild(option);
}
}
async function displayWeeklySummary() {
const highlightBox = document.getElementById('weekly-summary-highlight');
const today = new Date();
const dayOfWeek = today.getDay();
if (dayOfWeek !== 2) {
highlightBox.classList.add('hidden');
return;
}
const lastSunday = new Date(today);
lastSunday.setDate(today.getDate() - dayOfWeek);
lastSunday.setHours(23, 59, 59, 999);
const lastTuesday = new Date(lastSunday);
lastTuesday.setDate(lastSunday.getDate() - 5);
lastTuesday.setHours(0, 0, 0, 0);
const q = query(feedbackCollection, where("date", ">=", lastTuesday), where("date", "<=", lastSunday));
const querySnapshot = await getDocs(q);
let totalCustomers = 0;
let totalPieces = 0;
if (!querySnapshot.empty) {
totalCustomers = querySnapshot.size;
querySnapshot.forEach(doc => {
totalPieces += doc.data().pieces;
});
}
highlightBox.innerHTML = `
<h3 class="font-bold text-lg text-yellow-800 mb-2">先週のハイライト (火〜日)</h3>
<div class="grid grid-cols-2 gap-4 text-center mt-2">
<div>
<p class="text-sm text-yellow-700">施術人数</p>
<p class="text-2xl font-bold text-yellow-900">${totalCustomers} <span class="text-base font-medium">人</span></p>
</div>
<div>
<p class="text-sm text-yellow-700">施術本数</p>
<p class="text-2xl font-bold text-yellow-900">${totalPieces} <span class="text-base font-medium">本</span></p>
</div>
</div>
`;
highlightBox.classList.remove('hidden');
}
async function renderManagementView(period) {
const contentEl = document.getElementById(period === 'weekly' ? 'weeklyReportContent' : 'monthlyReportContent');
const dateRangeEl = document.getElementById(period === 'weekly' ? 'weeklyDateRange' : 'monthlyDateRange');
contentEl.innerHTML = '<p>レポートを生成中...</p>';
const today = new Date();
let startDate, endDate;
if (period === 'weekly') {
const dayOfWeek = today.getDay(); // 0=Sun, 1=Mon, 2=Tue...
const diffToTuesday = dayOfWeek >= 2 ? dayOfWeek - 2 : dayOfWeek + 5;
startDate = new Date(today);
startDate.setDate(today.getDate() - diffToTuesday);
startDate.setHours(0, 0, 0, 0);
endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6); // Tuesday to Sunday
endDate.setHours(23, 59, 59, 999);
dateRangeEl.textContent = `${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`;
} else { // monthly
startDate = new Date(today.getFullYear(), today.getMonth(), 1);
endDate = new Date(today.getFullYear(), today.getMonth() + 1, 0);
endDate.setHours(23, 59, 59, 999);
dateRangeEl.textContent = `${today.getFullYear()}年${today.getMonth() + 1}月`;
}
const reportData = {};
allItems.forEach(item => {
reportData[item.name] = { consumed: 0, ordered: 0 };
});
// Fetch consumption data from feedback
const feedbackQuery = query(feedbackCollection, where("date", ">=", startDate), where("date", "<=", endDate));
const feedbackSnapshot = await getDocs(feedbackQuery);
feedbackSnapshot.forEach(doc => {
const feedback = doc.data();
if (reportData[feedback.color]) {
reportData[feedback.color].consumed += feedback.pieces;
}
});
// Fetch order data from order_logs
orderLogsCollection = collection(db, `apps/${appId}/order_logs`);
const orderQuery = query(orderLogsCollection, where('timestamp', '>=', startDate), where('timestamp', '<=', endDate));
const orderSnapshot = await getDocs(orderQuery);
orderSnapshot.forEach(doc => {
const orderLog = doc.data();
if (reportData[orderLog.name]) {
reportData[orderLog.name].ordered += orderLog.quantityOrdered;
}
});
let tableHTML = `
<div class="overflow-x-auto">
<table class="w-full text-left table-auto">
<thead class="bg-gray-100">
<tr>
<th class="p-2">商品名</th>
<th class="p-2 text-center">消費数(本)</th>
<th class="p-2 text-center">発注数(袋)</th>
</tr>
</thead>
<tbody>
`;
let hasData = false;
for (const name in reportData) {
const data = reportData[name];
if (data.consumed > 0 || data.ordered > 0) {
hasData = true;
tableHTML += `
<tr class="border-b">
<td class="p-2 font-medium">${name}</td>
<td class="p-2 text-center">${data.consumed}</td>
<td class="p-2 text-center">${data.ordered}</td>
</tr>
`;
}
}
if (!hasData) {
tableHTML += `<tr><td colspan="3" class="text-center p-4 text-gray-500">この期間のデータはありません。</td></tr>`;
}
tableHTML += `
</tbody>
</table>
</div>
`;
contentEl.innerHTML = tableHTML;
}
// --- アプリケーション初期化 ---
const initialize = async () => {
try {
app = initializeApp(firebaseConfig);
db = getFirestore(app);
auth = getAuth(app);
onAuthStateChanged(auth, (user) => {
if (user) {
userId = user.uid;
setupFirestoreListeners();
}
});
await signInAnonymously(auth);
addEventListeners();
displayDynamicComment();
setInterval(displayDynamicComment, 300000);
checkOrderDay();
document.getElementById('feedbackDate').valueAsDate = new Date();
populateSelectWithOptions('feedbackPrice', 450, 650, 50, '円');
} catch (error) {
console.error("Firebase初期化エラー: ", error);
showAlert("致命的なエラー", `アプリケーションの初期化に失敗しました。エラー: ${error.message}`);
}
};
</script>
</body>
</html>