後台調整

This commit is contained in:
larry2701 2025-05-29 17:47:37 +08:00
parent a560065aec
commit 6e1b511d19
30 changed files with 5078 additions and 10 deletions

122
Backstage/admin_add.html Normal file
View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>新增管理員帳號</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">新增管理員帳號</h2>
<form id="addAdminForm">
<div class="row">
<div class="col-md-6 mb-3">
<label for="creationDate" class="form-label">建立日期</label>
<input type="date" class="form-control" id="creationDate" required>
</div>
<div class="col-md-6 mb-3">
<label for="adminCategory" class="form-label">管理員類別</label>
<select class="form-select" id="adminCategory" required>
<option value="" disabled selected>請選擇類別</option>
<option value="最高管理員">最高管理員</option>
<option value="管委會委員">管委會委員</option>
<option value="管理員">管理員</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="adminName" class="form-label">管理員姓名</label>
<input type="text" class="form-control" id="adminName" placeholder="輸入管理員姓名" required>
</div>
<div class="col-md-6 mb-3">
<label for="username" class="form-label">登入帳號</label>
<input type="text" class="form-control" id="username" placeholder="輸入登入帳號" required autocomplete="username">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="password" class="form-label">密碼</label>
<input type="password" class="form-control" id="password" placeholder="輸入密碼" required autocomplete="new-password">
</div>
<div class="col-md-6 mb-3">
<label for="confirmPassword" class="form-label">確認密碼</label>
<input type="password" class="form-control" id="confirmPassword" placeholder="再次輸入密碼" required autocomplete="new-password">
</div>
</div>
<div class="mb-3">
<label for="notes" class="form-label">備註</label>
<textarea class="form-control" id="notes" rows="3" placeholder="輸入其他備註事項 (選填)"></textarea>
</div>
<button type="submit" class="btn btn-primary">儲存管理員帳號</button>
<a href="admin_list.html" class="btn btn-secondary ms-2">返回列表</a>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const today = new Date();
const creationDateInput = document.getElementById('creationDate');
if(creationDateInput) {
creationDateInput.value = today.toISOString().split('T')[0];
}
});
document.getElementById('addAdminForm').addEventListener('submit', function(event) {
event.preventDefault();
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (password !== confirmPassword) {
alert("密碼與確認密碼不相符,請重新輸入。");
document.getElementById('confirmPassword').focus();
return;
}
// Check if username already exists
const username = document.getElementById('username').value;
let existingAdmins = JSON.parse(localStorage.getItem('admins_management_data_temp')) || [];
if (existingAdmins.some(admin => admin.username === username)) {
alert("此登入帳號已被使用,請選擇其他帳號。");
document.getElementById('username').focus();
return;
}
const newAdmin = {
id: Date.now(), // Unique ID
creationDate: document.getElementById('creationDate').value,
adminName: document.getElementById('adminName').value,
username: username,
password: password, // In a real application, hash the password before saving
adminCategory: document.getElementById('adminCategory').value,
notes: document.getElementById('notes').value
};
console.log("新增管理員資料:", newAdmin);
existingAdmins.push(newAdmin);
localStorage.setItem('admins_management_data_temp', JSON.stringify(existingAdmins));
alert("管理員帳號已成功新增!\n管理員姓名" + newAdmin.adminName + "\n登入帳號" + newAdmin.username);
this.reset();
const today = new Date();
const creationDateInput = document.getElementById('creationDate');
if(creationDateInput) {
creationDateInput.value = today.toISOString().split('T')[0];
}
document.getElementById('adminName').focus();
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

159
Backstage/admin_edit.html Normal file
View File

@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>編輯管理員帳號</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">編輯管理員帳號</h2>
<form id="editAdminForm">
<input type="hidden" id="adminId" name="adminId">
<div class="row">
<div class="col-md-6 mb-3">
<label for="creationDate" class="form-label">建立日期</label>
<input type="date" class="form-control" id="creationDate" readonly>
</div>
<div class="col-md-6 mb-3">
<label for="adminCategory" class="form-label">管理員類別</label>
<select class="form-select" id="adminCategory" required>
<option value="最高管理員">最高管理員</option>
<option value="管委會委員">管委會委員</option>
<option value="管理員">管理員</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="adminName" class="form-label">管理員姓名</label>
<input type="text" class="form-control" id="adminName" placeholder="輸入管理員姓名" required>
</div>
<div class="col-md-6 mb-3">
<label for="username" class="form-label">登入帳號</label>
<input type="text" class="form-control" id="username" placeholder="輸入登入帳號" required autocomplete="username">
</div>
</div>
<hr>
<p class="text-muted">如需變更密碼,請輸入新密碼,否則請留空。</p>
<div class="row">
<div class="col-md-6 mb-3">
<label for="newPassword" class="form-label">新密碼 (選填)</label>
<input type="password" class="form-control" id="newPassword" placeholder="輸入新密碼" autocomplete="new-password">
</div>
<div class="col-md-6 mb-3">
<label for="confirmNewPassword" class="form-label">確認新密碼 (選填)</label>
<input type="password" class="form-control" id="confirmNewPassword" placeholder="再次輸入新密碼" autocomplete="new-password">
</div>
</div>
<div class="mb-3">
<label for="notes" class="form-label">備註</label>
<textarea class="form-control" id="notes" rows="3" placeholder="輸入其他備註事項 (選填)"></textarea>
</div>
<button type="submit" class="btn btn-primary">更新管理員帳號</button>
<a href="admin_list.html" class="btn btn-secondary ms-2">返回列表</a>
</form>
</div>
<script>
let currentEditingAdmin = {};
document.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const adminIdFromUrl = parseInt(urlParams.get('id'));
let existingAdmins = JSON.parse(localStorage.getItem('admins_management_data_temp')) || [];
const foundAdmin = existingAdmins.find(admin => admin.id === adminIdFromUrl);
if (foundAdmin) {
currentEditingAdmin = { ...foundAdmin };
} else {
alert("找不到指定的管理員帳號!將載入範例資料。 ID: " + adminIdFromUrl);
// Fallback to example data if admin not found
currentEditingAdmin = {
id: 99901,
creationDate: new Date().toISOString().split('T')[0],
adminName: "測試管理員",
username: "testadmin_edit",
adminCategory: "管理員",
notes: "這是一筆用於編輯頁面測試的管理員備註。",
password: "password123" // Original password placeholder
};
}
document.getElementById('adminId').value = currentEditingAdmin.id;
document.getElementById('creationDate').value = currentEditingAdmin.creationDate;
document.getElementById('adminName').value = currentEditingAdmin.adminName;
document.getElementById('username').value = currentEditingAdmin.username;
document.getElementById('adminCategory').value = currentEditingAdmin.adminCategory;
document.getElementById('notes').value = currentEditingAdmin.notes || "";
});
document.getElementById('editAdminForm').addEventListener('submit', function(event) {
event.preventDefault();
const newPassword = document.getElementById('newPassword').value;
const confirmNewPassword = document.getElementById('confirmNewPassword').value;
const originalUsername = currentEditingAdmin.username;
const newUsername = document.getElementById('username').value;
if (newPassword || confirmNewPassword) { // Only validate if either password field is touched
if (newPassword !== confirmNewPassword) {
alert("新密碼與確認新密碼不相符,請重新輸入或留空以保持原密碼。");
document.getElementById('confirmNewPassword').focus();
return;
}
}
let existingAdmins = JSON.parse(localStorage.getItem('admins_management_data_temp')) || [];
// Check if new username already exists (if username was changed)
if (newUsername !== originalUsername) {
if (existingAdmins.some(admin => admin.username === newUsername && admin.id !== currentEditingAdmin.id)) {
alert("此登入帳號已被其他管理員使用,請選擇其他帳號。");
document.getElementById('username').focus();
return;
}
}
currentEditingAdmin.adminName = document.getElementById('adminName').value;
currentEditingAdmin.username = newUsername;
currentEditingAdmin.adminCategory = document.getElementById('adminCategory').value;
currentEditingAdmin.notes = document.getElementById('notes').value;
if (newPassword) { // If new password is provided and validated
currentEditingAdmin.password = newPassword; // In a real app, hash this
}
// If newPassword is empty, currentEditingAdmin.password retains its original value loaded initially.
console.log("更新後管理員資料:", currentEditingAdmin);
const adminIndex = existingAdmins.findIndex(admin => admin.id === currentEditingAdmin.id);
if (adminIndex > -1) {
existingAdmins[adminIndex] = { ...currentEditingAdmin };
localStorage.setItem('admins_management_data_temp', JSON.stringify(existingAdmins));
alert("管理員帳號已更新!\n管理員姓名" + currentEditingAdmin.adminName);
} else {
// This case should ideally not happen if loaded correctly, but as a fallback:
existingAdmins.push({ ...currentEditingAdmin });
localStorage.setItem('admins_management_data_temp', JSON.stringify(existingAdmins));
alert("管理員帳號 (ID: " + currentEditingAdmin.id + ") 在列表中未找到,已新增至列表!");
}
// Optionally redirect or clear password fields
document.getElementById('newPassword').value = '';
document.getElementById('confirmNewPassword').value = '';
// window.location.href = 'admin_list.html';
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

265
Backstage/admin_list.html Normal file
View File

@ -0,0 +1,265 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>管理員帳號管理</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
}
.container.mt-4 {
flex-grow: 1;
}
</style>
</head>
<body>
<div class="container mt-4">
<h2 class="mb-4">管理員帳號管理</h2> 開發備註:管理員不能新增修改自己以外的資料,管委會委員可以新增管理員與修改自己的資料,最高管理員可以新增修改所有資料
<div class="row g-2 mb-3">
<div class="col-auto"> <a href="admin_add.html" class="btn btn-outline-success rounded-pill px-3">新增管理員</a> </div>
<div class="col-auto"> <button class="btn btn-outline-secondary rounded-pill px-3" onclick="exportAdmins()">匯出列表</button>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-sm-6 col-md-4 col-lg-4">
<input type="text" id="searchAdminName" class="form-control" placeholder="搜尋管理員姓名">
</div>
<div class="col-sm-6 col-md-3 col-lg-3">
<select id="searchAdminCategory" class="form-select">
<option value="">所有類別</option>
<option value="最高管理員">最高管理員</option>
<option value="管委會委員">管委會委員</option>
<option value="管理員">管理員</option>
</select>
</div>
<div class="col-auto">
<button class="btn btn-outline-primary w-100 rounded-pill" onclick="performSearch()">搜尋</button>
</div>
<div class="col-auto"> <button class="btn btn-outline-danger w-100 rounded-pill" onclick="deleteSelected()">刪除勾選</button>
</div>
</div>
<table class="table table-bordered table-hover align-middle">
<thead class="table-dark">
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleAll(this)"></th>
<th>編號</th>
<th>建立日期</th>
<th>帳號</th>
<th>管理員姓名</th>
<th>管理員類別</th>
<th>操作</th>
</tr>
</thead>
<tbody id="adminTable">
</tbody>
</table>
<div class="d-flex justify-content-between align-items-center mt-3">
<div><span id="currentPageInfo">1</span> 頁,共 <span id="totalPagesInfo">1</span></div>
<div>
<button id="prevPageBtn" class="btn btn-sm btn-outline-secondary me-1" onclick="prevPage()">上一頁</button>
<button id="nextPageBtn" class="btn btn-sm btn-outline-secondary" onclick="nextPage()">下一頁</button>
</div>
</div>
</div>
<script>
const pageSize = 5;
let currentPage = 1;
let currentFilteredData = [];
// Sample data for administrators
let admins_data = JSON.parse(localStorage.getItem('admins_management_data_temp')) || [
{ id: 1, creationDate: "2025/01/15", username: "superadmin", adminName: "林大衛", adminCategory: "最高管理員", notes: "系統最高權限" },
{ id: 2, creationDate: "2025/02/10", username: "committee_chang", adminName: "張麗華", adminCategory: "管委會委員", notes: "財務委員" },
{ id: 3, creationDate: "2025/03/05", username: "guard_lee", adminName: "李明", adminCategory: "管理員", notes: "日間管理員" },
{ id: 4, creationDate: "2025/03/20", username: "admin_wang", adminName: "王小虎", adminCategory: "管理員", notes: "夜間管理員" },
{ id: 5, creationDate: "2025/04/01", username: "top_boss", adminName: "陳總經理", adminCategory: "最高管理員", notes: "CEO" },
{ id: 6, creationDate: "2025/04/15", username: "member_kao", adminName: "高美玲", adminCategory: "管委會委員", notes: "活動委員" },
{ id: 7, creationDate: "2025/05/01", username: "security_chen", adminName: "陳保全", adminCategory: "管理員", notes: "大門保全" }
];
if (!localStorage.getItem('admins_management_data_temp') || JSON.parse(localStorage.getItem('admins_management_data_temp')).length === 0) {
localStorage.setItem('admins_management_data_temp', JSON.stringify(admins_data));
}
function renderTable(dataToRender) {
const tbody = document.getElementById("adminTable");
tbody.innerHTML = "";
const start = (currentPage - 1) * pageSize;
const pageData = dataToRender.slice(start, start + pageSize);
pageData.forEach(adm => {
const row = document.createElement("tr");
let categoryBadgeClass = 'bg-secondary'; // Default
switch (adm.adminCategory) {
case '最高管理員':
categoryBadgeClass = 'bg-danger text-white';
break;
case '管委會委員':
categoryBadgeClass = 'bg-warning text-dark';
break;
case '管理員':
categoryBadgeClass = 'bg-info text-dark';
break;
}
row.innerHTML = `
<td><input type="checkbox" class="row-checkbox" data-id="${adm.id}"></td>
<td>${adm.id}</td>
<td>${adm.creationDate}</td>
<td>${adm.username}</td>
<td>${adm.adminName}</td>
<td><span class="badge ${categoryBadgeClass}">${adm.adminCategory}</span></td>
<td>
<a href="admin_edit.html?id=${adm.id}" class="btn btn-outline-secondary btn-sm">編輯</a>
<button class="btn btn-outline-danger btn-sm" onclick="deleteAdmin(${adm.id})">刪除</button>
</td>
`;
tbody.appendChild(row);
});
updatePaginationControls(dataToRender.length);
const selectAllCheckbox = document.getElementById("selectAll");
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
}
function updatePaginationControls(totalItems) {
const totalPages = Math.ceil(totalItems / pageSize) || 1;
const currentPageDisplay = document.getElementById("currentPageInfo");
const totalPagesDisplay = document.getElementById("totalPagesInfo");
const prevButton = document.getElementById("prevPageBtn");
const nextButton = document.getElementById("nextPageBtn");
if (currentPageDisplay) currentPageDisplay.textContent = currentPage;
if (totalPagesDisplay) totalPagesDisplay.textContent = totalPages;
if (prevButton) prevButton.disabled = currentPage === 1;
if (nextButton) nextButton.disabled = currentPage === totalPages || totalItems === 0;
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
renderTable(currentFilteredData);
} else if (totalItems === 0) {
if (currentPageDisplay) currentPageDisplay.textContent = 1;
if (totalPagesDisplay) totalPagesDisplay.textContent = 1;
if (prevButton) prevButton.disabled = true;
if (nextButton) nextButton.disabled = true;
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function nextPage() {
const totalPages = Math.ceil(currentFilteredData.length / pageSize);
if (currentPage < totalPages) {
currentPage++;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function performSearch() {
const adminNameInput = document.getElementById("searchAdminName").value.toLowerCase();
const adminCategoryInput = document.getElementById("searchAdminCategory").value;
admins_data = JSON.parse(localStorage.getItem('admins_management_data_temp')) || [];
currentFilteredData = admins_data.filter(adm =>
adm.adminName.toLowerCase().includes(adminNameInput) &&
(adminCategoryInput === "" || adm.adminCategory === adminCategoryInput)
);
currentPage = 1;
renderTable(currentFilteredData);
}
function toggleAll(source) {
const checkboxes = document.querySelectorAll(".row-checkbox");
checkboxes.forEach(cb => cb.checked = source.checked);
}
function deleteSelected() {
if (!confirm("確定要刪除勾選的管理員帳號嗎?")) return;
const selected = document.querySelectorAll(".row-checkbox:checked");
if (selected.length === 0) {
alert("請先勾選要刪除的管理員帳號");
return;
}
const idsToDelete = Array.from(selected).map(cb => parseInt(cb.dataset.id));
admins_data = admins_data.filter(adm => !idsToDelete.includes(adm.id));
localStorage.setItem('admins_management_data_temp', JSON.stringify(admins_data));
performSearch();
}
function deleteAdmin(id) {
if (!confirm("確定要刪除此管理員帳號嗎?")) return;
admins_data = admins_data.filter(adm => adm.id !== id);
localStorage.setItem('admins_management_data_temp', JSON.stringify(admins_data));
performSearch();
}
function exportAdmins() {
const adminNameInput = document.getElementById("searchAdminName").value.toLowerCase();
const adminCategoryInput = document.getElementById("searchAdminCategory").value;
let dataToExport = JSON.parse(localStorage.getItem('admins_management_data_temp')) || [];
if (adminNameInput || adminCategoryInput) { // If any search field has a value
dataToExport = currentFilteredData; // Export only filtered data
}
if (dataToExport.length === 0) {
alert("沒有資料可匯出。");
return;
}
let csvContent = "data:text/csv;charset=utf-8,\uFEFF"; // \uFEFF for UTF-8 BOM
csvContent += "編號,建立日期,帳號,管理員姓名,管理員類別,備註\n";
dataToExport.forEach(adm => {
const cleanAdminName = `"${(adm.adminName || '').replace(/"/g, '""')}"`;
const cleanUsername = `"${(adm.username || '').replace(/"/g, '""')}"`;
const cleanAdminCategory = `"${(adm.adminCategory || '').replace(/"/g, '""')}"`;
const cleanNotes = `"${adm.notes ? adm.notes.replace(/"/g, '""') : ''}"`;
const row = [adm.id, adm.creationDate, cleanUsername, cleanAdminName, cleanAdminCategory, cleanNotes];
csvContent += row.join(",") + "\n";
});
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "admins_list_export.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
alert(`準備匯出 ${dataToExport.length} 筆管理員資料(${adminNameInput || adminCategoryInput ? "依搜尋條件" : "全部"}`);
}
document.addEventListener('DOMContentLoaded', () => {
performSearch(); // Initial load of data
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,194 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>新增公告</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* CSS for suggestions box */
.suggestions-container {
position: relative;
}
.suggestions-box {
position: absolute;
border: 1px solid #ced4da;
border-top: none;
max-height: 200px;
overflow-y: auto;
background-color: white;
width: 100%;
z-index: 1000;
border-radius: 0 0 0.25rem 0.25rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.suggestion-item {
padding: 0.5rem 0.75rem;
cursor: pointer;
}
.suggestion-item:hover, .suggestion-item.active {
background-color: #e9ecef;
}
.suggestions-box:empty {
display: none;
}
</style>
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">新增公告</h2>
<form id="addAnnouncementForm">
<div class="mb-3">
<label for="publicationDate" class="form-label">發布日期</label>
<input type="date" class="form-control" id="publicationDate" required>
</div>
<div class="mb-3 suggestions-container">
<label for="announcementTitle" class="form-label">公告標題</label>
<input type="text" class="form-control" id="announcementTitle" placeholder="輸入公告標題或關鍵字搜尋" required autocomplete="off">
<div id="titleSuggestionsBox" class="suggestions-box"></div>
</div>
<div class="mb-3">
<label for="announcementContent" class="form-label">公告內容</label>
<textarea class="form-control" id="announcementContent" rows="5" placeholder="輸入詳細公告內容" required></textarea>
</div>
<button type="submit" class="btn btn-primary">儲存公告</button>
<a href="announcement_list.html" class="btn btn-secondary ms-2">返回列表</a>
</form>
</div>
<script>
// Predefined common announcement titles/keywords
const commonAnnouncementTitles = [
"水塔清洗通知", "停電通知", "消防演練公告", "社區會議通知",
"電梯保養公告", "包裹領取通知", "住戶規約修訂公告",
"節慶活動通知 (如:中秋節、端午節)", "公共區域消毒公告",
"颱風防災準備通知", "停車場清潔通知", "管委會選舉公告",
"財務報表公告", "社區修繕工程通知", "重要事項宣達", "春節假期服務調整"
];
const announcementTitleInput = document.getElementById('announcementTitle');
const titleSuggestionsBox = document.getElementById('titleSuggestionsBox');
let activeTitleSuggestionIndex = -1;
document.addEventListener('DOMContentLoaded', (event) => {
const today = new Date().toISOString().split('T')[0];
const publicationDateInput = document.getElementById('publicationDate');
if(publicationDateInput) {
publicationDateInput.value = today;
}
});
announcementTitleInput.addEventListener('input', function() {
const inputText = this.value.toLowerCase();
titleSuggestionsBox.innerHTML = '';
activeTitleSuggestionIndex = -1;
if (inputText.length === 0) {
titleSuggestionsBox.style.display = 'none';
return;
}
const filteredTitles = commonAnnouncementTitles.filter(title =>
title.toLowerCase().includes(inputText)
);
if (filteredTitles.length > 0) {
filteredTitles.forEach((title, index) => {
const item = document.createElement('div');
item.classList.add('suggestion-item');
item.textContent = title;
item.addEventListener('click', function() {
announcementTitleInput.value = title;
titleSuggestionsBox.innerHTML = '';
titleSuggestionsBox.style.display = 'none';
});
item.setAttribute('data-index', index);
titleSuggestionsBox.appendChild(item);
});
titleSuggestionsBox.style.display = 'block';
} else {
titleSuggestionsBox.style.display = 'none';
}
});
announcementTitleInput.addEventListener('keydown', function(e) {
const items = titleSuggestionsBox.querySelectorAll('.suggestion-item');
if (items.length === 0 || titleSuggestionsBox.style.display === 'none') return;
if (e.key === 'ArrowDown') {
e.preventDefault();
activeTitleSuggestionIndex = (activeTitleSuggestionIndex + 1) % items.length;
updateActiveTitleSuggestion(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
activeTitleSuggestionIndex = (activeTitleSuggestionIndex - 1 + items.length) % items.length;
updateActiveTitleSuggestion(items);
} else if (e.key === 'Enter') {
e.preventDefault();
if (activeTitleSuggestionIndex > -1 && items[activeTitleSuggestionIndex]) {
items[activeTitleSuggestionIndex].click();
} else if (items.length > 0) { // If no suggestion is actively selected, pick the first one or just close
// items[0].click(); // Optionally select the first one
titleSuggestionsBox.innerHTML = '';
titleSuggestionsBox.style.display = 'none';
}
} else if (e.key === 'Escape') {
titleSuggestionsBox.innerHTML = '';
titleSuggestionsBox.style.display = 'none';
activeTitleSuggestionIndex = -1;
}
});
function updateActiveTitleSuggestion(items) {
items.forEach(item => item.classList.remove('active'));
if (items[activeTitleSuggestionIndex]) {
items[activeTitleSuggestionIndex].classList.add('active');
items[activeTitleSuggestionIndex].scrollIntoView({ block: 'nearest' });
}
}
// Global click listener to hide suggestions when clicking outside
document.addEventListener('click', function(event) {
if (!announcementTitleInput.contains(event.target) && !titleSuggestionsBox.contains(event.target)) {
titleSuggestionsBox.innerHTML = '';
titleSuggestionsBox.style.display = 'none';
activeTitleSuggestionIndex = -1;
}
});
document.getElementById('addAnnouncementForm').addEventListener('submit', function(event) {
event.preventDefault();
const newAnnouncement = {
id: Date.now(),
publicationDate: document.getElementById('publicationDate').value,
title: announcementTitleInput.value.trim(), // Use value from the input
content: document.getElementById('announcementContent').value.trim()
};
if (!newAnnouncement.title || !newAnnouncement.content) {
alert("公告標題和內容為必填項目。");
return;
}
console.log("新公告資料:", newAnnouncement);
let existingAnnouncements = JSON.parse(localStorage.getItem('announcements_data_temp')) || [];
existingAnnouncements.push(newAnnouncement);
existingAnnouncements.sort((a, b) => {
if (b.publicationDate < a.publicationDate) return -1;
if (b.publicationDate > a.publicationDate) return 1;
return b.id - a.id;
});
localStorage.setItem('announcements_data_temp', JSON.stringify(existingAnnouncements));
alert("公告「" + newAnnouncement.title + "」已儲存!");
window.location.href = 'announcement_list.html';
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,219 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>編輯公告</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* CSS for suggestions box */
.suggestions-container {
position: relative;
}
.suggestions-box {
position: absolute;
border: 1px solid #ced4da;
border-top: none;
max-height: 200px;
overflow-y: auto;
background-color: white;
width: 100%;
z-index: 1000;
border-radius: 0 0 0.25rem 0.25rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.suggestion-item {
padding: 0.5rem 0.75rem;
cursor: pointer;
}
.suggestion-item:hover, .suggestion-item.active {
background-color: #e9ecef;
}
.suggestions-box:empty {
display: none;
}
</style>
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">編輯公告</h2>
<form id="editAnnouncementForm">
<input type="hidden" id="announcementId" name="announcementId">
<div class="mb-3">
<label for="publicationDate" class="form-label">發布日期</label>
<input type="date" class="form-control" id="publicationDate" required>
</div>
<div class="mb-3 suggestions-container">
<label for="announcementTitle" class="form-label">公告標題</label>
<input type="text" class="form-control" id="announcementTitle" placeholder="輸入公告標題或關鍵字搜尋" required autocomplete="off">
<div id="titleSuggestionsBox" class="suggestions-box"></div>
</div>
<div class="mb-3">
<label for="announcementContent" class="form-label">公告內容</label>
<textarea class="form-control" id="announcementContent" rows="5" placeholder="輸入詳細公告內容" required></textarea>
</div>
<button type="submit" class="btn btn-primary">更新公告</button>
<a href="announcement_list.html" class="btn btn-secondary ms-2">返回列表</a>
</form>
</div>
<script>
// Predefined common announcement titles/keywords
const commonAnnouncementTitles = [
"水塔清洗通知", "停電通知", "消防演練公告", "社區會議通知",
"電梯保養公告", "包裹領取通知", "住戶規約修訂公告",
"節慶活動通知 (如:中秋節、端午節)", "公共區域消毒公告",
"颱風防災準備通知", "停車場清潔通知", "管委會選舉公告",
"財務報表公告", "社區修繕工程通知", "重要事項宣達", "春節假期服務調整"
];
const announcementTitleInput = document.getElementById('announcementTitle');
const titleSuggestionsBox = document.getElementById('titleSuggestionsBox');
let activeTitleSuggestionIndex = -1;
let currentEditingAnnouncement = {};
document.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const announcementIdFromUrl = parseInt(urlParams.get('id'));
let existingAnnouncements = JSON.parse(localStorage.getItem('announcements_data_temp')) || [];
const foundAnnouncement = existingAnnouncements.find(a => a.id === announcementIdFromUrl);
if (foundAnnouncement) {
currentEditingAnnouncement = { ...foundAnnouncement };
} else {
alert("找不到指定的公告資料!將返回列表。");
window.location.href = 'announcement_list.html';
return;
}
document.getElementById('announcementId').value = currentEditingAnnouncement.id;
document.getElementById('publicationDate').value = currentEditingAnnouncement.publicationDate;
announcementTitleInput.value = currentEditingAnnouncement.title; // Set initial value for title input
document.getElementById('announcementContent').value = currentEditingAnnouncement.content;
});
announcementTitleInput.addEventListener('input', function() {
const inputText = this.value.toLowerCase();
titleSuggestionsBox.innerHTML = '';
activeTitleSuggestionIndex = -1;
if (inputText.length === 0) {
titleSuggestionsBox.style.display = 'none';
return;
}
const filteredTitles = commonAnnouncementTitles.filter(title =>
title.toLowerCase().includes(inputText)
);
if (filteredTitles.length > 0) {
filteredTitles.forEach((title, index) => {
const item = document.createElement('div');
item.classList.add('suggestion-item');
item.textContent = title;
item.addEventListener('click', function() {
announcementTitleInput.value = title;
titleSuggestionsBox.innerHTML = '';
titleSuggestionsBox.style.display = 'none';
});
item.setAttribute('data-index', index);
titleSuggestionsBox.appendChild(item);
});
titleSuggestionsBox.style.display = 'block';
} else {
titleSuggestionsBox.style.display = 'none';
}
});
announcementTitleInput.addEventListener('keydown', function(e) {
const items = titleSuggestionsBox.querySelectorAll('.suggestion-item');
if (items.length === 0 || titleSuggestionsBox.style.display === 'none') return;
if (e.key === 'ArrowDown') {
e.preventDefault();
activeTitleSuggestionIndex = (activeTitleSuggestionIndex + 1) % items.length;
updateActiveTitleSuggestion(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
activeTitleSuggestionIndex = (activeTitleSuggestionIndex - 1 + items.length) % items.length;
updateActiveTitleSuggestion(items);
} else if (e.key === 'Enter') {
e.preventDefault();
if (activeTitleSuggestionIndex > -1 && items[activeTitleSuggestionIndex]) {
items[activeTitleSuggestionIndex].click();
} else if (items.length > 0) {
titleSuggestionsBox.innerHTML = '';
titleSuggestionsBox.style.display = 'none';
}
} else if (e.key === 'Escape') {
titleSuggestionsBox.innerHTML = '';
titleSuggestionsBox.style.display = 'none';
activeTitleSuggestionIndex = -1;
}
});
function updateActiveTitleSuggestion(items) {
items.forEach(item => item.classList.remove('active'));
if (items[activeTitleSuggestionIndex]) {
items[activeTitleSuggestionIndex].classList.add('active');
items[activeTitleSuggestionIndex].scrollIntoView({ block: 'nearest' });
}
}
// Global click listener to hide suggestions when clicking outside
document.addEventListener('click', function(event) {
if (!announcementTitleInput.contains(event.target) && !titleSuggestionsBox.contains(event.target)) {
titleSuggestionsBox.innerHTML = '';
titleSuggestionsBox.style.display = 'none';
activeTitleSuggestionIndex = -1;
}
});
document.getElementById('editAnnouncementForm').addEventListener('submit', function(event) {
event.preventDefault();
currentEditingAnnouncement.publicationDate = document.getElementById('publicationDate').value;
currentEditingAnnouncement.title = announcementTitleInput.value.trim(); // Use value from the input
currentEditingAnnouncement.content = document.getElementById('announcementContent').value.trim();
if (!currentEditingAnnouncement.title || !currentEditingAnnouncement.content) {
alert("公告標題和內容為必填項目。");
return;
}
console.log("更新後公告資料:", currentEditingAnnouncement);
let existingAnnouncements = JSON.parse(localStorage.getItem('announcements_data_temp')) || [];
const announcementIndex = existingAnnouncements.findIndex(a => a.id === currentEditingAnnouncement.id);
if (announcementIndex > -1) {
existingAnnouncements[announcementIndex] = { ...currentEditingAnnouncement };
existingAnnouncements.sort((a, b) => {
if (b.publicationDate < a.publicationDate) return -1;
if (b.publicationDate > a.publicationDate) return 1;
return b.id - a.id;
});
localStorage.setItem('announcements_data_temp', JSON.stringify(existingAnnouncements));
alert("公告「" + currentEditingAnnouncement.title + "」資料已更新!");
} else {
existingAnnouncements.push({ ...currentEditingAnnouncement });
existingAnnouncements.sort((a, b) => {
if (b.publicationDate < a.publicationDate) return -1;
if (b.publicationDate > a.publicationDate) return 1;
return b.id - a.id;
});
localStorage.setItem('announcements_data_temp', JSON.stringify(existingAnnouncements));
alert("公告 (ID: " + currentEditingAnnouncement.id + ") 在列表中未找到,已模擬新增!");
}
window.location.href = 'announcement_list.html';
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,271 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>公告管理</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
}
.container.mt-4 {
flex-grow: 1;
}
.content-snippet {
max-width: 300px; /* Adjust as needed */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
</head>
<body>
<div class="container mt-4">
<h2 class="mb-4">公告管理</h2>
<div class="row g-2 mb-3">
<div class="col-auto"> <a href="announcement_add.html" class="btn btn-outline-success rounded-pill px-3">新增公告</a> </div>
<div class="col-auto"> <button class="btn btn-outline-secondary rounded-pill px-3" onclick="exportAnnouncements()">匯出列表</button>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-sm-6 col-md-4 col-lg-4">
<input type="text" id="searchTitle" class="form-control" placeholder="搜尋公告標題">
</div>
<div class="col-sm-6 col-md-4 col-lg-4">
<input type="date" id="searchDate" class="form-control">
</div>
<div class="col-6 col-md-2 col-lg-2">
<button class="btn btn-outline-primary w-100 rounded-pill" onclick="performSearch()">搜尋</button>
</div>
<div class="col-6 col-md-2 col-lg-2"> <button class="btn btn-outline-danger w-100 rounded-pill" onclick="deleteSelected()">刪除勾選</button>
</div>
</div>
<table class="table table-bordered table-hover align-middle">
<thead class="table-dark">
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleAll(this)"></th>
<th>ID</th>
<th>發布日期</th>
<th>公告標題</th>
<th>內容摘要</th>
<th>操作</th>
</tr>
</thead>
<tbody id="announcementTable">
</tbody>
</table>
<div class="d-flex justify-content-between align-items-center mt-3">
<div><span id="currentPageInfo">1</span> 頁,共 <span id="totalPagesInfo">1</span></div>
<div>
<button id="prevPageBtn" class="btn btn-sm btn-outline-secondary me-1" onclick="prevPage()">上一頁</button>
<button id="nextPageBtn" class="btn btn-sm btn-outline-secondary" onclick="nextPage()">下一頁</button>
</div>
</div>
</div>
<script>
const pageSize = 5; // Number of items per page
let currentPage = 1;
let currentFilteredData = [];
let announcements_data = []; // Will be loaded from localStorage
// Sample announcement data if localStorage is empty
const sampleAnnouncements = [
{ id: 1, publicationDate: "2025-04-28", title: "水塔清洗通知", content: "本社將於 2025/05/05 進行水塔清洗作業,請住戶提前儲水,清洗期間將暫停供水,造成不便敬請見諒。" },
{ id: 2, publicationDate: "2025-04-25", title: "停電通知", content: "台電預計於 5/1 上午進行電路維修,期間將暫時停電,請提前準備。" },
{ id: 3, publicationDate: "2025-04-20", title: "消防演練公告", content: "消防演練將於 5/10 下午舉行,請住戶配合參與並聆聽安全說明。" },
{ id: 4, publicationDate: "2025-05-10", title: "社區會議通知", content: "茲訂於2025年5月15日晚間7時30分於社區活動中心召開區分所有權人會議請各位住戶撥冗出席。" },
{ id: 5, publicationDate: "2025-05-12", title: "電梯定期保養公告", content: "A棟及B棟電梯將於2025年5月20日上午9時至下午5時進行定期保養期間電梯將暫停使用。" },
{ id: 6, publicationDate: "2025-05-15", title: "端午節活動通知", content: "為慶祝端午佳節管委會將於6月3日舉辦包粽子活動歡迎住戶踴躍報名參加。" }
];
function formatDateForDisplay(dateString) {
if (!dateString) return '';
const [year, month, day] = dateString.split('-');
return `${year}/${month}/${day}`;
}
function renderTable(dataToRender) {
const tbody = document.getElementById("announcementTable");
tbody.innerHTML = "";
const start = (currentPage - 1) * pageSize;
const pageData = dataToRender.slice(start, start + pageSize);
pageData.forEach(announcement => {
const row = document.createElement("tr");
const contentSnippet = announcement.content.length > 30 ? announcement.content.substring(0, 27) + "..." : announcement.content;
row.innerHTML = `
<td><input type="checkbox" class="row-checkbox" data-id="${announcement.id}"></td>
<td>${announcement.id}</td>
<td>${formatDateForDisplay(announcement.publicationDate)}</td>
<td>${announcement.title}</td>
<td><div class="content-snippet" title="${announcement.content}">${contentSnippet}</div></td>
<td>
<a href="announcement_edit.html?id=${announcement.id}" class="btn btn-outline-secondary btn-sm">編輯</a>
<button class="btn btn-outline-danger btn-sm" onclick="deleteAnnouncement(${announcement.id})">刪除</button>
</td>
`;
tbody.appendChild(row);
});
updatePaginationControls(dataToRender.length);
const selectAllCheckbox = document.getElementById("selectAll");
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
}
function updatePaginationControls(totalItems) {
const totalPages = Math.ceil(totalItems / pageSize) || 1;
const currentPageDisplay = document.getElementById("currentPageInfo");
const totalPagesDisplay = document.getElementById("totalPagesInfo");
const prevButton = document.getElementById("prevPageBtn");
const nextButton = document.getElementById("nextPageBtn");
if (currentPageDisplay) currentPageDisplay.textContent = currentPage;
if (totalPagesDisplay) totalPagesDisplay.textContent = totalPages;
if (prevButton) prevButton.disabled = currentPage === 1;
if (nextButton) nextButton.disabled = currentPage === totalPages || totalItems === 0;
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
renderTable(currentFilteredData);
} else if (totalItems === 0) {
if (currentPageDisplay) currentPageDisplay.textContent = 1;
if (totalPagesDisplay) totalPagesDisplay.textContent = 1;
if (prevButton) prevButton.disabled = true;
if (nextButton) nextButton.disabled = true;
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function nextPage() {
const totalPages = Math.ceil(currentFilteredData.length / pageSize);
if (currentPage < totalPages) {
currentPage++;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function performSearch() {
const titleInput = document.getElementById("searchTitle").value.toLowerCase();
const dateInput = document.getElementById("searchDate").value; // YYYY-MM-DD
const storedAnnouncements = JSON.parse(localStorage.getItem('announcements_data_temp'));
if (storedAnnouncements && storedAnnouncements.length > 0) {
announcements_data = storedAnnouncements;
} else {
announcements_data = [...sampleAnnouncements]; // Use sample if localStorage is empty
localStorage.setItem('announcements_data_temp', JSON.stringify(announcements_data));
}
currentFilteredData = announcements_data.filter(announcement =>
announcement.title.toLowerCase().includes(titleInput) &&
(dateInput === "" || announcement.publicationDate === dateInput)
);
currentPage = 1;
renderTable(currentFilteredData);
}
function toggleAll(source) {
const checkboxes = document.querySelectorAll(".row-checkbox");
checkboxes.forEach(cb => cb.checked = source.checked);
}
function deleteSelected() {
if (!confirm("確定要刪除勾選的公告嗎?")) return;
const selected = document.querySelectorAll(".row-checkbox:checked");
if (selected.length === 0) {
alert("請先勾選要刪除的公告");
return;
}
const idsToDelete = Array.from(selected).map(cb => parseInt(cb.dataset.id));
announcements_data = announcements_data.filter(announcement => !idsToDelete.includes(announcement.id));
localStorage.setItem('announcements_data_temp', JSON.stringify(announcements_data));
performSearch(); // Re-filter and re-render
}
function deleteAnnouncement(id) {
if (!confirm("確定要刪除此筆公告嗎?")) return;
announcements_data = announcements_data.filter(announcement => announcement.id !== id);
localStorage.setItem('announcements_data_temp', JSON.stringify(announcements_data));
// Adjust current page if it becomes empty after deletion
const totalPages = Math.ceil(announcements_data.length / pageSize) || 1;
if (currentPage > totalPages) {
currentPage = totalPages;
}
if (currentFilteredData.length === 1 && currentFilteredData[0].id === id && currentPage > 1) {
// If the deleted item was the only one on the current page (and not the first page)
const currentSearchTitle = document.getElementById("searchTitle").value.toLowerCase();
const currentSearchDate = document.getElementById("searchDate").value;
const stillMatching = announcements_data.filter(a => a.title.toLowerCase().includes(currentSearchTitle) && (currentSearchDate === "" || a.publicationDate === currentSearchDate));
if (Math.ceil(stillMatching.length / pageSize) < currentPage) {
currentPage--;
}
}
performSearch(); // Re-filter and re-render
}
function exportAnnouncements() {
const titleInput = document.getElementById("searchTitle").value.toLowerCase();
const dateInput = document.getElementById("searchDate").value;
let dataToExport = announcements_data;
if (titleInput || dateInput) { // Export filtered if search is active
dataToExport = currentFilteredData;
}
if (dataToExport.length === 0) {
alert("沒有資料可匯出。");
return;
}
let csvContent = "data:text/csv;charset=utf-8,\uFEFF"; // \uFEFF for BOM
csvContent += "ID,發布日期,公告標題,公告內容\n";
dataToExport.forEach(ann => {
const cleanTitle = `"${ann.title.replace(/"/g, '""')}"`;
const cleanContent = `"${ann.content.replace(/"/g, '""')}"`;
const row = [ann.id, formatDateForDisplay(ann.publicationDate), cleanTitle, cleanContent];
csvContent += row.join(",") + "\n";
});
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "announcements_export.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
alert(`準備匯出 ${dataToExport.length} 筆公告資料(${titleInput || dateInput ? "依搜尋條件" : "全部"}`);
}
document.addEventListener('DOMContentLoaded', () => {
performSearch(); // Load data and render table
document.getElementById("searchDate").value = ""; // Ensure date is clear on load
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>新增公告標題提示</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">新增公告標題提示</h2>
<form id="createTitleForm">
<div class="mb-3">
<label for="titleName" class="form-label">標題內容</label>
<input type="text" class="form-control" id="titleName" placeholder="輸入公告標題提示文字" required>
</div>
<button type="submit" class="btn btn-success">新增標題提示</button>
<a href="announcement_title_list.html" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
<script>
const localStorageKey = 'announcement_titles_data'; // Key changed
document.getElementById("createTitleForm").addEventListener("submit", function (e) {
e.preventDefault();
const titleNameValue = document.getElementById("titleName").value.trim();
if (!titleNameValue) {
alert("標題內容不能為空!");
return;
}
let announcementTitles = JSON.parse(localStorage.getItem(localStorageKey)) || []; // Variable name changed
if (announcementTitles.some(title => title.name.toLowerCase() === titleNameValue.toLowerCase())) { // Use new variable name
alert("此標題內容已存在!");
return;
}
const newTitle = {
id: announcementTitles.length > 0 ? Math.max(...announcementTitles.map(t => t.id)) + 1 : 1, // Use new variable name
name: titleNameValue
};
announcementTitles.push(newTitle); // Use new variable name
announcementTitles.sort((a, b) => a.name.localeCompare(b.name, 'zh-Hant'));
localStorage.setItem(localStorageKey, JSON.stringify(announcementTitles)); // Use new variable name
console.log("新增的公告標題提示:", newTitle); // Text changed
alert("公告標題提示「" + newTitle.name + "」已新增!"); // Text changed
window.location.href = "announcement_title_list.html";
});
</script>
</body>
</html>

View File

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>編輯公告標題提示</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">編輯公告標題提示</h2>
<form id="editTitleForm">
<input type="hidden" id="titleId">
<div class="mb-3">
<label for="titleName" class="form-label">標題內容</label>
<input type="text" class="form-control" id="titleName" required>
</div>
<button type="submit" class="btn btn-primary">儲存變更</button>
<a href="announcement_title_list.html" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
<script>
const localStorageKey = 'announcement_titles_data'; // Key changed
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
const results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
document.addEventListener('DOMContentLoaded', () => {
const titleId = getUrlParameter('id');
if (!titleId) {
alert("未指定標題ID");
window.location.href = "announcement_title_list.html";
return;
}
let announcementTitles = JSON.parse(localStorage.getItem(localStorageKey)) || []; // Variable name changed
const titleToEdit = announcementTitles.find(item => item.id === parseInt(titleId)); // Use new variable name
if (titleToEdit) {
document.getElementById("titleId").value = titleToEdit.id;
document.getElementById("titleName").value = titleToEdit.name;
} else {
alert("找不到要編輯的標題提示ID: " + titleId); // Text changed
window.location.href = "announcement_title_list.html";
}
});
document.getElementById("editTitleForm").addEventListener("submit", function (e) {
e.preventDefault();
const titleId = parseInt(document.getElementById("titleId").value);
const updatedName = document.getElementById("titleName").value.trim();
if (!updatedName) {
alert("標題內容不能為空!");
return;
}
let announcementTitles = JSON.parse(localStorage.getItem(localStorageKey)) || []; // Variable name changed
if (announcementTitles.some(title => title.id !== titleId && title.name.toLowerCase() === updatedName.toLowerCase())) { // Use new variable name
alert("此標題內容已存在!");
return;
}
const titleIndex = announcementTitles.findIndex(item => item.id === titleId); // Use new variable name
if (titleIndex > -1) {
announcementTitles[titleIndex].name = updatedName; // Use new variable name
announcementTitles.sort((a, b) => a.name.localeCompare(b.name, 'zh-Hant'));
localStorage.setItem(localStorageKey, JSON.stringify(announcementTitles)); // Use new variable name
alert("公告標題提示已更新!"); // Text changed
} else {
alert("更新失敗,找不到原始標題資料!");
}
window.location.href = "announcement_title_list.html";
});
</script>
</body>
</html>

View File

@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>公告標題提示管理</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
}
.container.mt-4 {
flex-grow: 1;
}
</style>
</head>
<body>
<div class="container mt-4">
<h2 class="mb-4">公告標題提示管理 <small class="text-muted fs-6">(用於新增/編輯公告時的標題提示)</small></h2>
<div class="row g-2 mb-3">
<div class="col-auto">
<a href="announcement_title_add.html" class="btn btn-outline-success rounded-pill">新增標題提示</a>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-sm-8 col-md-5 col-lg-4">
<input type="text" id="searchTitleContent" class="form-control" placeholder="搜尋標題內容">
</div>
<div class="col-auto">
<button class="btn btn-outline-primary w-100 rounded-pill" onclick="performSearch()">搜尋</button>
</div>
<div class="col-auto">
<button class="btn btn-outline-danger w-100 rounded-pill" onclick="deleteSelected()">刪除勾選</button>
</div>
</div>
<table class="table table-bordered table-hover align-middle">
<thead class="table-dark">
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleAll(this)"></th>
<th>標題內容</th>
<th>操作</th>
</tr>
</thead>
<tbody id="titlesTable"></tbody>
</table>
<div class="d-flex justify-content-between align-items-center mt-3">
<div><span id="currentPageInfo">1</span> 頁,共 <span id="totalPagesInfo">1</span></div>
<div>
<button id="prevPageBtn" class="btn btn-sm btn-outline-secondary me-1" onclick="prevPage()">上一頁</button>
<button id="nextPageBtn" class="btn btn-sm btn-outline-secondary" onclick="nextPage()">下一頁</button>
</div>
</div>
</div>
<script>
const pageSize = 5;
let currentPage = 1;
let currentFilteredData = [];
const localStorageKey = 'announcement_titles_data'; // Key changed
// Sample data for announcement titles
let announcementTitles = [ // Variable name changed
{ id: 1, name: "水塔清洗通知" },
{ id: 2, name: "停電通知" },
{ id: 3, name: "消防演練公告" },
{ id: 4, name: "社區會議通知" },
{ id: 5, name: "電梯保養公告" },
{ id: 6, name: "包裹領取通知" },
{ id: 7, name: "住戶規約修訂公告" },
{ id: 8, name: "節慶活動通知 (如:中秋節、端午節)" },
{ id: 9, name: "公共區域消毒公告" },
{ id: 10, name: "颱風防災準備通知" }
// Add more if needed or manage completely through add/edit
];
function loadData() {
const storedData = localStorage.getItem(localStorageKey);
if (storedData) {
announcementTitles = JSON.parse(storedData); // Use new variable name
} else {
localStorage.setItem(localStorageKey, JSON.stringify(announcementTitles));
}
announcementTitles.forEach((item, index) => {
if (!item.id) item.id = Date.now() + index;
});
announcementTitles.sort((a, b) => a.name.localeCompare(b.name, 'zh-Hant'));
}
function saveData() {
localStorage.setItem(localStorageKey, JSON.stringify(announcementTitles)); // Use new variable name
}
function renderTable(dataToRender) {
const tbody = document.getElementById("titlesTable");
tbody.innerHTML = "";
const start = (currentPage - 1) * pageSize;
const pageData = dataToRender.slice(start, start + pageSize);
pageData.forEach(item => {
const row = document.createElement("tr");
row.innerHTML = `
<td><input type="checkbox" class="row-checkbox" data-id="${item.id}"></td>
<td>${item.name}</td>
<td>
<a href="announcement_title_edit.html?id=${item.id}" class="btn btn-outline-secondary btn-sm">編輯</a>
<button class="btn btn-outline-danger btn-sm" onclick="deleteTitle(${item.id})">刪除</button>
</td>
`;
tbody.appendChild(row);
});
updatePaginationControls(dataToRender.length);
const selectAllCheckbox = document.getElementById("selectAll");
if (selectAllCheckbox) selectAllCheckbox.checked = false;
}
function updatePaginationControls(totalItems) {
const totalPages = Math.ceil(totalItems / pageSize) || 1;
const currentPageDisplay = document.getElementById("currentPageInfo");
const totalPagesDisplay = document.getElementById("totalPagesInfo");
const prevButton = document.getElementById("prevPageBtn");
const nextButton = document.getElementById("nextPageBtn");
if (currentPageDisplay) currentPageDisplay.textContent = currentPage;
if (totalPagesDisplay) totalPagesDisplay.textContent = totalPages;
if (prevButton) prevButton.disabled = currentPage === 1;
if (nextButton) nextButton.disabled = currentPage === totalPages || totalItems === 0;
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
renderTable(currentFilteredData);
} else if (totalItems === 0 && currentPage !== 1) {
currentPage = 1;
renderTable(currentFilteredData);
} else if (totalItems === 0) {
if (currentPageDisplay) currentPageDisplay.textContent = 1;
if (totalPagesDisplay) totalPagesDisplay.textContent = 1;
if (prevButton) prevButton.disabled = true;
if (nextButton) nextButton.disabled = true;
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function nextPage() {
const totalPages = Math.ceil(currentFilteredData.length / pageSize);
if (currentPage < totalPages) {
currentPage++;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function performSearch() {
const contentInput = document.getElementById("searchTitleContent").value.toLowerCase();
currentFilteredData = announcementTitles.filter(item => // Use new variable name
item.name.toLowerCase().includes(contentInput)
);
currentPage = 1;
renderTable(currentFilteredData);
}
function toggleAll(source) {
const checkboxes = document.querySelectorAll(".row-checkbox");
checkboxes.forEach(cb => cb.checked = source.checked);
}
function deleteSelected() {
const selectedCheckboxes = document.querySelectorAll(".row-checkbox:checked");
if (selectedCheckboxes.length === 0) {
alert("請先勾選要刪除的標題提示"); // Text changed
return;
}
if (!confirm(`確定要刪除勾選的 ${selectedCheckboxes.length} 個標題提示嗎?`)) return; // Text changed
const idsToDelete = Array.from(selectedCheckboxes).map(cb => parseInt(cb.dataset.id));
announcementTitles = announcementTitles.filter(item => !idsToDelete.includes(item.id)); // Use new variable name
saveData();
const contentInput = document.getElementById("searchTitleContent").value.toLowerCase();
currentFilteredData = announcementTitles.filter(item => // Use new variable name
item.name.toLowerCase().includes(contentInput)
);
const totalPages = Math.ceil(currentFilteredData.length / pageSize) || 1;
if (currentPage > totalPages) {
currentPage = totalPages;
}
if (currentFilteredData.length === 0) {
currentPage = 1;
}
renderTable(currentFilteredData);
if (document.getElementById("selectAll")) document.getElementById("selectAll").checked = false;
}
function deleteTitle(id) {
if (!confirm("確定要刪除此筆標題提示嗎?")) return; // Text changed
announcementTitles = announcementTitles.filter(item => item.id !== id); // Use new variable name
saveData();
const contentInput = document.getElementById("searchTitleContent").value.toLowerCase();
currentFilteredData = announcementTitles.filter(item => // Use new variable name
item.name.toLowerCase().includes(contentInput)
);
const totalItemsOnPage = currentFilteredData.slice((currentPage - 1) * pageSize, currentPage * pageSize).length;
if (totalItemsOnPage === 0 && currentPage > 1) {
currentPage--;
}
const totalPages = Math.ceil(currentFilteredData.length / pageSize) || 1;
if (currentPage > totalPages) {
currentPage = totalPages;
}
if (currentFilteredData.length === 0) {
currentPage = 1;
}
renderTable(currentFilteredData);
}
document.addEventListener('DOMContentLoaded', () => {
loadData();
performSearch();
});
</script>
</body>
</html>

View File

@ -0,0 +1,185 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>編輯社區資料 (測試模式)</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* Basic styling, can be expanded if needed */
</style>
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">編輯社區資料 (測試模式)</h2>
<form id="editCommunityForm">
<input type="hidden" id="communityId" name="communityId">
<div class="mb-3">
<label for="communityName" class="form-label">社區名稱</label>
<input type="text" class="form-control" id="communityName" placeholder="輸入社區完整名稱" required>
</div>
<div class="mb-3">
<label for="address" class="form-label">地址</label>
<input type="text" class="form-control" id="address" placeholder="輸入社區地址" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="city" class="form-label">城市</label>
<input type="text" class="form-control" id="city" placeholder="例如:臺中市">
</div>
<div class="col-md-6 mb-3">
<label for="district" class="form-label">區域</label>
<input type="text" class="form-control" id="district" placeholder="例如:梧棲區">
</div>
</div>
<div class="mb-3">
<label for="zipCode" class="form-label">郵遞區號</label>
<input type="text" class="form-control" id="zipCode" placeholder="輸入郵遞區號">
</div>
<div class="mb-3">
<label for="mainContactPerson" class="form-label">主要聯絡人</label>
<input type="text" class="form-control" id="mainContactPerson" placeholder="輸入主要聯絡人姓名">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="contactPhone" class="form-label">聯絡電話</label>
<input type="tel" class="form-control" id="contactPhone" placeholder="輸入聯絡電話">
</div>
<div class="col-md-6 mb-3">
<label for="contactEmail" class="form-label">電子郵件</label>
<input type="email" class="form-control" id="contactEmail" placeholder="輸入電子郵件">
</div>
</div>
<div class="mb-3">
<label for="managementCompany" class="form-label">管理公司</label>
<input type="text" class="form-control" id="managementCompany" placeholder="輸入管理公司名稱 (若有)">
</div>
<div class="mb-3">
<label for="numberOfUnits" class="form-label">戶數</label>
<input type="number" class="form-control" id="numberOfUnits" placeholder="輸入社區總戶數" min="0">
</div>
<div class="mb-3">
<label for="notes" class="form-label">備註</label>
<textarea class="form-control" id="notes" rows="3" placeholder="輸入其他備註事項 (選填)"></textarea>
</div>
<div class="mb-3">
<label for="lastUpdatedDisplay" class="form-label">最後更新時間</label>
<input type="text" class="form-control" id="lastUpdatedDisplay" readonly disabled>
</div>
<button type="submit" class="btn btn-primary">更新資料</button>
</form>
</div>
<script>
let currentEditingCommunityData = {}; // Holds the data for the community being edited
document.addEventListener('DOMContentLoaded', () => {
// Sample default data (can be replaced by fetching from an actual source or localStorage)
currentEditingCommunityData = {
id: "COMM001",
communityName: "幸福花園社區",
address: "測試路123號",
city: "臺中市",
district: "梧棲區",
zipCode: "435",
mainContactPerson: "林管理員",
contactPhone: "04-26567890",
contactEmail: "manager@example.com",
managementCompany: "好管家保全公司",
numberOfUnits: 150,
notes: "這是一筆用於編輯社區資料頁面測試的範例備註。",
lastUpdated: new Date().toLocaleString() // Store as a string
};
// Attempt to retrieve from localStorage based on URL parameter
const urlParams = new URLSearchParams(window.location.search);
const communityIdFromUrl = urlParams.get('id'); // Community ID might be a string
if (communityIdFromUrl) {
let existingCommunities = JSON.parse(localStorage.getItem('communities_data_temp')) || [];
const foundCommunity = existingCommunities.find(c => c.id === communityIdFromUrl);
if (foundCommunity) {
currentEditingCommunityData = { ...foundCommunity }; // Use a copy
} else {
alert("找不到指定的社區資料!將使用範例資料。 ID: " + communityIdFromUrl);
// Fallback to sample if not found, or assign a new ID if creating
currentEditingCommunityData.id = communityIdFromUrl; // Or generate a new one if it implies creation
}
}
// Populate form fields
document.getElementById('communityId').value = currentEditingCommunityData.id || '';
document.getElementById('communityName').value = currentEditingCommunityData.communityName || '';
document.getElementById('address').value = currentEditingCommunityData.address || '';
document.getElementById('city').value = currentEditingCommunityData.city || '';
document.getElementById('district').value = currentEditingCommunityData.district || '';
document.getElementById('zipCode').value = currentEditingCommunityData.zipCode || '';
document.getElementById('mainContactPerson').value = currentEditingCommunityData.mainContactPerson || '';
document.getElementById('contactPhone').value = currentEditingCommunityData.contactPhone || '';
document.getElementById('contactEmail').value = currentEditingCommunityData.contactEmail || '';
document.getElementById('managementCompany').value = currentEditingCommunityData.managementCompany || '';
document.getElementById('numberOfUnits').value = currentEditingCommunityData.numberOfUnits || 0;
document.getElementById('notes').value = currentEditingCommunityData.notes || '';
document.getElementById('lastUpdatedDisplay').value = currentEditingCommunityData.lastUpdated || new Date().toLocaleString();
});
document.getElementById('editCommunityForm').addEventListener('submit', function(event) {
event.preventDefault();
// Update the global object with form data
currentEditingCommunityData.communityName = document.getElementById('communityName').value;
currentEditingCommunityData.address = document.getElementById('address').value;
currentEditingCommunityData.city = document.getElementById('city').value;
currentEditingCommunityData.district = document.getElementById('district').value;
currentEditingCommunityData.zipCode = document.getElementById('zipCode').value;
currentEditingCommunityData.mainContactPerson = document.getElementById('mainContactPerson').value;
currentEditingCommunityData.contactPhone = document.getElementById('contactPhone').value;
currentEditingCommunityData.contactEmail = document.getElementById('contactEmail').value;
currentEditingCommunityData.managementCompany = document.getElementById('managementCompany').value;
currentEditingCommunityData.numberOfUnits = parseInt(document.getElementById('numberOfUnits').value) || 0;
currentEditingCommunityData.notes = document.getElementById('notes').value;
currentEditingCommunityData.lastUpdated = new Date().toLocaleString(); // Update timestamp
console.log("更新後社區資料 (測試模式):", currentEditingCommunityData);
// Simulate saving to localStorage
let existingCommunities = JSON.parse(localStorage.getItem('communities_data_temp')) || [];
const communityIndex = existingCommunities.findIndex(c => c.id === currentEditingCommunityData.id);
if (communityIndex > -1) {
existingCommunities[communityIndex] = { ...currentEditingCommunityData };
} else {
// If ID was not found (e.g. new entry or ID changed, though ID shouldn't change here)
// For simplicity, we'll assume an ID exists if we are editing.
// If it's a new community, it should have a unique ID assigned.
// For this example, if not found, it will add it. This might happen if the ID from URL didn't exist.
if (!currentEditingCommunityData.id) {
currentEditingCommunityData.id = "COMM" + Date.now(); // Simple unique ID generation
}
existingCommunities.push({ ...currentEditingCommunityData });
}
localStorage.setItem('communities_data_temp', JSON.stringify(existingCommunities));
document.getElementById('lastUpdatedDisplay').value = currentEditingCommunityData.lastUpdated;
alert("社區資料已「模擬更新」至 LocalStorage\n社區名稱" + currentEditingCommunityData.communityName);
// Optionally redirect or give other feedback
// window.location.href = 'community_list.html'; // Example redirect
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

279
Backstage/fee_add.html Normal file
View File

@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>新增繳費通知</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.suggestions-container {
position: relative;
}
.suggestions-box {
position: absolute;
border: 1px solid #ced4da;
border-top: none;
max-height: 200px;
overflow-y: auto;
background-color: white;
width: 100%;
z-index: 1000;
border-radius: 0 0 0.25rem 0.25rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.suggestion-item {
padding: 0.5rem 0.75rem;
cursor: pointer;
}
.suggestion-item:hover, .suggestion-item.active {
background-color: #e9ecef;
}
.suggestions-box:empty {
display: none;
}
</style>
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">新增繳費通知</h2>
<form id="addFeeForm">
<div class="mb-3">
<label for="billingDate" class="form-label">開單日期</label>
<input type="date" class="form-control" id="billingDate" required>
</div>
<div class="mb-3 suggestions-container">
<label for="payer" class="form-label">住戶/繳費人 (可輸入搜尋)</label>
<input type="text" class="form-control" id="payer" placeholder="輸入住戶姓名開始搜尋" required autocomplete="off">
<div id="suggestionsBox" class="suggestions-box"></div>
</div>
<div class="mb-3">
<label for="feeItemSelect" class="form-label">繳費項目</label>
<select class="form-select" id="feeItemSelect" required>
<option value="" disabled selected>請選擇繳費項目</option>
<option value="管理費">管理費</option>
<option value="停車費">停車費</option>
<option value="公設使用費">公設使用費</option>
<option value="垃圾清運費">垃圾清運費</option>
<option value="維修分攤費">維修分攤費</option>
<option value="社區活動費">社區活動費</option>
<option value="其他">其他</option>
</select>
<input type="text" class="form-control mt-2" id="otherFeeItem" placeholder="請輸入其他繳費項目名稱" style="display: none;">
</div>
<div class="mb-3">
<label for="amountDue" class="form-label">應繳金額</label>
<input type="number" class="form-control" id="amountDue" placeholder="輸入應繳金額" min="0" required>
</div>
<div class="mb-3">
<label for="billNumber" class="form-label">繳費單號</label>
<input type="text" class="form-control" id="billNumber" placeholder="輸入繳費單號或參考編號" required>
</div>
<div class="mb-3">
<label for="dueDate" class="form-label">繳費截止日</label>
<input type="date" class="form-control" id="dueDate">
</div>
<div class="mb-3">
<label for="paymentStatus" class="form-label">繳費狀態</label>
<select class="form-select" id="paymentStatus" required>
<option value="待繳費" selected>待繳費</option>
<option value="已繳費">已繳費</option>
<option value="已逾期">已逾期</option>
</select>
</div>
<div class="mb-3">
<label for="notes" class="form-label">備註</label>
<textarea class="form-control" id="notes" rows="3" placeholder="輸入其他備註事項 (選填)"></textarea>
</div>
<button type="submit" class="btn btn-primary">儲存繳費單</button>
<a href="fee_list.html" class="btn btn-secondary ms-2">返回列表</a>
</form>
</div>
<script>
const allResidents = [
"林小安 (A101)", "陳大明 (B203)", "王小美 (C502)", "李四 (A102)", "張三 (D301)",
"趙六 (B205)", "孫七 (E101)", "吳八金 (F405)", "鄭九玲 (G1201)", "周十全 (H707)",
"劉一一 (A103)", "黃二二 (B204)", "蔡三三 (C503)", "許四四 (A104)", "洪五五 (D302)",
"曾六六 (B206)", "蕭七七 (E102)", "葉八八 (F406)", "楊九九 (G1202)", "呂零零 (H708)",
"高才 (A201)", "簡明 (B301)", "施美 (C601)", "溫柔 (A202)", "方正 (D401)",
"管理委員會", "社區服務中心", "訪客登記處", "保全室", "清潔公司"
];
const payerInput = document.getElementById('payer');
const suggestionsBox = document.getElementById('suggestionsBox');
const feeItemSelect = document.getElementById('feeItemSelect');
const otherFeeItemInput = document.getElementById('otherFeeItem');
let activeSuggestionIndex = -1;
payerInput.addEventListener('input', function() {
const inputText = this.value.toLowerCase();
suggestionsBox.innerHTML = '';
activeSuggestionIndex = -1;
if (inputText.length === 0) {
suggestionsBox.style.display = 'none';
return;
}
const filteredResidents = allResidents.filter(resident =>
resident.toLowerCase().includes(inputText)
);
if (filteredResidents.length > 0) {
filteredResidents.forEach((resident, index) => {
const item = document.createElement('div');
item.classList.add('suggestion-item');
item.textContent = resident;
item.addEventListener('click', function() {
payerInput.value = resident;
suggestionsBox.innerHTML = '';
suggestionsBox.style.display = 'none';
});
item.setAttribute('data-index', index);
suggestionsBox.appendChild(item);
});
suggestionsBox.style.display = 'block';
} else {
suggestionsBox.style.display = 'none';
}
});
payerInput.addEventListener('keydown', function(e) {
const items = suggestionsBox.querySelectorAll('.suggestion-item');
if (items.length === 0 || suggestionsBox.style.display === 'none') return;
if (e.key === 'ArrowDown') {
e.preventDefault();
activeSuggestionIndex = (activeSuggestionIndex + 1) % items.length;
updateActiveSuggestion(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
activeSuggestionIndex = (activeSuggestionIndex - 1 + items.length) % items.length;
updateActiveSuggestion(items);
} else if (e.key === 'Enter') {
e.preventDefault();
if (activeSuggestionIndex > -1 && items[activeSuggestionIndex]) {
items[activeSuggestionIndex].click();
} else {
document.getElementById('addFeeForm').dispatchEvent(new Event('submit', {cancelable: true}));
}
} else if (e.key === 'Escape') {
suggestionsBox.innerHTML = '';
suggestionsBox.style.display = 'none';
activeSuggestionIndex = -1;
}
});
function updateActiveSuggestion(items) {
items.forEach(item => item.classList.remove('active'));
if (items[activeSuggestionIndex]) {
items[activeSuggestionIndex].classList.add('active');
items[activeSuggestionIndex].scrollIntoView({ block: 'nearest' });
}
}
document.addEventListener('click', function(event) {
if (!payerInput.contains(event.target) && !suggestionsBox.contains(event.target)) {
suggestionsBox.innerHTML = '';
suggestionsBox.style.display = 'none';
activeSuggestionIndex = -1;
}
});
feeItemSelect.addEventListener('change', function() {
if (this.value === '其他') {
otherFeeItemInput.style.display = 'block';
otherFeeItemInput.required = true;
otherFeeItemInput.focus();
} else {
otherFeeItemInput.style.display = 'none';
otherFeeItemInput.required = false;
otherFeeItemInput.value = '';
}
});
document.addEventListener('DOMContentLoaded', (event) => {
const today = new Date().toISOString().split('T')[0];
const billingDateInput = document.getElementById('billingDate');
if(billingDateInput) {
billingDateInput.value = today;
}
const dueDateInput = document.getElementById('dueDate');
if (dueDateInput) {
const defaultDueDate = new Date();
defaultDueDate.setDate(defaultDueDate.getDate() + 15);
dueDateInput.value = defaultDueDate.toISOString().split('T')[0];
}
// Ensure "Other" fee item field is hidden and not required on initial load
otherFeeItemInput.style.display = 'none';
otherFeeItemInput.required = false;
});
document.getElementById('addFeeForm').addEventListener('submit', function(event) {
event.preventDefault();
let feeItemValue = feeItemSelect.value;
if (feeItemValue === '其他') {
feeItemValue = otherFeeItemInput.value.trim();
if (!feeItemValue) {
alert("請輸入「其他繳費項目」的名稱。");
otherFeeItemInput.focus();
return;
}
}
const newFee = {
id: Date.now(),
billingDate: document.getElementById('billingDate').value,
payer: payerInput.value,
feeItem: feeItemValue,
amountDue: parseFloat(document.getElementById('amountDue').value),
billNumber: document.getElementById('billNumber').value,
dueDate: document.getElementById('dueDate').value,
paymentStatus: document.getElementById('paymentStatus').value,
notes: document.getElementById('notes').value,
reminderCount: 0
};
console.log("新繳費單資料:", newFee);
let existingFees = JSON.parse(localStorage.getItem('fees_data_temp')) || [];
existingFees.push(newFee);
localStorage.setItem('fees_data_temp', JSON.stringify(existingFees));
alert("繳費單資料已「模擬」儲存!\n住戶/繳費人:" + newFee.payer + "\n繳費單號" + newFee.billNumber);
this.reset();
const billingDateInput = document.getElementById('billingDate');
if(billingDateInput) {
billingDateInput.value = new Date().toISOString().split('T')[0];
}
const dueDateInput = document.getElementById('dueDate');
if (dueDateInput) {
const defaultDueDate = new Date();
defaultDueDate.setDate(defaultDueDate.getDate() + 15);
dueDateInput.value = defaultDueDate.toISOString().split('T')[0];
}
document.getElementById('paymentStatus').value = "待繳費";
document.getElementById('amountDue').value = "";
feeItemSelect.value = ""; // Reset select to default placeholder
otherFeeItemInput.style.display = 'none';
otherFeeItemInput.required = false;
otherFeeItemInput.value = '';
suggestionsBox.innerHTML = '';
suggestionsBox.style.display = 'none';
activeSuggestionIndex = -1;
payerInput.focus();
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

327
Backstage/fee_edit.html Normal file
View File

@ -0,0 +1,327 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>編輯繳費通知 (測試模式)</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.suggestions-container {
position: relative;
}
.suggestions-box {
position: absolute;
border: 1px solid #ced4da;
border-top: none;
max-height: 200px;
overflow-y: auto;
background-color: white;
width: 100%;
z-index: 1000;
border-radius: 0 0 0.25rem 0.25rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.suggestion-item {
padding: 0.5rem 0.75rem;
cursor: pointer;
}
.suggestion-item:hover, .suggestion-item.active {
background-color: #e9ecef;
}
.suggestions-box:empty {
display: none;
}
</style>
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">編輯繳費通知 (測試模式)</h2>
<form id="editFeeForm">
<input type="hidden" id="feeId" name="feeId">
<div class="mb-3">
<label for="billingDate" class="form-label">開單日期</label>
<input type="date" class="form-control" id="billingDate" required>
</div>
<div class="mb-3 suggestions-container">
<label for="payer" class="form-label">住戶/繳費人 (可輸入搜尋)</label>
<input type="text" class="form-control" id="payer" placeholder="輸入住戶姓名開始搜尋" required autocomplete="off">
<div id="suggestionsBox" class="suggestions-box"></div>
</div>
<div class="mb-3">
<label for="feeItemSelect" class="form-label">繳費項目</label>
<select class="form-select" id="feeItemSelect" required>
<option value="" disabled>請選擇繳費項目</option>
<option value="管理費">管理費</option>
<option value="停車費">停車費</option>
<option value="公設使用費">公設使用費</option>
<option value="垃圾清運費">垃圾清運費</option>
<option value="維修分攤費">維修分攤費</option>
<option value="社區活動費">社區活動費</option>
<option value="其他">其他</option>
</select>
<input type="text" class="form-control mt-2" id="otherFeeItem" placeholder="請輸入其他繳費項目名稱" style="display: none;">
</div>
<div class="mb-3">
<label for="amountDue" class="form-label">應繳金額</label>
<input type="number" class="form-control" id="amountDue" placeholder="輸入應繳金額" min="0" required>
</div>
<div class="mb-3">
<label for="billNumber" class="form-label">繳費單號</label>
<input type="text" class="form-control" id="billNumber" placeholder="輸入繳費單號或參考編號" required>
</div>
<div class="mb-3">
<label for="dueDate" class="form-label">繳費截止日</label>
<input type="date" class="form-control" id="dueDate">
</div>
<div class="mb-3">
<label for="paymentStatus" class="form-label">繳費狀態</label>
<select class="form-select" id="paymentStatus" required>
<option value="待繳費">待繳費</option>
<option value="已繳費">已繳費</option>
<option value="已逾期">已逾期</option>
</select>
</div>
<div class="row mb-3">
<div class="col-md-9">
<label for="notes" class="form-label">備註</label>
<textarea class="form-control" id="notes" rows="3" placeholder="輸入其他備註事項 (選填)"></textarea>
</div>
<div class="col-md-3">
<label for="reminderCountDisplay" class="form-label">提醒次數</label>
<input type="text" class="form-control" id="reminderCountDisplay" readonly disabled>
<button type="button" class="btn btn-outline-info btn-sm mt-2 w-100" id="resendReminderBtn">重發繳費提醒</button>
</div>
</div>
<button type="submit" class="btn btn-primary">更新繳費單</button>
<a href="fee_list.html" class="btn btn-secondary ms-2">返回列表</a>
</form>
</div>
<script>
let currentEditingFee = {};
const allResidents = [
"林小安 (A101)", "陳大明 (B203)", "王小美 (C502)", "李四 (A102)", "張三 (D301)",
"趙六 (B205)", "孫七 (E101)", "吳八金 (F405)", "鄭九玲 (G1201)", "周十全 (H707)",
"劉一一 (A103)", "黃二二 (B204)", "蔡三三 (C503)", "許四四 (A104)", "洪五五 (D302)",
"曾六六 (B206)", "蕭七七 (E102)", "葉八八 (F406)", "楊九九 (G1202)", "呂零零 (H708)",
"高才 (A201)", "簡明 (B301)", "施美 (C601)", "溫柔 (A202)", "方正 (D401)",
"管理委員會", "社區服務中心", "訪客登記處", "保全室", "清潔公司", "測試用戶 (Z999)"
];
const payerInput = document.getElementById('payer');
const suggestionsBox = document.getElementById('suggestionsBox');
const reminderCountDisplay = document.getElementById('reminderCountDisplay');
const resendReminderBtn = document.getElementById('resendReminderBtn');
const feeItemSelect = document.getElementById('feeItemSelect');
const otherFeeItemInput = document.getElementById('otherFeeItem');
let activeSuggestionIndex = -1;
payerInput.addEventListener('input', function() {
const inputText = this.value.toLowerCase();
suggestionsBox.innerHTML = '';
activeSuggestionIndex = -1;
if (inputText.length === 0) {
suggestionsBox.style.display = 'none';
return;
}
const filteredResidents = allResidents.filter(resident =>
resident.toLowerCase().includes(inputText)
);
if (filteredResidents.length > 0) {
filteredResidents.forEach((resident, index) => {
const item = document.createElement('div');
item.classList.add('suggestion-item');
item.textContent = resident;
item.addEventListener('click', function() {
payerInput.value = resident;
suggestionsBox.innerHTML = '';
suggestionsBox.style.display = 'none';
});
item.setAttribute('data-index', index);
suggestionsBox.appendChild(item);
});
suggestionsBox.style.display = 'block';
} else {
suggestionsBox.style.display = 'none';
}
});
payerInput.addEventListener('keydown', function(e) {
const items = suggestionsBox.querySelectorAll('.suggestion-item');
if (items.length === 0 || suggestionsBox.style.display === 'none') return;
if (e.key === 'ArrowDown') {
e.preventDefault();
activeSuggestionIndex = (activeSuggestionIndex + 1) % items.length;
updateActiveSuggestion(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
activeSuggestionIndex = (activeSuggestionIndex - 1 + items.length) % items.length;
updateActiveSuggestion(items);
} else if (e.key === 'Enter') {
e.preventDefault();
if (activeSuggestionIndex > -1 && items[activeSuggestionIndex]) {
items[activeSuggestionIndex].click();
} else {
document.getElementById('editFeeForm').dispatchEvent(new Event('submit', {cancelable: true}));
}
} else if (e.key === 'Escape') {
suggestionsBox.innerHTML = '';
suggestionsBox.style.display = 'none';
activeSuggestionIndex = -1;
}
});
function updateActiveSuggestion(items) {
items.forEach(item => item.classList.remove('active'));
if (items[activeSuggestionIndex]) {
items[activeSuggestionIndex].classList.add('active');
items[activeSuggestionIndex].scrollIntoView({ block: 'nearest' });
}
}
document.addEventListener('click', function(event) {
if (!payerInput.contains(event.target) && !suggestionsBox.contains(event.target)) {
suggestionsBox.innerHTML = '';
suggestionsBox.style.display = 'none';
activeSuggestionIndex = -1;
}
});
feeItemSelect.addEventListener('change', function() {
if (this.value === '其他') {
otherFeeItemInput.style.display = 'block';
otherFeeItemInput.required = true;
otherFeeItemInput.focus();
} else {
otherFeeItemInput.style.display = 'none';
otherFeeItemInput.required = false;
otherFeeItemInput.value = '';
}
});
document.addEventListener('DOMContentLoaded', () => {
currentEditingFee = {
id: 99901,
billingDate: "2025-05-20",
payer: "測試用戶 (Z999)",
feeItem: "測試其他項目", // Sample for "Other"
amountDue: 1500,
billNumber: "TESTFEE00123XYZ",
dueDate: "2025-06-05",
paymentStatus: "待繳費",
notes: "這是一筆用於編輯頁面測試的繳費備註。",
reminderCount: 1
};
const urlParams = new URLSearchParams(window.location.search);
const feeIdFromUrl = parseInt(urlParams.get('id'));
if (feeIdFromUrl) {
let existingFees = JSON.parse(localStorage.getItem('fees_data_temp')) || [];
const foundFee = existingFees.find(f => f.id === feeIdFromUrl);
if (foundFee) {
currentEditingFee = { ...foundFee };
} else {
alert("找不到指定的繳費通知資料!將使用範例資料。 ID: " + feeIdFromUrl);
}
}
document.getElementById('feeId').value = currentEditingFee.id;
document.getElementById('billingDate').value = currentEditingFee.billingDate;
payerInput.value = currentEditingFee.payer;
document.getElementById('amountDue').value = currentEditingFee.amountDue;
document.getElementById('billNumber').value = currentEditingFee.billNumber;
document.getElementById('dueDate').value = currentEditingFee.dueDate || '';
document.getElementById('paymentStatus').value = currentEditingFee.paymentStatus;
document.getElementById('notes').value = currentEditingFee.notes;
reminderCountDisplay.value = currentEditingFee.reminderCount || 0;
// Populate fee item dropdown
const feeItemValueFromData = currentEditingFee.feeItem;
let isOtherFeeItem = true;
if (feeItemValueFromData) {
for (let i = 0; i < feeItemSelect.options.length; i++) {
if (feeItemSelect.options[i].value === feeItemValueFromData) {
feeItemSelect.value = feeItemValueFromData;
isOtherFeeItem = false;
break;
}
}
if (isOtherFeeItem) {
feeItemSelect.value = '其他';
otherFeeItemInput.value = feeItemValueFromData;
otherFeeItemInput.style.display = 'block';
otherFeeItemInput.required = true;
} else {
otherFeeItemInput.style.display = 'none';
otherFeeItemInput.required = false;
}
} else {
feeItemSelect.value = ""; // Set to default "請選擇繳費項目"
otherFeeItemInput.style.display = 'none';
otherFeeItemInput.required = false;
}
});
resendReminderBtn.addEventListener('click', function() {
currentEditingFee.reminderCount = (parseInt(currentEditingFee.reminderCount) || 0) + 1;
reminderCountDisplay.value = currentEditingFee.reminderCount;
alert(`已模擬重新發送第 ${currentEditingFee.reminderCount} 次繳費提醒給 ${currentEditingFee.payer}`);
});
document.getElementById('editFeeForm').addEventListener('submit', function(event) {
event.preventDefault();
let updatedFeeItemValue = feeItemSelect.value;
if (updatedFeeItemValue === '其他') {
updatedFeeItemValue = otherFeeItemInput.value.trim();
if (!updatedFeeItemValue) {
alert("請輸入「其他繳費項目」的名稱。");
otherFeeItemInput.focus();
return;
}
}
currentEditingFee.billingDate = document.getElementById('billingDate').value;
currentEditingFee.payer = payerInput.value;
currentEditingFee.feeItem = updatedFeeItemValue;
currentEditingFee.amountDue = parseFloat(document.getElementById('amountDue').value);
currentEditingFee.billNumber = document.getElementById('billNumber').value;
currentEditingFee.dueDate = document.getElementById('dueDate').value;
currentEditingFee.paymentStatus = document.getElementById('paymentStatus').value;
currentEditingFee.notes = document.getElementById('notes').value;
console.log("更新後繳費單資料 (測試模式):", currentEditingFee);
let existingFees = JSON.parse(localStorage.getItem('fees_data_temp')) || [];
const feeIndex = existingFees.findIndex(f => f.id === currentEditingFee.id);
if (feeIndex > -1) {
existingFees[feeIndex] = { ...currentEditingFee };
localStorage.setItem('fees_data_temp', JSON.stringify(existingFees));
alert("繳費單資料已「模擬更新」至 LocalStorage\n住戶/繳費人:" + currentEditingFee.payer + "\n提醒次數" + currentEditingFee.reminderCount);
} else {
existingFees.push({ ...currentEditingFee });
localStorage.setItem('fees_data_temp', JSON.stringify(existingFees));
alert("繳費單資料 (ID: " + currentEditingFee.id + ") 在列表中未找到,已「模擬新增」至 LocalStorage\n提醒次數" + currentEditingFee.reminderCount);
}
suggestionsBox.innerHTML = '';
suggestionsBox.style.display = 'none';
activeSuggestionIndex = -1;
// window.location.href = 'fee_list.html';
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

280
Backstage/fee_list.html Normal file
View File

@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>繳費通知管理</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
}
.container.mt-4 {
flex-grow: 1;
}
</style>
</head>
<body>
<div class="container mt-4">
<h2 class="mb-4">繳費通知管理</h2>
<div class="row g-2 mb-3">
<div class="col-auto"> <a href="fee_add.html" class="btn btn-outline-success rounded-pill px-3">新增繳費單</a> </div>
<div class="col-auto"> <button class="btn btn-outline-secondary rounded-pill px-3" onclick="exportFees()">匯出列表</button>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-sm-6 col-md-3 col-lg-3">
<input type="text" id="searchPayer" class="form-control" placeholder="搜尋住戶/繳費人">
</div>
<div class="col-sm-6 col-md-3 col-lg-3">
<input type="text" id="searchBillNumber" class="form-control" placeholder="搜尋繳費單號">
</div>
<div class="col-sm-6 col-md-2 col-lg-2">
<select id="searchPaymentStatus" class="form-select">
<option value="">所有狀態</option>
<option value="待繳費">待繳費</option>
<option value="已繳費">已繳費</option>
<option value="已逾期">已逾期</option>
</select>
</div>
<div class="col-6 col-md-2 col-lg-2">
<button class="btn btn-outline-primary w-100 rounded-pill" onclick="performSearch()">搜尋</button>
</div>
<div class="col-6 col-md-2 col-lg-2"> <button class="btn btn-outline-danger w-100 rounded-pill" onclick="deleteSelected()">刪除勾選</button>
</div>
</div>
<table class="table table-bordered table-hover align-middle">
<thead class="table-dark">
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleAll(this)"></th>
<th>ID</th>
<th>開單日期</th>
<th>住戶/繳費人</th>
<th>應繳金額</th>
<th>繳費項目</th>
<th>繳費單號</th>
<th>繳費狀態</th>
<th>操作</th>
</tr>
</thead>
<tbody id="feeTable">
</tbody>
</table>
<div class="d-flex justify-content-between align-items-center mt-3">
<div><span id="currentPageInfo">1</span> 頁,共 <span id="totalPagesInfo">1</span></div>
<div>
<button id="prevPageBtn" class="btn btn-sm btn-outline-secondary me-1" onclick="prevPage()">上一頁</button>
<button id="nextPageBtn" class="btn btn-sm btn-outline-secondary" onclick="nextPage()">下一頁</button>
</div>
</div>
</div>
<script>
const pageSize = 5; // Number of items per page
let currentPage = 1;
let currentFilteredData = [];
// Sample fee data. In a real application, this would come from a server.
let fees_data = [
{ id: 1, billingDate: "2025/05/01", payer: "林小安 (A101)", amountDue: 1200, feeItem: "管理費 2025年5月", billNumber: "FEE20250501001", paymentStatus: "待繳費", notes: "一般管理費", dueDate: "2025/05/15" },
{ id: 2, billingDate: "2025/05/01", payer: "陳大明 (B203)", amountDue: 300, feeItem: "停車清潔費", billNumber: "FEE20250501002", paymentStatus: "待繳費", notes: "", dueDate: "2025/05/10" },
{ id: 3, billingDate: "2025/04/01", payer: "王小美 (C502)", amountDue: 1200, feeItem: "管理費 2025年4月", billNumber: "FEE20250401001", paymentStatus: "已繳費", notes: "臨櫃繳清", dueDate: "2025/04/15" },
{ id: 4, billingDate: "2025/03/01", payer: "李四 (A102)", amountDue: 500, feeItem: "公設修繕分攤", billNumber: "FEE20250301003", paymentStatus: "已逾期", notes: "多次催繳未付", dueDate: "2025/03/15" },
{ id: 5, billingDate: "2025/05/05", payer: "張三 (D301)", amountDue: 150, feeItem: "腳踏車位租金", billNumber: "FEE20250505001", paymentStatus: "待繳費", notes: "", dueDate: "2025/05/20" },
{ id: 6, billingDate: "2025/05/10", payer: "趙六 (B205)", amountDue: 2000, feeItem: "預繳上半年管理費", billNumber: "FEE20250510001", paymentStatus: "已繳費", notes: "一次付清", dueDate: "2025/05/25" },
{ id: 7, billingDate: "2025/04/15", payer: "孫七 (E101)", amountDue: 750, feeItem: "社區活動費", billNumber: "FEE20250415001", paymentStatus: "待繳費", notes: "母親節活動", dueDate: "2025/04/30" }
];
function renderTable(dataToRender) {
const tbody = document.getElementById("feeTable");
tbody.innerHTML = "";
const start = (currentPage - 1) * pageSize;
const pageData = dataToRender.slice(start, start + pageSize);
pageData.forEach(fee => {
const row = document.createElement("tr");
let statusBadgeClass = 'bg-secondary'; // Default badge
switch (fee.paymentStatus) {
case '待繳費':
statusBadgeClass = 'bg-warning text-dark';
break;
case '已繳費':
statusBadgeClass = 'bg-success';
break;
case '已逾期':
statusBadgeClass = 'bg-danger';
break;
}
row.innerHTML = `
<td><input type="checkbox" class="row-checkbox" data-id="${fee.id}"></td>
<td>${fee.id}</td>
<td>${fee.billingDate}</td>
<td>${fee.payer}</td>
<td>$${fee.amountDue}</td>
<td>${fee.feeItem}</td>
<td>${fee.billNumber}</td>
<td><span class="badge ${statusBadgeClass}">${fee.paymentStatus}</span></td>
<td>
<a href="fee_edit.html?id=${fee.id}" class="btn btn-outline-secondary btn-sm">編輯</a>
<button class="btn btn-outline-danger btn-sm" onclick="deleteFee(${fee.id})">刪除</button>
</td>
`;
tbody.appendChild(row);
});
updatePaginationControls(dataToRender.length);
const selectAllCheckbox = document.getElementById("selectAll");
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
}
function updatePaginationControls(totalItems) {
const totalPages = Math.ceil(totalItems / pageSize) || 1;
const currentPageDisplay = document.getElementById("currentPageInfo");
const totalPagesDisplay = document.getElementById("totalPagesInfo");
const prevButton = document.getElementById("prevPageBtn");
const nextButton = document.getElementById("nextPageBtn");
if (currentPageDisplay) currentPageDisplay.textContent = currentPage;
if (totalPagesDisplay) totalPagesDisplay.textContent = totalPages;
if (prevButton) prevButton.disabled = currentPage === 1;
if (nextButton) nextButton.disabled = currentPage === totalPages || totalItems === 0;
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
renderTable(currentFilteredData);
} else if (totalItems === 0) {
if (currentPageDisplay) currentPageDisplay.textContent = 1;
if (totalPagesDisplay) totalPagesDisplay.textContent = 1;
if (prevButton) prevButton.disabled = true;
if (nextButton) nextButton.disabled = true;
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function nextPage() {
const totalPages = Math.ceil(currentFilteredData.length / pageSize);
if (currentPage < totalPages) {
currentPage++;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function performSearch() {
const payerInput = document.getElementById("searchPayer").value.toLowerCase();
const billNumberInput = document.getElementById("searchBillNumber").value.toLowerCase();
const paymentStatusInput = document.getElementById("searchPaymentStatus").value;
// Load from localStorage if available
const storedFees = JSON.parse(localStorage.getItem('fees_data_temp'));
if (storedFees) {
fees_data = storedFees;
}
currentFilteredData = fees_data.filter(fee =>
fee.payer.toLowerCase().includes(payerInput) &&
fee.billNumber.toLowerCase().includes(billNumberInput) &&
(paymentStatusInput === "" || fee.paymentStatus === paymentStatusInput)
);
currentPage = 1;
renderTable(currentFilteredData);
}
function toggleAll(source) {
const checkboxes = document.querySelectorAll(".row-checkbox");
checkboxes.forEach(cb => cb.checked = source.checked);
}
function deleteSelected() {
if (!confirm("確定要刪除勾選的繳費通知嗎?")) return;
const selected = document.querySelectorAll(".row-checkbox:checked");
if (selected.length === 0) {
alert("請先勾選要刪除的繳費通知");
return;
}
const idsToDelete = Array.from(selected).map(cb => parseInt(cb.dataset.id));
fees_data = fees_data.filter(fee => !idsToDelete.includes(fee.id));
localStorage.setItem('fees_data_temp', JSON.stringify(fees_data)); // Update localStorage
performSearch(); // Re-filter and re-render
}
function deleteFee(id) {
if (!confirm("確定要刪除此筆繳費通知嗎?")) return;
fees_data = fees_data.filter(fee => fee.id !== id);
localStorage.setItem('fees_data_temp', JSON.stringify(fees_data)); // Update localStorage
performSearch(); // Re-filter and re-render
}
function exportFees() {
const payerInput = document.getElementById("searchPayer").value.toLowerCase();
const billNumberInput = document.getElementById("searchBillNumber").value.toLowerCase();
const paymentStatusInput = document.getElementById("searchPaymentStatus").value;
let dataToExport = fees_data;
// If any search criteria is active, export filtered data
if (payerInput || billNumberInput || paymentStatusInput) {
dataToExport = currentFilteredData;
}
if (dataToExport.length === 0) {
alert("沒有資料可匯出。");
return;
}
let csvContent = "data:text/csv;charset=utf-8,\uFEFF"; // \uFEFF for BOM
csvContent += "ID,開單日期,住戶/繳費人,應繳金額,繳費項目,繳費單號,繳費狀態,繳費截止日,備註,提醒次數\n";
dataToExport.forEach(fee => {
const cleanPayer = `"${fee.payer.replace(/"/g, '""')}"`;
const cleanFeeItem = `"${fee.feeItem.replace(/"/g, '""')}"`;
const cleanBillNum = `"${fee.billNumber.replace(/"/g, '""')}"`;
const cleanStatus = `"${fee.paymentStatus.replace(/"/g, '""')}"`;
const cleanDueDate = `"${fee.dueDate ? fee.dueDate.replace(/"/g, '""') : ''}"`;
const cleanNotes = `"${fee.notes ? fee.notes.replace(/"/g, '""') : ''}"`;
const reminderCount = fee.reminderCount || 0;
const row = [fee.id, fee.billingDate, cleanPayer, fee.amountDue, cleanFeeItem, cleanBillNum, cleanStatus, cleanDueDate, cleanNotes, reminderCount];
csvContent += row.join(",") + "\n";
});
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "fees_export.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
alert(`準備匯出 ${dataToExport.length} 筆繳費資料(${payerInput || billNumberInput || paymentStatusInput ? "依搜尋條件" : "全部"}`);
}
// Initial load: apply no filters (show all) and render
document.addEventListener('DOMContentLoaded', () => {
performSearch();
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -19,7 +19,7 @@
</head>
<body>
<div class="container mt-4">
<h2 class="mb-4">郵寄廠商</h2>
<h2 class="mb-4">郵寄廠商(會在新增郵寄廠商的下拉選單顯示)</h2>
<div class="row g-2 mb-3">
<div class="col-auto">

View File

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>新增繳費項目</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">新增繳費項目</h2>
<form id="createItemForm">
<div class="mb-3">
<label for="itemName" class="form-label">項目名稱</label>
<input type="text" class="form-control" id="itemName" required>
</div>
<button type="submit" class="btn btn-success">新增項目</button>
<a href="payment_item_list.html" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
<script>
document.getElementById("createItemForm").addEventListener("submit", function (e) {
e.preventDefault();
const newItem = {
name: document.getElementById("itemName").value
};
// In a real application, you would generate a unique ID here or get it from the backend.
// For demonstration, we'll assume the list page's script handles ID generation or can work without it initially.
// To pass data to the list page, you could use localStorage or similar mechanisms.
// For example:
// let items = JSON.parse(localStorage.getItem('paymentItems')) || [];
// newItem.id = items.length > 0 ? Math.max(...items.map(i => i.id)) + 1 : 1; // Simple ID generation
// items.push(newItem);
// localStorage.setItem('paymentItems', JSON.stringify(items));
console.log("新增的繳費項目資料:", newItem);
alert("繳費項目資料已新增(此為前端演示,實際需串接後端 API 並更新列表頁資料來源)");
// Typically, you'd pass the new data to the list page or have the list page re-fetch.
// For now, we'll just redirect.
window.location.href = "payment_item_list.html";
});
</script>
</body>
</html>
payment_item_edit.html
HTML
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>編輯繳費項目資料</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">編輯繳費項目資料</h2>
<form id="editItemForm">
<div class="mb-3">
<label for="itemName" class="form-label">項目名稱</label>
<input type="text" class="form-control" id="itemName" required>
</div>
<button type="submit" class="btn btn-primary">儲存變更</button>
<a href="payment_item_list.html" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
<script>
// Function to get URL parameters
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
const results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
const itemId = getUrlParameter('id');
let currentItemName = "範例繳費項目"; // Default name
// This is a simplified placeholder for fetching the item's current name.
// In a real scenario, you'd retrieve this from localStorage (if set by the list page)
// or an API using itemId.
// For example, if you stored items in localStorage from the list page:
// let items = JSON.parse(localStorage.getItem('paymentItems')) || [];
// const itemToEdit = items.find(item => item.id === parseInt(itemId));
// if (itemToEdit) {
// currentItemName = itemToEdit.name;
// } else if (itemId) {
// currentItemName = `項目 #${itemId}`; // Fallback if not found but ID exists
// }
// Populate the form field
document.getElementById("itemName").value = currentItemName;
if (itemId) {
// If you have a way to get the actual name, display it.
// For this demo, we will try to get it from a conceptual 'paymentItems' array
// that might be available globally or via localStorage if the list page sets it.
// This part is illustrative as data isn't directly passed between standalone HTML files without a backend or localStorage.
// The `payment_item_list.html` file initializes `paymentItems`.
// For a more robust solution, you would typically fetch by ID from a backend or ensure localStorage is populated.
// To make this example work somewhat standalone without implementing full localStorage passing for editing:
// We'll log that we'd be fetching actual data here.
console.log("Attempting to edit item with ID:", itemId);
// In a real application, you would retrieve the existing item's name here, e.g.,
// fetch(`/api/payment-items/${itemId}`).then(res => res.json()).then(data => {
// document.getElementById("itemName").value = data.name;
// });
// For now, the placeholder "範例繳費項目" or a generic name will be used if not found in a hypothetical localStorage.
}
document.getElementById("editItemForm").addEventListener("submit", function (e) {
e.preventDefault();
const updatedItem = {
id: itemId ? parseInt(itemId) : null, // Keep the ID if it exists
name: document.getElementById("itemName").value
};
// In a real application, you would send this data to a backend to update.
// And update localStorage if you are using it for the list.
// For example:
// let items = JSON.parse(localStorage.getItem('paymentItems')) || [];
// const itemIndex = items.findIndex(item => item.id === updatedItem.id);
// if (itemIndex > -1) {
// items[itemIndex] = updatedItem;
// localStorage.setItem('paymentItems', JSON.stringify(items));
// }
console.log("更新後的繳費項目資料:", updatedItem);
alert("繳費項目資料已更新(此為前端演示,實際需串接後端 API 並更新列表頁資料來源)");
window.location.href = "payment_item_list.html";
});
</script>
</body>
</html>

View File

@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>編輯繳費項目資料</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">編輯繳費項目資料</h2>
<form id="editItemForm">
<div class="mb-3">
<label for="itemName" class="form-label">項目名稱</label>
<input type="text" class="form-control" id="itemName" required>
</div>
<button type="submit" class="btn btn-primary">儲存變更</button>
<a href="payment_item_list.html" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
<script>
// Function to get URL parameters
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
const results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
const itemId = getUrlParameter('id');
let currentItemName = "範例繳費項目"; // Default name
// This is a simplified placeholder for fetching the item's current name.
// In a real scenario, you'd retrieve this from localStorage (if set by the list page)
// or an API using itemId.
// For example, if you stored items in localStorage from the list page:
// let items = JSON.parse(localStorage.getItem('paymentItems')) || [];
// const itemToEdit = items.find(item => item.id === parseInt(itemId));
// if (itemToEdit) {
// currentItemName = itemToEdit.name;
// } else if (itemId) {
// currentItemName = `項目 #${itemId}`; // Fallback if not found but ID exists
// }
// Populate the form field
document.getElementById("itemName").value = currentItemName;
if (itemId) {
// If you have a way to get the actual name, display it.
// For this demo, we will try to get it from a conceptual 'paymentItems' array
// that might be available globally or via localStorage if the list page sets it.
// This part is illustrative as data isn't directly passed between standalone HTML files without a backend or localStorage.
// The `payment_item_list.html` file initializes `paymentItems`.
// For a more robust solution, you would typically fetch by ID from a backend or ensure localStorage is populated.
// To make this example work somewhat standalone without implementing full localStorage passing for editing:
// We'll log that we'd be fetching actual data here.
console.log("Attempting to edit item with ID:", itemId);
// In a real application, you would retrieve the existing item's name here, e.g.,
// fetch(`/api/payment-items/${itemId}`).then(res => res.json()).then(data => {
// document.getElementById("itemName").value = data.name;
// });
// For now, the placeholder "範例繳費項目" or a generic name will be used if not found in a hypothetical localStorage.
}
document.getElementById("editItemForm").addEventListener("submit", function (e) {
e.preventDefault();
const updatedItem = {
id: itemId ? parseInt(itemId) : null, // Keep the ID if it exists
name: document.getElementById("itemName").value
};
// In a real application, you would send this data to a backend to update.
// And update localStorage if you are using it for the list.
// For example:
// let items = JSON.parse(localStorage.getItem('paymentItems')) || [];
// const itemIndex = items.findIndex(item => item.id === updatedItem.id);
// if (itemIndex > -1) {
// items[itemIndex] = updatedItem;
// localStorage.setItem('paymentItems', JSON.stringify(items));
// }
console.log("更新後的繳費項目資料:", updatedItem);
alert("繳費項目資料已更新(此為前端演示,實際需串接後端 API 並更新列表頁資料來源)");
window.location.href = "payment_item_list.html";
});
</script>
</body>
</html>

View File

@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>繳費項目列表</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
}
.container.mt-4 {
flex-grow: 1;
}
</style>
</head>
<body>
<div class="container mt-4">
<h2 class="mb-4">繳費項目(會在新增編輯繳費通知的下拉選單顯示)</h2>
<div class="row g-2 mb-3">
<div class="col-auto">
<a href="payment_item_add.html" class="btn btn-outline-success rounded-pill">新增項目</a>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-sm-8 col-md-5 col-lg-4">
<input type="text" id="searchName" class="form-control" placeholder="搜尋項目名稱">
</div>
<div class="col-auto">
<button class="btn btn-outline-primary w-100 rounded-pill" onclick="performSearch()">搜尋</button>
</div>
<div class="col-auto">
<button class="btn btn-outline-danger w-100 rounded-pill" onclick="deleteSelected()">刪除勾選</button>
</div>
</div>
<table class="table table-bordered table-hover align-middle">
<thead class="table-dark">
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleAll(this)"></th>
<th>項目名稱</th>
<th>操作</th>
</tr>
</thead>
<tbody id="itemTable"></tbody>
</table>
<div class="d-flex justify-content-between align-items-center mt-3">
<div><span id="currentPageInfo">1</span> 頁,共 <span id="totalPagesInfo">1</span></div>
<div>
<button id="prevPageBtn" class="btn btn-sm btn-outline-secondary me-1" onclick="prevPage()">上一頁</button>
<button id="nextPageBtn" class="btn btn-sm btn-outline-secondary" onclick="nextPage()">下一頁</button>
</div>
</div>
</div>
<script>
const pageSize = 5; // Adjusted page size for simpler data
let currentPage = 1;
let currentFilteredData = [];
// Sample data for payment items
let paymentItems = [
{ id: 1, name: "社區管理費" },
{ id: 2, name: "水費" },
{ id: 3, name: "電費" },
{ id: 4, name: "瓦斯費" },
{ id: 5, name: "網路費" },
{ id: 6, name: "第四台有線電視費" },
{ id: 7, name: "停車場清潔費" },
{ id: 8, name: "社區公基金" },
{ id: 9, name: "垃圾處理費" },
{ id: 10, name: "書報雜誌費" },
{ id: 11, name: "健身房使用月費" }
];
function renderTable(dataToRender) {
const tbody = document.getElementById("itemTable");
tbody.innerHTML = "";
const start = (currentPage - 1) * pageSize;
const pageData = dataToRender.slice(start, start + pageSize);
pageData.forEach(item => {
const row = document.createElement("tr");
row.innerHTML = `
<td><input type="checkbox" class="row-checkbox" data-id="${item.id}"></td>
<td>${item.name}</td>
<td>
<a href="payment_item_edit.html?id=${item.id}" class="btn btn-outline-secondary btn-sm">編輯</a>
<button class="btn btn-outline-danger btn-sm" onclick="deleteItem(${item.id})">刪除</button>
</td>
`;
tbody.appendChild(row);
});
updatePaginationControls(dataToRender.length);
}
function updatePaginationControls(totalItems) {
const totalPages = Math.ceil(totalItems / pageSize) || 1;
const currentPageDisplay = document.getElementById("currentPageInfo");
const totalPagesDisplay = document.getElementById("totalPagesInfo");
const prevButton = document.getElementById("prevPageBtn");
const nextButton = document.getElementById("nextPageBtn");
if (currentPageDisplay) currentPageDisplay.textContent = currentPage;
if (totalPagesDisplay) totalPagesDisplay.textContent = totalPages;
if (prevButton) prevButton.disabled = currentPage === 1;
if (nextButton) nextButton.disabled = currentPage === totalPages || totalItems === 0;
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
renderTable(currentFilteredData);
} else if (totalItems === 0) {
if (currentPageDisplay) currentPageDisplay.textContent = 1;
if (totalPagesDisplay) totalPagesDisplay.textContent = 1;
if (prevButton) prevButton.disabled = true;
if (nextButton) nextButton.disabled = true;
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function nextPage() {
const totalPages = Math.ceil(currentFilteredData.length / pageSize);
if (currentPage < totalPages) {
currentPage++;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function performSearch() {
const nameInput = document.getElementById("searchName").value.toLowerCase();
currentFilteredData = paymentItems.filter(item =>
item.name.toLowerCase().includes(nameInput)
);
currentPage = 1;
renderTable(currentFilteredData);
}
function toggleAll(source) {
const checkboxes = document.querySelectorAll(".row-checkbox");
checkboxes.forEach(cb => cb.checked = source.checked);
}
function deleteSelected() {
if (!confirm("確定要刪除勾選的資料嗎?")) return;
const selected = document.querySelectorAll(".row-checkbox:checked");
if (selected.length === 0) {
alert("請先勾選要刪除的資料");
return;
}
const idsToDelete = Array.from(selected).map(cb => parseInt(cb.dataset.id));
paymentItems = paymentItems.filter(item => !idsToDelete.includes(item.id));
const nameInput = document.getElementById("searchName").value.toLowerCase();
currentFilteredData = paymentItems.filter(item =>
item.name.toLowerCase().includes(nameInput)
);
const totalPages = Math.ceil(currentFilteredData.length / pageSize) || 1;
if (currentPage > totalPages) {
currentPage = totalPages;
}
if (currentFilteredData.length === 0) {
currentPage = 1;
}
renderTable(currentFilteredData);
document.getElementById("selectAll").checked = false;
}
function deleteItem(id) {
if (!confirm("確定要刪除此筆資料嗎?")) return;
paymentItems = paymentItems.filter(item => item.id !== id);
const nameInput = document.getElementById("searchName").value.toLowerCase();
currentFilteredData = paymentItems.filter(item =>
item.name.toLowerCase().includes(nameInput)
);
const itemsOnCurrentPageAfterDelete = currentFilteredData.slice((currentPage - 1) * pageSize, currentPage * pageSize).length;
const totalPages = Math.ceil(currentFilteredData.length / pageSize) || 1;
if (itemsOnCurrentPageAfterDelete === 0 && currentPage > 1) {
currentPage--;
} else if (currentPage > totalPages) {
currentPage = totalPages;
}
if (currentFilteredData.length === 0) {
currentPage = 1;
}
renderTable(currentFilteredData);
}
// Initial load
performSearch();
</script>
</body>
</html>

View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>新增來訪事由</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">新增來訪事由</h2>
<form id="createForm">
<div class="mb-3">
<label for="itemName" class="form-label">事由名稱</label>
<input type="text" class="form-control" id="itemName" required>
</div>
<button type="submit" class="btn btn-success">新增事由</button>
<a href="purpose_list.html" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
<script>
document.getElementById("createForm").addEventListener("submit", function (e) {
e.preventDefault();
const localStorageKey = 'purposesData'; // Changed Key
const itemName = document.getElementById("itemName").value;
if (!itemName.trim()) {
alert("事由名稱不能為空!");
return;
}
let dataItems = JSON.parse(localStorage.getItem(localStorageKey)) || [];
if (dataItems.some(item => item.name.toLowerCase() === itemName.toLowerCase())) {
alert("此事由名稱已存在!");
return;
}
const newItem = {
id: dataItems.length > 0 ? Math.max(...dataItems.map(item => item.id)) + 1 : 1,
name: itemName
};
dataItems.push(newItem);
localStorage.setItem(localStorageKey, JSON.stringify(dataItems));
console.log("新增的來訪事由資料:", newItem);
alert("來訪事由資料已新增");
window.location.href = "purpose_list.html";
});
</script>
</body>
</html>

View File

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>編輯來訪事由資料</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">編輯來訪事由資料</h2>
<form id="editForm">
<div class="mb-3">
<label for="itemName" class="form-label">事由名稱</label>
<input type="text" class="form-control" id="itemName" required>
</div>
<button type="submit" class="btn btn-primary">儲存變更</button>
<a href="purpose_list.html" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
<script>
const localStorageKey = 'purposesData'; // Changed Key
let currentItemId = null;
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
const results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
document.addEventListener('DOMContentLoaded', function() {
currentItemId = parseInt(getUrlParameter('id'));
if (isNaN(currentItemId)) {
alert("無效的事由 ID");
window.location.href = "purpose_list.html";
return;
}
let dataItems = JSON.parse(localStorage.getItem(localStorageKey)) || [];
const itemToEdit = dataItems.find(item => item.id === currentItemId);
if (itemToEdit) {
document.getElementById("itemName").value = itemToEdit.name;
} else {
alert("找不到要編輯的事由資料");
window.location.href = "purpose_list.html";
}
});
document.getElementById("editForm").addEventListener("submit", function (e) {
e.preventDefault();
const updatedItemName = document.getElementById("itemName").value;
if (!updatedItemName.trim()) {
alert("事由名稱不能為空!");
return;
}
let dataItems = JSON.parse(localStorage.getItem(localStorageKey)) || [];
if (dataItems.some(item => item.id !== currentItemId && item.name.toLowerCase() === updatedItemName.toLowerCase())) {
alert("此事由名稱已存在!");
return;
}
const itemIndex = dataItems.findIndex(item => item.id === currentItemId);
if (itemIndex > -1) {
dataItems[itemIndex].name = updatedItemName;
localStorage.setItem(localStorageKey, JSON.stringify(dataItems));
console.log("更新後的來訪事由資料:", dataItems[itemIndex]);
alert("來訪事由資料已更新");
} else {
alert("更新失敗,找不到資料");
}
window.location.href = "purpose_list.html";
});
</script>
</body>
</html>

204
Backstage/purpose_list.html Normal file
View File

@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>來訪事由列表</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
}
.container.mt-4 {
flex-grow: 1;
}
</style>
</head>
<body>
<div class="container mt-4">
<h2 class="mb-4">來訪事由管理 (會顯示於訪客登記的事由建議選單)</h2>
<div class="row g-2 mb-3">
<div class="col-auto">
<a href="purpose_add.html" class="btn btn-outline-success rounded-pill">新增事由</a>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-sm-8 col-md-5 col-lg-4">
<input type="text" id="searchName" class="form-control" placeholder="搜尋事由名稱">
</div>
<div class="col-auto">
<button class="btn btn-outline-primary w-100 rounded-pill" onclick="performSearch()">搜尋</button>
</div>
<div class="col-auto">
<button class="btn btn-outline-danger w-100 rounded-pill" onclick="deleteSelected()">刪除勾選</button>
</div>
</div>
<table class="table table-bordered table-hover align-middle">
<thead class="table-dark">
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleAll(this)"></th>
<th>事由名稱</th>
<th>操作</th>
</tr>
</thead>
<tbody id="dataTable"></tbody>
</table>
<div class="d-flex justify-content-between align-items-center mt-3">
<div><span id="currentPageInfo">1</span> 頁,共 <span id="totalPagesInfo">1</span></div>
<div>
<button id="prevPageBtn" class="btn btn-sm btn-outline-secondary me-1" onclick="prevPage()">上一頁</button>
<button id="nextPageBtn" class="btn btn-sm btn-outline-secondary" onclick="nextPage()">下一頁</button>
</div>
</div>
</div>
<script>
const pageSize = 5;
let currentPage = 1;
let currentFilteredData = [];
const localStorageKey = 'purposesData'; // Changed Key
let dataItems = [];
function loadData() {
const storedData = localStorage.getItem(localStorageKey);
if (storedData) {
dataItems = JSON.parse(storedData);
} else {
// Default data if localStorage is empty
dataItems = [
{ id: 1, name: "拜訪親友" },
{ id: 2, name: "洽談公務" },
{ id: 3, name: "廠商會議" },
{ id: 4, name: "送貨" },
{ id: 5, name: "收貨" },
{ id: 6, name: "維修服務" },
{ id: 7, name: "估價" },
{ id: 8, name: "社區服務" },
{ id: 9, name: "面試/應徵" },
{ id: 10, name: "送餐" },
{ id: 11, name: "包裹寄送" },
{ id: 12, name: "其他" }
];
localStorage.setItem(localStorageKey, JSON.stringify(dataItems));
}
}
function saveData() {
localStorage.setItem(localStorageKey, JSON.stringify(dataItems));
}
function renderTable(dataToRender) {
const tbody = document.getElementById("dataTable");
tbody.innerHTML = "";
const start = (currentPage - 1) * pageSize;
const pageData = dataToRender.slice(start, start + pageSize);
pageData.forEach(item => {
const row = document.createElement("tr");
row.innerHTML = `
<td><input type="checkbox" class="row-checkbox" data-id="${item.id}"></td>
<td>${item.name}</td>
<td>
<a href="purpose_edit.html?id=${item.id}" class="btn btn-outline-secondary btn-sm">編輯</a>
<button class="btn btn-outline-danger btn-sm" onclick="deleteItem(${item.id})">刪除</button>
</td>
`;
tbody.appendChild(row);
});
updatePaginationControls(dataToRender.length);
if (document.getElementById("selectAll")) {
document.getElementById("selectAll").checked = false;
}
}
function updatePaginationControls(totalItems) {
const totalPages = Math.ceil(totalItems / pageSize) || 1;
const currentPageDisplay = document.getElementById("currentPageInfo");
const totalPagesDisplay = document.getElementById("totalPagesInfo");
const prevButton = document.getElementById("prevPageBtn");
const nextButton = document.getElementById("nextPageBtn");
if (currentPageDisplay) currentPageDisplay.textContent = currentPage;
if (totalPagesDisplay) totalPagesDisplay.textContent = totalPages;
if (prevButton) prevButton.disabled = currentPage === 1;
if (nextButton) nextButton.disabled = currentPage === totalPages || totalItems === 0;
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
renderTable(currentFilteredData);
} else if (totalItems === 0 && currentPage !==1) {
currentPage = 1;
renderTable(currentFilteredData);
} else if (totalItems === 0) {
if (currentPageDisplay) currentPageDisplay.textContent = 1;
if (totalPagesDisplay) totalPagesDisplay.textContent = 1;
if (prevButton) prevButton.disabled = true;
if (nextButton) nextButton.disabled = true;
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function nextPage() {
const totalPages = Math.ceil(currentFilteredData.length / pageSize);
if (currentPage < totalPages) {
currentPage++;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function performSearch() {
loadData();
const nameInput = document.getElementById("searchName").value.toLowerCase();
currentFilteredData = dataItems.filter(item =>
item.name.toLowerCase().includes(nameInput)
);
currentPage = 1;
renderTable(currentFilteredData);
}
function toggleAll(source) {
const checkboxes = document.querySelectorAll(".row-checkbox");
checkboxes.forEach(cb => cb.checked = source.checked);
}
function deleteSelected() {
if (!confirm("確定要刪除勾選的事由資料嗎?")) return;
const selected = document.querySelectorAll(".row-checkbox:checked");
if (selected.length === 0) {
alert("請先勾選要刪除的事由資料");
return;
}
const idsToDelete = Array.from(selected).map(cb => parseInt(cb.dataset.id));
dataItems = dataItems.filter(item => !idsToDelete.includes(item.id));
saveData();
performSearch();
}
function deleteItem(id) {
if (!confirm("確定要刪除此筆事由資料嗎?")) return;
dataItems = dataItems.filter(item => item.id !== id);
saveData();
performSearch();
}
document.addEventListener('DOMContentLoaded', performSearch);
</script>
</body>
</html>

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>新增快速訊息</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">新增快速訊息</h2>
<form id="createMessageForm">
<div class="mb-3">
<label for="messageContent" class="form-label">訊息內容</label>
<textarea class="form-control" id="messageContent" rows="3" required></textarea>
</div>
<button type="submit" class="btn btn-success">新增訊息</button>
<a href="quick_message_list.html" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
<script>
document.getElementById("createMessageForm").addEventListener("submit", function (e) {
e.preventDefault();
const newMessage = {
content: document.getElementById("messageContent").value
};
// In a real application, you would generate a unique ID here or get it from the backend.
// For demonstration, we'll assume the list page's script handles ID generation
// or can work without it initially if data is passed via localStorage.
// This example relies on the list page to manage its data array.
// To persist, you'd use localStorage or an API.
// Example: If managing via localStorage (ensure quick_message_list.html also uses this)
// let messages = JSON.parse(localStorage.getItem('quickMessages')) || [];
// newMessage.id = messages.length > 0 ? Math.max(...messages.map(m => m.id)) + 1 : 1; // Simple ID generation
// messages.push(newMessage);
// localStorage.setItem('quickMessages', JSON.stringify(messages));
console.log("新增的快速訊息:", newMessage);
alert("快速訊息已新增(此為前端演示,實際需串接後端 API 並更新列表頁資料來源)");
window.location.href = "quick_message_list.html"; // Redirect to the list page
});
</script>
</body>
</html>

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>編輯快速訊息</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">編輯快速訊息</h2>
<form id="editMessageForm">
<div class="mb-3">
<label for="messageContent" class="form-label">訊息內容</label>
<textarea class="form-control" id="messageContent" rows="3" required></textarea>
</div>
<button type="submit" class="btn btn-primary">儲存變更</button>
<a href="quick_message_list.html" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
<script>
// Function to get URL parameters
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
const results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
const messageId = getUrlParameter('id');
let currentMessageContent = "範例快速訊息內容"; // Default content
// This is a simplified placeholder for fetching the message's current content.
// In a real scenario, you'd retrieve this from localStorage or an API using messageId.
// For example, if 'quickMessages' were stored in localStorage by the list page:
// let messages = JSON.parse(localStorage.getItem('quickMessages')) || [];
// const messageToEdit = messages.find(msg => msg.id === parseInt(messageId));
// if (messageToEdit) {
// currentMessageContent = messageToEdit.content;
// } else if (messageId) {
// currentMessageContent = `訊息 #${messageId} (未找到)`; // Fallback
// }
// Populate the form field
document.getElementById("messageContent").value = currentMessageContent;
if (messageId) {
console.log("Attempting to edit message with ID:", messageId);
// In a real application, you would retrieve the existing message's content here.
// For demonstration, if the list page had populated localStorage:
// (Assuming quick_message_list.html initializes and stores 'quickMessages' in localStorage)
// let allMessages = JSON.parse(localStorage.getItem('quickMessages')) || [];
// const foundMessage = allMessages.find(m => m.id === parseInt(messageId));
// if (foundMessage) {
// document.getElementById("messageContent").value = foundMessage.content;
// } else {
// document.getElementById("messageContent").value = "訊息未找到或無法載入";
// alert("無法載入訊息內容。");
// }
// For this standalone demo without localStorage pre-population by the list page,
// it will use the default or a generic ID-based placeholder.
// The key is that the list page (quick_message_list.html) would be the source of truth for data.
}
document.getElementById("editMessageForm").addEventListener("submit", function (e) {
e.preventDefault();
const updatedMessage = {
id: messageId ? parseInt(messageId) : null, // Keep the ID
content: document.getElementById("messageContent").value
};
// In a real application, send this data to a backend or update localStorage.
// For example, with localStorage:
// let messages = JSON.parse(localStorage.getItem('quickMessages')) || [];
// const messageIndex = messages.findIndex(msg => msg.id === updatedMessage.id);
// if (messageIndex > -1) {
// messages[messageIndex] = updatedMessage;
// localStorage.setItem('quickMessages', JSON.stringify(messages));
// }
console.log("更新後的快速訊息:", updatedMessage);
alert("快速訊息已更新(此為前端演示,實際需串接後端 API 並更新列表頁資料來源)");
window.location.href = "quick_message_list.html";
});
</script>
</body>
</html>

View File

@ -0,0 +1,219 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>快速訊息列表</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
}
.container.mt-4 {
flex-grow: 1;
}
</style>
</head>
<body>
<div class="container mt-4">
<h2 class="mb-4">快速訊息管理 (罐頭訊息)</h2>
<div class="row g-2 mb-3">
<div class="col-auto">
<a href="quick_message_add.html" class="btn btn-outline-success rounded-pill">新增訊息</a>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-sm-8 col-md-5 col-lg-4">
<input type="text" id="searchMessage" class="form-control" placeholder="搜尋訊息內容">
</div>
<div class="col-auto">
<button class="btn btn-outline-primary w-100 rounded-pill" onclick="performSearch()">搜尋</button>
</div>
<div class="col-auto">
<button class="btn btn-outline-danger w-100 rounded-pill" onclick="deleteSelected()">刪除勾選</button>
</div>
</div>
<table class="table table-bordered table-hover align-middle">
<thead class="table-dark">
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleAll(this)"></th>
<th>訊息內容</th>
<th>操作</th>
</tr>
</thead>
<tbody id="messageTable"></tbody>
</table>
<div class="d-flex justify-content-between align-items-center mt-3">
<div><span id="currentPageInfo">1</span> 頁,共 <span id="totalPagesInfo">1</span></div>
<div>
<button id="prevPageBtn" class="btn btn-sm btn-outline-secondary me-1" onclick="prevPage()">上一頁</button>
<button id="nextPageBtn" class="btn btn-sm btn-outline-secondary" onclick="nextPage()">下一頁</button>
</div>
</div>
</div>
<script>
const pageSize = 5;
let currentPage = 1;
let currentFilteredData = [];
// Sample data for quick messages
let quickMessages = [
{ id: 1, content: "您好,請問有什麼可以為您服務的嗎?" },
{ id: 2, content: "感謝您的來電,我們將盡快處理您的問題。" },
{ id: 3, content: "請提供您的訂單編號,以便我們查詢。" },
{ id: 4, content: "祝您有個美好的一天!" },
{ id: 5, content: "我們的上班時間是週一至週五上午9點到下午6點。" },
{ id: 6, content: "很抱歉,目前線路忙碌中,請稍後再試。" },
{ id: 7, content: "您可以參考我們的網站以獲取更多資訊。" },
{ id: 8, content: "謝謝您的耐心等候。" },
{ id: 9, content: "若有其他問題,歡迎隨時提出。" },
{ id: 10, content: "系統維護中,預計一小時後恢復服務。" }
];
function renderTable(dataToRender) {
const tbody = document.getElementById("messageTable");
tbody.innerHTML = "";
const start = (currentPage - 1) * pageSize;
const pageData = dataToRender.slice(start, start + pageSize);
pageData.forEach(item => {
const row = document.createElement("tr");
// For longer messages, you might want to truncate or use a tooltip.
// Here we display it directly. Consider how long messages impact table layout.
const displayContent = item.content.length > 50 ? item.content.substring(0, 47) + "..." : item.content;
row.innerHTML = `
<td><input type="checkbox" class="row-checkbox" data-id="${item.id}"></td>
<td title="${item.content}">${displayContent}</td>
<td>
<a href="quick_message_edit.html?id=${item.id}" class="btn btn-outline-secondary btn-sm">編輯</a>
<button class="btn btn-outline-danger btn-sm" onclick="deleteMessage(${item.id})">刪除</button>
</td>
`;
tbody.appendChild(row);
});
updatePaginationControls(dataToRender.length);
}
function updatePaginationControls(totalItems) {
const totalPages = Math.ceil(totalItems / pageSize) || 1;
const currentPageDisplay = document.getElementById("currentPageInfo");
const totalPagesDisplay = document.getElementById("totalPagesInfo");
const prevButton = document.getElementById("prevPageBtn");
const nextButton = document.getElementById("nextPageBtn");
if (currentPageDisplay) currentPageDisplay.textContent = currentPage;
if (totalPagesDisplay) totalPagesDisplay.textContent = totalPages;
if (prevButton) prevButton.disabled = currentPage === 1;
if (nextButton) nextButton.disabled = currentPage === totalPages || totalItems === 0;
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
renderTable(currentFilteredData);
} else if (totalItems === 0) {
if (currentPageDisplay) currentPageDisplay.textContent = 1;
if (totalPagesDisplay) totalPagesDisplay.textContent = 1;
if (prevButton) prevButton.disabled = true;
if (nextButton) nextButton.disabled = true;
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function nextPage() {
const totalPages = Math.ceil(currentFilteredData.length / pageSize);
if (currentPage < totalPages) {
currentPage++;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function performSearch() {
const searchInput = document.getElementById("searchMessage").value.toLowerCase();
currentFilteredData = quickMessages.filter(item =>
item.content.toLowerCase().includes(searchInput)
);
currentPage = 1;
renderTable(currentFilteredData);
}
function toggleAll(source) {
const checkboxes = document.querySelectorAll(".row-checkbox");
checkboxes.forEach(cb => cb.checked = source.checked);
}
function deleteSelected() {
if (!confirm("確定要刪除勾選的快速訊息嗎?")) return;
const selected = document.querySelectorAll(".row-checkbox:checked");
if (selected.length === 0) {
alert("請先勾選要刪除的訊息");
return;
}
const idsToDelete = Array.from(selected).map(cb => parseInt(cb.dataset.id));
quickMessages = quickMessages.filter(item => !idsToDelete.includes(item.id));
// Re-filter based on current search term
const searchInput = document.getElementById("searchMessage").value.toLowerCase();
currentFilteredData = quickMessages.filter(item =>
item.content.toLowerCase().includes(searchInput)
);
const totalPages = Math.ceil(currentFilteredData.length / pageSize) || 1;
if (currentPage > totalPages) {
currentPage = totalPages;
}
if (currentFilteredData.length === 0) {
currentPage = 1;
}
renderTable(currentFilteredData);
document.getElementById("selectAll").checked = false;
}
function deleteMessage(id) {
if (!confirm("確定要刪除此筆快速訊息嗎?")) return;
quickMessages = quickMessages.filter(item => item.id !== id);
const searchInput = document.getElementById("searchMessage").value.toLowerCase();
currentFilteredData = quickMessages.filter(item =>
item.content.toLowerCase().includes(searchInput)
);
const itemsOnCurrentPageAfterDelete = currentFilteredData.slice((currentPage - 1) * pageSize, currentPage * pageSize).length;
const totalPages = Math.ceil(currentFilteredData.length / pageSize) || 1;
if (itemsOnCurrentPageAfterDelete === 0 && currentPage > 1) {
currentPage--;
} else if (currentPage > totalPages) {
currentPage = totalPages;
}
if (currentFilteredData.length === 0) {
currentPage = 1;
}
renderTable(currentFilteredData);
}
// Initial load: Simulate fetching/loading data
// In a real app, this data might come from localStorage or an API
// For now, we directly use the quickMessages array and apply any existing search filter (which is none on init)
performSearch();
</script>
</body>
</html>

View File

@ -180,15 +180,22 @@
</li>
<li onclick="requestNavigation('some_page1.html', this)">出入管理</li>
<li onclick="requestNavigation('message_list.html', this)">
<div class="li-content">
<span>訊息通知</span>
<span class="badge" data-count="99">99</span>
<span class="submenu-arrow">&emsp;</span>
<li class="has-submenu">
<div class="submenu-toggle" onclick="toggleSubmenu(this)">
<span>居民訊息</span>
<span class="parent-badge"></span>
<span class="submenu-arrow"></span>
</div>
<ul>
<li onclick="requestNavigation('message_list.html', this)">
<div class="li-content">
<span>居民訊息</span>
<span class="badge" data-count="99">99</span>
</div>
</li>
<li onclick="requestNavigation('quick_message_list.html', this)">快速訊息</li>
</ul>
</li>
<li class="has-submenu">
<div class="submenu-toggle" onclick="toggleSubmenu(this)">
<span>水電報修</span>
@ -230,8 +237,43 @@
<li onclick="requestNavigation('mailing_firm_list.html', this)">郵寄廠商</li>
</ul>
</li>
<li onclick="requestNavigation('some_page6.html', this)">報表匯出</li>
<li onclick="requestNavigation('some_page7.html', this)">設定</li>
<li class="has-submenu">
<div class="submenu-toggle" onclick="toggleSubmenu(this)">
<span>訪客管理</span>
<span class="parent-badge"></span>
<span class="submenu-arrow"></span>
</div>
<ul>
<li onclick="requestNavigation('visitor_list.html', this)">訪客管理</li>
<li onclick="requestNavigation('visitor_role_list.html', this)">訪客身分</li>
<li onclick="requestNavigation('purpose_list.html', this)">訪客事由</li>
</ul>
</li>
<li class="has-submenu">
<div class="submenu-toggle" onclick="toggleSubmenu(this)">
<span>公告管理</span>
<span class="parent-badge"></span>
<span class="submenu-arrow"></span>
</div>
<ul>
<li onclick="requestNavigation('announcement_list.html', this)">公告管理</li>
<li onclick="requestNavigation('announcement_title_list.html', this)">公告標題</li>
</ul>
</li>
<li class="has-submenu">
<div class="submenu-toggle" onclick="toggleSubmenu(this)">
<span>繳費通知</span>
<span class="parent-badge"></span>
<span class="submenu-arrow"></span>
</div>
<ul>
<li onclick="requestNavigation('fee_list.html', this)">繳費通知</li>
<li onclick="requestNavigation('payment_item_list.html', this)">繳費項目</li>
</ul>
</li>
<li onclick="requestNavigation('admin_list.html', this)">管理員設定</li>
<li onclick="requestNavigation('community_edit.html', this)">設定</li>
<li onclick="window.top.location.href='login.html';" style="cursor:pointer;">登出</li>
</ul>
</div>

293
Backstage/visitor_add.html Normal file
View File

@ -0,0 +1,293 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>登記新訪客</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.suggestions-container {
position: relative;
}
.suggestions-box {
position: absolute;
border: 1px solid #ced4da;
border-top: none;
max-height: 200px;
overflow-y: auto;
background-color: white;
width: 100%;
z-index: 1000;
border-radius: 0 0 0.25rem 0.25rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.suggestion-item {
padding: 0.5rem 0.75rem;
cursor: pointer;
}
.suggestion-item:hover, .suggestion-item.active {
background-color: #e9ecef;
}
.suggestions-box:empty {
display: none;
}
</style>
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">登記新訪客</h2>
<form id="addVisitorLogForm">
<div class="row">
<div class="col-md-6 mb-3">
<label for="visitDate" class="form-label">來訪日期</label>
<input type="date" class="form-control" id="visitDate" required>
</div>
<div class="col-md-6 mb-3">
<label for="entryTime" class="form-label">進入時間</label>
<input type="time" class="form-control" id="entryTime" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3 suggestions-container">
<label for="visitorName" class="form-label">訪客姓名</label>
<input type="text" class="form-control" id="visitorName" placeholder="輸入訪客姓名" required>
</div>
<div class="col-md-6 mb-3 suggestions-container">
<label for="visitorRole" class="form-label">訪客身分 (可輸入搜尋)</label>
<input type="text" class="form-control" id="visitorRole" placeholder="例如:朋友、廠商、外送員" required autocomplete="off">
<div id="suggestionsBoxVisitorRole" class="suggestions-box"></div>
</div>
</div>
<div class="mb-3 suggestions-container">
<label for="visitedUnit" class="form-label">拜訪對象/單位 (可輸入搜尋)</label>
<input type="text" class="form-control" id="visitedUnit" placeholder="輸入拜訪對象/單位開始搜尋" required autocomplete="off">
<div id="suggestionsBoxVisitedUnit" class="suggestions-box"></div>
</div>
<div class="mb-3 suggestions-container">
<label for="purpose" class="form-label">來訪事由 (可輸入搜尋)</label>
<input type="text" class="form-control" id="purpose" placeholder="輸入來訪事由" required autocomplete="off">
<div id="suggestionsBoxPurpose" class="suggestions-box"></div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="exitTime" class="form-label">離開時間 (選填)</label>
<input type="time" class="form-control" id="exitTime">
</div>
<div class="col-md-6 mb-3">
<label for="status" class="form-label">狀態</label>
<select class="form-select" id="status" required>
<option value="已進入" selected>已進入</option>
<option value="已離開">已離開</option>
</select>
</div>
</div>
<div class="mb-3">
<label for="notes" class="form-label">備註</label>
<textarea class="form-control" id="notes" rows="3" placeholder="輸入其他備註事項 (選填)"></textarea>
</div>
<button type="submit" class="btn btn-primary">儲存來訪紀錄</button>
<a href="visitor_list.html" class="btn btn-secondary ms-2">返回列表</a>
</form>
</div>
<script>
const allUnitsOrResidents = [
"林小安 (A101)", "陳大明 (B203)", "王小美 (C502)", "李四 (A102)", "張三 (D301)",
"趙六 (B205)", "孫七 (E101)", "吳八金 (F405)", "鄭九玲 (G1201)", "周十全 (H707)",
"管理委員會", "社區服務中心", "訪客登記處", "保全室"
];
const allVisitorRoles = [
"朋友", "親戚", "廠商", "外送員 (Foodpanda)", "外送員 (Uber Eats)", "外送員 (Lalamove)",
"快遞員 (黑貓宅急便)", "快遞員 (嘉里大榮)", "快遞員 (郵局)", "快遞員 (宅配通)",
"維修人員 (水電)", "維修人員 (冷氣)", "維修人員 (電器)", "維修人員 (網路/第四台)", "維修人員 (其他)",
"清潔人員", "業務代表", "應徵者", "政府稽查員", "其他"
];
const allPurposes = [
"拜訪親友", "洽談公務", "廠商會議", "送貨", "收貨", "維修服務", "估價", "社區服務",
"面試/應徵", "送餐", "包裹寄送", "抄表 (水/電/瓦斯)", "稽查", "其他"
];
const visitedUnitInput = document.getElementById('visitedUnit');
const suggestionsBoxVisitedUnit = document.getElementById('suggestionsBoxVisitedUnit');
const visitorRoleInput = document.getElementById('visitorRole');
const suggestionsBoxVisitorRole = document.getElementById('suggestionsBoxVisitorRole');
const purposeInput = document.getElementById('purpose');
const suggestionsBoxPurpose = document.getElementById('suggestionsBoxPurpose');
function setupSuggestions(inputElement, suggestionsBoxElement, sourceArray) {
let currentActiveIndex = -1;
inputElement.addEventListener('input', function() {
const inputText = this.value.toLowerCase();
suggestionsBoxElement.innerHTML = '';
currentActiveIndex = -1;
if (inputText.length === 0 && sourceArray.includes("其他")) { // Show "其他" if empty and "其他" is an option
// Or don't show anything if input is empty, this is optional
} else if (inputText.length === 0) {
suggestionsBoxElement.style.display = 'none';
return;
}
const filteredItems = sourceArray.filter(item =>
item.toLowerCase().includes(inputText)
);
if (filteredItems.length > 0) {
filteredItems.forEach((itemText, index) => {
const item = document.createElement('div');
item.classList.add('suggestion-item');
item.textContent = itemText;
item.addEventListener('click', function() {
inputElement.value = itemText;
suggestionsBoxElement.innerHTML = '';
suggestionsBoxElement.style.display = 'none';
});
item.setAttribute('data-index', index);
suggestionsBoxElement.appendChild(item);
});
suggestionsBoxElement.style.display = 'block';
} else {
suggestionsBoxElement.style.display = 'none';
}
});
inputElement.addEventListener('keydown', function(e) {
const items = suggestionsBoxElement.querySelectorAll('.suggestion-item');
if (items.length === 0 || suggestionsBoxElement.style.display === 'none') return;
if (e.key === 'ArrowDown') {
e.preventDefault();
currentActiveIndex = (currentActiveIndex + 1) % items.length;
} else if (e.key === 'ArrowUp') {
e.preventDefault();
currentActiveIndex = (currentActiveIndex - 1 + items.length) % items.length;
} else if (e.key === 'Enter') {
e.preventDefault();
if (currentActiveIndex > -1 && items[currentActiveIndex]) {
items[currentActiveIndex].click();
} else {
// If no suggestion is active, allow form submission or move to next field
// For now, let form submission be handled by the main submit button
}
return;
} else if (e.key === 'Escape') {
suggestionsBoxElement.innerHTML = '';
suggestionsBoxElement.style.display = 'none';
currentActiveIndex = -1;
}
updateActiveSuggestion(items, currentActiveIndex);
});
}
function updateActiveSuggestion(items, activeIndex) {
items.forEach(item => item.classList.remove('active'));
if (items[activeIndex]) {
items[activeIndex].classList.add('active');
items[activeIndex].scrollIntoView({ block: 'nearest' });
}
}
setupSuggestions(visitedUnitInput, suggestionsBoxVisitedUnit, allUnitsOrResidents);
setupSuggestions(visitorRoleInput, suggestionsBoxVisitorRole, allVisitorRoles);
setupSuggestions(purposeInput, suggestionsBoxPurpose, allPurposes);
document.addEventListener('click', function(event) {
const suggestionInputs = [visitedUnitInput, visitorRoleInput, purposeInput];
const suggestionBoxes = [suggestionsBoxVisitedUnit, suggestionsBoxVisitorRole, suggestionsBoxPurpose];
let clickedInsideASuggestionArea = false;
for (let i = 0; i < suggestionInputs.length; i++) {
if ((suggestionInputs[i] && suggestionInputs[i].contains(event.target)) ||
(suggestionBoxes[i] && suggestionBoxes[i].contains(event.target))) {
clickedInsideASuggestionArea = true;
break;
}
}
if (!clickedInsideASuggestionArea) {
suggestionBoxes.forEach(box => {
if (box) {
box.innerHTML = '';
box.style.display = 'none';
}
});
}
});
document.addEventListener('DOMContentLoaded', (event) => {
const today = new Date();
const visitDateInput = document.getElementById('visitDate');
if(visitDateInput) {
visitDateInput.value = today.toISOString().split('T')[0];
}
const entryTimeInput = document.getElementById('entryTime');
if(entryTimeInput){
const hours = String(today.getHours()).padStart(2, '0');
const minutes = String(today.getMinutes()).padStart(2, '0');
entryTimeInput.value = `${hours}:${minutes}`;
}
});
document.getElementById('addVisitorLogForm').addEventListener('submit', function(event) {
event.preventDefault();
const newVisitorLog = {
id: Date.now(),
visitDate: document.getElementById('visitDate').value,
entryTime: document.getElementById('entryTime').value,
exitTime: document.getElementById('exitTime').value,
visitorName: document.getElementById('visitorName').value,
visitorRole: visitorRoleInput.value,
visitedUnit: visitedUnitInput.value,
purpose: purposeInput.value,
status: document.getElementById('status').value,
notes: document.getElementById('notes').value
};
console.log("新訪客來訪紀錄:", newVisitorLog);
let existingLogs = JSON.parse(localStorage.getItem('visitors_log_data_temp')) || [];
existingLogs.push(newVisitorLog);
localStorage.setItem('visitors_log_data_temp', JSON.stringify(existingLogs));
alert("訪客來訪紀錄已儲存!\n訪客" + newVisitorLog.visitorName + "\n拜訪對象" + newVisitorLog.visitedUnit);
this.reset();
const today = new Date();
const visitDateInput = document.getElementById('visitDate');
if(visitDateInput) {
visitDateInput.value = today.toISOString().split('T')[0];
}
const entryTimeInput = document.getElementById('entryTime');
if(entryTimeInput){
const hours = String(today.getHours()).padStart(2, '0');
const minutes = String(today.getMinutes()).padStart(2, '0');
entryTimeInput.value = `${hours}:${minutes}`;
}
document.getElementById('status').value = "已進入";
[suggestionsBoxVisitedUnit, suggestionsBoxVisitorRole, suggestionsBoxPurpose].forEach(box => {
if (box) {
box.innerHTML = '';
box.style.display = 'none';
}
});
document.getElementById('visitorName').focus();
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

297
Backstage/visitor_edit.html Normal file
View File

@ -0,0 +1,297 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>編輯訪客來訪紀錄</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.suggestions-container {
position: relative;
}
.suggestions-box {
position: absolute;
border: 1px solid #ced4da;
border-top: none;
max-height: 200px;
overflow-y: auto;
background-color: white;
width: 100%;
z-index: 1000;
border-radius: 0 0 0.25rem 0.25rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.suggestion-item {
padding: 0.5rem 0.75rem;
cursor: pointer;
}
.suggestion-item:hover, .suggestion-item.active {
background-color: #e9ecef;
}
.suggestions-box:empty {
display: none;
}
</style>
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">編輯訪客來訪紀錄</h2>
<form id="editVisitorLogForm">
<input type="hidden" id="visitorLogId" name="visitorLogId">
<div class="row">
<div class="col-md-6 mb-3">
<label for="visitDate" class="form-label">來訪日期</label>
<input type="date" class="form-control" id="visitDate" required>
</div>
<div class="col-md-6 mb-3">
<label for="entryTime" class="form-label">進入時間</label>
<input type="time" class="form-control" id="entryTime" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3 suggestions-container">
<label for="visitorName" class="form-label">訪客姓名</label>
<input type="text" class="form-control" id="visitorName" placeholder="輸入訪客姓名" required>
</div>
<div class="col-md-6 mb-3 suggestions-container">
<label for="visitorRole" class="form-label">訪客身分 (可輸入搜尋)</label>
<input type="text" class="form-control" id="visitorRole" placeholder="例如:朋友、廠商、外送員" required autocomplete="off">
<div id="suggestionsBoxVisitorRole" class="suggestions-box"></div>
</div>
</div>
<div class="mb-3 suggestions-container">
<label for="visitedUnit" class="form-label">拜訪對象/單位 (可輸入搜尋)</label>
<input type="text" class="form-control" id="visitedUnit" placeholder="輸入拜訪對象/單位開始搜尋" required autocomplete="off">
<div id="suggestionsBoxVisitedUnit" class="suggestions-box"></div>
</div>
<div class="mb-3 suggestions-container">
<label for="purpose" class="form-label">來訪事由 (可輸入搜尋)</label>
<input type="text" class="form-control" id="purpose" placeholder="輸入來訪事由" required autocomplete="off">
<div id="suggestionsBoxPurpose" class="suggestions-box"></div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="exitTime" class="form-label">離開時間 (選填)</label>
<input type="time" class="form-control" id="exitTime">
</div>
<div class="col-md-6 mb-3">
<label for="status" class="form-label">狀態</label>
<select class="form-select" id="status" required>
<option value="已進入">已進入</option>
<option value="已離開">已離開</option>
</select>
</div>
</div>
<div class="mb-3">
<label for="notes" class="form-label">備註</label>
<textarea class="form-control" id="notes" rows="3" placeholder="輸入其他備註事項 (選填)"></textarea>
</div>
<button type="submit" class="btn btn-primary">更新來訪紀錄</button>
<a href="visitor_list.html" class="btn btn-secondary ms-2">返回列表</a>
</form>
</div>
<script>
let currentEditingVisitorLog = {};
const allUnitsOrResidents = [
"林小安 (A101)", "陳大明 (B203)", "王小美 (C502)", "李四 (A102)", "張三 (D301)",
"趙六 (B205)", "孫七 (E101)", "吳八金 (F405)", "鄭九玲 (G1201)", "周十全 (H707)",
"管理委員會", "社區服務中心", "訪客登記處", "保全室", "測試訪客目標 (Z999)"
];
const allVisitorRoles = [
"朋友", "親戚", "廠商", "外送員 (Foodpanda)", "外送員 (Uber Eats)", "外送員 (Lalamove)",
"快遞員 (黑貓宅急便)", "快遞員 (嘉里大榮)", "快遞員 (郵局)", "快遞員 (宅配通)",
"維修人員 (水電)", "維修人員 (冷氣)", "維修人員 (電器)", "維修人員 (網路/第四台)", "維修人員 (其他)",
"清潔人員", "業務代表", "應徵者", "政府稽查員", "其他"
];
const allPurposes = [
"拜訪親友", "洽談公務", "廠商會議", "送貨", "收貨", "維修服務", "估價", "社區服務",
"面試/應徵", "送餐", "包裹寄送", "抄表 (水/電/瓦斯)", "稽查", "其他"
];
const visitedUnitInput = document.getElementById('visitedUnit');
const suggestionsBoxVisitedUnit = document.getElementById('suggestionsBoxVisitedUnit');
const visitorRoleInput = document.getElementById('visitorRole');
const suggestionsBoxVisitorRole = document.getElementById('suggestionsBoxVisitorRole');
const purposeInput = document.getElementById('purpose');
const suggestionsBoxPurpose = document.getElementById('suggestionsBoxPurpose');
function setupSuggestions(inputElement, suggestionsBoxElement, sourceArray) {
let currentActiveIndex = -1;
inputElement.addEventListener('input', function() {
const inputText = this.value.toLowerCase();
suggestionsBoxElement.innerHTML = '';
currentActiveIndex = -1;
if (inputText.length === 0) {
suggestionsBoxElement.style.display = 'none';
return;
}
const filteredItems = sourceArray.filter(item =>
item.toLowerCase().includes(inputText)
);
if (filteredItems.length > 0) {
filteredItems.forEach((itemText, index) => {
const item = document.createElement('div');
item.classList.add('suggestion-item');
item.textContent = itemText;
item.addEventListener('click', function() {
inputElement.value = itemText;
suggestionsBoxElement.innerHTML = '';
suggestionsBoxElement.style.display = 'none';
});
item.setAttribute('data-index', index);
suggestionsBoxElement.appendChild(item);
});
suggestionsBoxElement.style.display = 'block';
} else {
suggestionsBoxElement.style.display = 'none';
}
});
inputElement.addEventListener('keydown', function(e) {
const items = suggestionsBoxElement.querySelectorAll('.suggestion-item');
if (items.length === 0 || suggestionsBoxElement.style.display === 'none') return;
if (e.key === 'ArrowDown') {
e.preventDefault();
currentActiveIndex = (currentActiveIndex + 1) % items.length;
} else if (e.key === 'ArrowUp') {
e.preventDefault();
currentActiveIndex = (currentActiveIndex - 1 + items.length) % items.length;
} else if (e.key === 'Enter') {
e.preventDefault();
if (currentActiveIndex > -1 && items[currentActiveIndex]) {
items[currentActiveIndex].click();
} else {
// Allow form submission via main button if no suggestion active
}
return;
} else if (e.key === 'Escape') {
suggestionsBoxElement.innerHTML = '';
suggestionsBoxElement.style.display = 'none';
currentActiveIndex = -1;
}
updateActiveSuggestion(items, currentActiveIndex);
});
}
function updateActiveSuggestion(items, activeIndex) {
items.forEach(item => item.classList.remove('active'));
if (items[activeIndex]) {
items[activeIndex].classList.add('active');
items[activeIndex].scrollIntoView({ block: 'nearest' });
}
}
setupSuggestions(visitedUnitInput, suggestionsBoxVisitedUnit, allUnitsOrResidents);
setupSuggestions(visitorRoleInput, suggestionsBoxVisitorRole, allVisitorRoles);
setupSuggestions(purposeInput, suggestionsBoxPurpose, allPurposes);
document.addEventListener('click', function(event) {
const suggestionInputs = [visitedUnitInput, visitorRoleInput, purposeInput];
const suggestionBoxes = [suggestionsBoxVisitedUnit, suggestionsBoxVisitorRole, suggestionsBoxPurpose];
let clickedInsideASuggestionArea = false;
for (let i = 0; i < suggestionInputs.length; i++) {
if ((suggestionInputs[i] && suggestionInputs[i].contains(event.target)) ||
(suggestionBoxes[i] && suggestionBoxes[i].contains(event.target))) {
clickedInsideASuggestionArea = true;
break;
}
}
if (!clickedInsideASuggestionArea) {
suggestionBoxes.forEach(box => {
if (box) {
box.innerHTML = '';
box.style.display = 'none';
}
});
}
});
document.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const visitorLogIdFromUrl = parseInt(urlParams.get('id'));
let existingLogs = JSON.parse(localStorage.getItem('visitors_log_data_temp')) || [];
const foundLog = existingLogs.find(log => log.id === visitorLogIdFromUrl);
if (foundLog) {
currentEditingVisitorLog = { ...foundLog };
} else {
alert("找不到指定的訪客紀錄!將使用範例資料。 ID: " + visitorLogIdFromUrl);
currentEditingVisitorLog = {
id: 99901,
visitDate: new Date().toISOString().split('T')[0],
entryTime: "12:00",
exitTime: "",
visitorName: "測試訪客",
visitorRole: "測試身分",
visitedUnit: "測試訪客目標 (Z999)",
purpose: "編輯頁面測試",
status: "已進入",
notes: "這是一筆用於編輯頁面測試的訪客備註。"
};
}
document.getElementById('visitorLogId').value = currentEditingVisitorLog.id;
document.getElementById('visitDate').value = currentEditingVisitorLog.visitDate;
document.getElementById('entryTime').value = currentEditingVisitorLog.entryTime;
document.getElementById('exitTime').value = currentEditingVisitorLog.exitTime || "";
document.getElementById('visitorName').value = currentEditingVisitorLog.visitorName;
visitorRoleInput.value = currentEditingVisitorLog.visitorRole || ""; // Use input element directly
visitedUnitInput.value = currentEditingVisitorLog.visitedUnit; // Use input element directly
purposeInput.value = currentEditingVisitorLog.purpose || ""; // Use input element directly
document.getElementById('status').value = currentEditingVisitorLog.status;
document.getElementById('notes').value = currentEditingVisitorLog.notes || "";
});
document.getElementById('editVisitorLogForm').addEventListener('submit', function(event) {
event.preventDefault();
currentEditingVisitorLog.visitDate = document.getElementById('visitDate').value;
currentEditingVisitorLog.entryTime = document.getElementById('entryTime').value;
currentEditingVisitorLog.exitTime = document.getElementById('exitTime').value;
currentEditingVisitorLog.visitorName = document.getElementById('visitorName').value;
currentEditingVisitorLog.visitorRole = visitorRoleInput.value;
currentEditingVisitorLog.visitedUnit = visitedUnitInput.value;
currentEditingVisitorLog.purpose = purposeInput.value;
currentEditingVisitorLog.status = document.getElementById('status').value;
currentEditingVisitorLog.notes = document.getElementById('notes').value;
console.log("更新後訪客紀錄:", currentEditingVisitorLog);
let existingLogs = JSON.parse(localStorage.getItem('visitors_log_data_temp')) || [];
const logIndex = existingLogs.findIndex(log => log.id === currentEditingVisitorLog.id);
if (logIndex > -1) {
existingLogs[logIndex] = { ...currentEditingVisitorLog };
localStorage.setItem('visitors_log_data_temp', JSON.stringify(existingLogs));
alert("訪客紀錄已更新!\n訪客" + currentEditingVisitorLog.visitorName);
} else {
existingLogs.push({ ...currentEditingVisitorLog });
localStorage.setItem('visitors_log_data_temp', JSON.stringify(existingLogs));
alert("訪客紀錄 (ID: " + currentEditingVisitorLog.id + ") 在列表中未找到,已新增至列表!");
}
[suggestionsBoxVisitedUnit, suggestionsBoxVisitorRole, suggestionsBoxPurpose].forEach(box => {
if (box) {
box.innerHTML = '';
box.style.display = 'none';
}
});
// window.location.href = 'visitor_list.html';
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

279
Backstage/visitor_list.html Normal file
View File

@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>訪客來訪紀錄管理</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
}
.container.mt-4 {
flex-grow: 1;
}
</style>
</head>
<body>
<div class="container mt-4">
<h2 class="mb-4">訪客來訪紀錄管理</h2>
<div class="row g-2 mb-3">
<div class="col-auto"> <a href="visitor_add.html" class="btn btn-outline-success rounded-pill px-3">登記新訪客</a> </div>
<div class="col-auto"> <button class="btn btn-outline-secondary rounded-pill px-3" onclick="exportVisitors()">匯出列表</button>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-sm-6 col-md-2 col-lg-2">
<input type="text" id="searchVisitorName" class="form-control" placeholder="搜尋訪客姓名">
</div>
<div class="col-sm-6 col-md-2 col-lg-2">
<input type="text" id="searchVisitorRole" class="form-control" placeholder="搜尋訪客身分">
</div>
<div class="col-sm-6 col-md-3 col-lg-3">
<input type="text" id="searchVisitedUnit" class="form-control" placeholder="搜尋拜訪對象/單位">
</div>
<div class="col-sm-6 col-md-2 col-lg-2">
<select id="searchStatus" class="form-select">
<option value="">所有狀態</option>
<option value="已進入">已進入</option>
<option value="已離開">已離開</option>
</select>
</div>
<div class="col-auto">
<button class="btn btn-outline-primary w-100 rounded-pill" onclick="performSearch()">搜尋</button>
</div>
<div class="col-auto"> <button class="btn btn-outline-danger w-100 rounded-pill" onclick="deleteSelected()">刪除勾選</button>
</div>
</div>
<table class="table table-bordered table-hover align-middle">
<thead class="table-dark">
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleAll(this)"></th>
<th>編號</th>
<th>來訪日期</th>
<th>進入時間</th>
<th>訪客姓名</th>
<th>訪客身分</th>
<th>拜訪對象/單位</th>
<th>來訪事由</th>
<th>狀態</th>
<th>操作</th>
</tr>
</thead>
<tbody id="visitorTable">
</tbody>
</table>
<div class="d-flex justify-content-between align-items-center mt-3">
<div><span id="currentPageInfo">1</span> 頁,共 <span id="totalPagesInfo">1</span></div>
<div>
<button id="prevPageBtn" class="btn btn-sm btn-outline-secondary me-1" onclick="prevPage()">上一頁</button>
<button id="nextPageBtn" class="btn btn-sm btn-outline-secondary" onclick="nextPage()">下一頁</button>
</div>
</div>
</div>
<script>
const pageSize = 5;
let currentPage = 1;
let currentFilteredData = [];
let visitors_data = JSON.parse(localStorage.getItem('visitors_log_data_temp')) || [
{ id: 1, visitDate: "2025/04/22", entryTime: "14:35", exitTime: "15:30", visitorName: "王小明", visitorRole: "朋友", visitedUnit: "林小安 (A101)", purpose: "拜訪林小安", status: "已離開", notes: "同行1人" },
{ id: 2, visitDate: "2025/04/21", entryTime: "10:20", exitTime: "11:00", visitorName: "陳美麗", visitorRole: "水電師傅", visitedUnit: "陳大明 (B203)", purpose: "維修浴室", status: "已離開", notes: "更換水龍頭" },
{ id: 3, visitDate: "2025/04/20", entryTime: "17:45", exitTime: "", visitorName: "李建國", visitorRole: "外送員", visitedUnit: "王小美 (C502)", purpose: "送晚餐 (Uber Eats)", status: "已進入", notes: "餐點A餐" },
{ id: 4, visitDate: "2025/05/28", entryTime: "09:15", exitTime: "", visitorName: "好速配快遞", visitorRole: "送貨員", visitedUnit: "管理委員會", purpose: "文件送達", status: "已進入", notes: "" },
{ id: 5, visitDate: "2025/05/29", entryTime: "11:00", exitTime: "11:05", visitorName: "張太太", visitorRole: "親戚", visitedUnit: "李四 (A102)", purpose: "拿取物品", status: "已離開", notes: "" },
{ id: 6, visitDate: "2025/05/29", entryTime: "16:30", exitTime: "", visitorName: "汎德業務員", visitorRole: "廠商", visitedUnit: "社區服務中心", purpose: "洽談社區服務", status: "已進入", notes: "提供型錄" }
];
if (!localStorage.getItem('visitors_log_data_temp') || JSON.parse(localStorage.getItem('visitors_log_data_temp')).length === 0) {
localStorage.setItem('visitors_log_data_temp', JSON.stringify(visitors_data));
}
function renderTable(dataToRender) {
const tbody = document.getElementById("visitorTable");
tbody.innerHTML = "";
const start = (currentPage - 1) * pageSize;
const pageData = dataToRender.slice(start, start + pageSize);
pageData.forEach(vst => {
const row = document.createElement("tr");
let statusBadgeClass = 'bg-secondary';
switch (vst.status) {
case '已進入':
statusBadgeClass = 'bg-success';
break;
case '已離開':
statusBadgeClass = 'bg-secondary';
break;
}
row.innerHTML = `
<td><input type="checkbox" class="row-checkbox" data-id="${vst.id}"></td>
<td>${vst.id}</td>
<td>${vst.visitDate}</td>
<td>${vst.entryTime}</td>
<td>${vst.visitorName}</td>
<td>${vst.visitorRole || 'N/A'}</td>
<td>${vst.visitedUnit}</td>
<td>${vst.purpose}</td>
<td><span class="badge ${statusBadgeClass}">${vst.status}</span></td>
<td>
<a href="visitor_edit.html?id=${vst.id}" class="btn btn-outline-secondary btn-sm">編輯</a>
<button class="btn btn-outline-danger btn-sm" onclick="deleteVisitor(${vst.id})">刪除</button>
</td>
`;
tbody.appendChild(row);
});
updatePaginationControls(dataToRender.length);
const selectAllCheckbox = document.getElementById("selectAll");
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
}
function updatePaginationControls(totalItems) {
const totalPages = Math.ceil(totalItems / pageSize) || 1;
const currentPageDisplay = document.getElementById("currentPageInfo");
const totalPagesDisplay = document.getElementById("totalPagesInfo");
const prevButton = document.getElementById("prevPageBtn");
const nextButton = document.getElementById("nextPageBtn");
if (currentPageDisplay) currentPageDisplay.textContent = currentPage;
if (totalPagesDisplay) totalPagesDisplay.textContent = totalPages;
if (prevButton) prevButton.disabled = currentPage === 1;
if (nextButton) nextButton.disabled = currentPage === totalPages || totalItems === 0;
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
renderTable(currentFilteredData);
} else if (totalItems === 0) {
if (currentPageDisplay) currentPageDisplay.textContent = 1;
if (totalPagesDisplay) totalPagesDisplay.textContent = 1;
if (prevButton) prevButton.disabled = true;
if (nextButton) nextButton.disabled = true;
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function nextPage() {
const totalPages = Math.ceil(currentFilteredData.length / pageSize);
if (currentPage < totalPages) {
currentPage++;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function performSearch() {
const visitorNameInput = document.getElementById("searchVisitorName").value.toLowerCase();
const visitorRoleInput = document.getElementById("searchVisitorRole").value.toLowerCase();
const visitedUnitInput = document.getElementById("searchVisitedUnit").value.toLowerCase();
const statusInput = document.getElementById("searchStatus").value;
visitors_data = JSON.parse(localStorage.getItem('visitors_log_data_temp')) || [];
currentFilteredData = visitors_data.filter(vst =>
vst.visitorName.toLowerCase().includes(visitorNameInput) &&
(vst.visitorRole || '').toLowerCase().includes(visitorRoleInput) &&
vst.visitedUnit.toLowerCase().includes(visitedUnitInput) &&
(statusInput === "" || vst.status === statusInput)
);
currentPage = 1;
renderTable(currentFilteredData);
}
function toggleAll(source) {
const checkboxes = document.querySelectorAll(".row-checkbox");
checkboxes.forEach(cb => cb.checked = source.checked);
}
function deleteSelected() {
if (!confirm("確定要刪除勾選的訪客紀錄嗎?")) return;
const selected = document.querySelectorAll(".row-checkbox:checked");
if (selected.length === 0) {
alert("請先勾選要刪除的訪客紀錄");
return;
}
const idsToDelete = Array.from(selected).map(cb => parseInt(cb.dataset.id));
visitors_data = visitors_data.filter(vst => !idsToDelete.includes(vst.id));
localStorage.setItem('visitors_log_data_temp', JSON.stringify(visitors_data));
performSearch();
}
function deleteVisitor(id) {
if (!confirm("確定要刪除此訪客紀錄嗎?")) return;
visitors_data = visitors_data.filter(vst => vst.id !== id);
localStorage.setItem('visitors_log_data_temp', JSON.stringify(visitors_data));
performSearch();
}
function exportVisitors() {
const visitorNameInput = document.getElementById("searchVisitorName").value.toLowerCase();
const visitorRoleInput = document.getElementById("searchVisitorRole").value.toLowerCase();
const visitedUnitInput = document.getElementById("searchVisitedUnit").value.toLowerCase();
const statusInput = document.getElementById("searchStatus").value;
let dataToExport = JSON.parse(localStorage.getItem('visitors_log_data_temp')) || [];
if (visitorNameInput || visitorRoleInput || visitedUnitInput || statusInput) {
dataToExport = currentFilteredData;
}
if (dataToExport.length === 0) {
alert("沒有資料可匯出。");
return;
}
let csvContent = "data:text/csv;charset=utf-8,\uFEFF";
csvContent += "編號,來訪日期,進入時間,離開時間,訪客姓名,訪客身分,拜訪對象/單位,來訪事由,狀態,備註\n";
dataToExport.forEach(vst => {
const cleanVisitorName = `"${(vst.visitorName || '').replace(/"/g, '""')}"`;
const cleanVisitorRole = `"${(vst.visitorRole || '').replace(/"/g, '""')}"`;
const cleanVisitedUnit = `"${(vst.visitedUnit || '').replace(/"/g, '""')}"`;
const cleanPurpose = `"${(vst.purpose || '').replace(/"/g, '""')}"`;
const cleanStatus = `"${(vst.status || '').replace(/"/g, '""')}"`;
const cleanNotes = `"${vst.notes ? vst.notes.replace(/"/g, '""') : ''}"`;
const row = [vst.id, vst.visitDate, vst.entryTime, vst.exitTime || '', cleanVisitorName, cleanVisitorRole, cleanVisitedUnit, cleanPurpose, cleanStatus, cleanNotes];
csvContent += row.join(",") + "\n";
});
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "visitors_log_export.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
alert(`準備匯出 ${dataToExport.length} 筆訪客紀錄(${visitorNameInput || visitorRoleInput || visitedUnitInput || statusInput ? "依搜尋條件" : "全部"}`);
}
document.addEventListener('DOMContentLoaded', () => {
performSearch();
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>新增訪客身分</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">新增訪客身分</h2>
<form id="createForm">
<div class="mb-3">
<label for="itemName" class="form-label">身分名稱</label>
<input type="text" class="form-control" id="itemName" required>
</div>
<button type="submit" class="btn btn-success">新增身分</button>
<a href="visitor_role_list.html" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
<script>
document.getElementById("createForm").addEventListener("submit", function (e) {
e.preventDefault();
const localStorageKey = 'visitorRolesData';
const itemName = document.getElementById("itemName").value;
if (!itemName.trim()) {
alert("身分名稱不能為空!");
return;
}
let dataItems = JSON.parse(localStorage.getItem(localStorageKey)) || [];
// Check for duplicates
if (dataItems.some(item => item.name.toLowerCase() === itemName.toLowerCase())) {
alert("此身分名稱已存在!");
return;
}
const newItem = {
// Generate a simple new ID (in a real app, backend would do this)
id: dataItems.length > 0 ? Math.max(...dataItems.map(item => item.id)) + 1 : 1,
name: itemName
};
dataItems.push(newItem);
localStorage.setItem(localStorageKey, JSON.stringify(dataItems));
console.log("新增的訪客身分資料:", newItem);
alert("訪客身分資料已新增");
window.location.href = "visitor_role_list.html";
});
</script>
</body>
</html>

View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>編輯訪客身分資料</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h2 class="mb-4">編輯訪客身分資料</h2>
<form id="editForm">
<div class="mb-3">
<label for="itemName" class="form-label">身分名稱</label>
<input type="text" class="form-control" id="itemName" required>
</div>
<button type="submit" class="btn btn-primary">儲存變更</button>
<a href="visitor_role_list.html" class="btn btn-secondary ms-2">取消</a>
</form>
</div>
<script>
const localStorageKey = 'visitorRolesData';
let currentItemId = null;
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
const results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
document.addEventListener('DOMContentLoaded', function() {
currentItemId = parseInt(getUrlParameter('id'));
if (isNaN(currentItemId)) {
alert("無效的身分 ID");
window.location.href = "visitor_role_list.html";
return;
}
let dataItems = JSON.parse(localStorage.getItem(localStorageKey)) || [];
const itemToEdit = dataItems.find(item => item.id === currentItemId);
if (itemToEdit) {
document.getElementById("itemName").value = itemToEdit.name;
} else {
alert("找不到要編輯的身分資料");
window.location.href = "visitor_role_list.html";
}
});
document.getElementById("editForm").addEventListener("submit", function (e) {
e.preventDefault();
const updatedItemName = document.getElementById("itemName").value;
if (!updatedItemName.trim()) {
alert("身分名稱不能為空!");
return;
}
let dataItems = JSON.parse(localStorage.getItem(localStorageKey)) || [];
// Check for duplicates, excluding the current item being edited
if (dataItems.some(item => item.id !== currentItemId && item.name.toLowerCase() === updatedItemName.toLowerCase())) {
alert("此身分名稱已存在!");
return;
}
const itemIndex = dataItems.findIndex(item => item.id === currentItemId);
if (itemIndex > -1) {
dataItems[itemIndex].name = updatedItemName;
localStorage.setItem(localStorageKey, JSON.stringify(dataItems));
console.log("更新後的訪客身分資料:", dataItems[itemIndex]);
alert("訪客身分資料已更新");
} else {
alert("更新失敗,找不到資料");
}
window.location.href = "visitor_role_list.html";
});
</script>
</body>
</html>

View File

@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>訪客身分列表</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
}
.container.mt-4 {
flex-grow: 1;
}
</style>
</head>
<body>
<div class="container mt-4">
<h2 class="mb-4">訪客身分管理 (會顯示於訪客登記的身分建議選單)</h2>
<div class="row g-2 mb-3">
<div class="col-auto">
<a href="visitor_role_add.html" class="btn btn-outline-success rounded-pill">新增身分</a>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-sm-8 col-md-5 col-lg-4">
<input type="text" id="searchName" class="form-control" placeholder="搜尋身分名稱">
</div>
<div class="col-auto">
<button class="btn btn-outline-primary w-100 rounded-pill" onclick="performSearch()">搜尋</button>
</div>
<div class="col-auto">
<button class="btn btn-outline-danger w-100 rounded-pill" onclick="deleteSelected()">刪除勾選</button>
</div>
</div>
<table class="table table-bordered table-hover align-middle">
<thead class="table-dark">
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleAll(this)"></th>
<th>身分名稱</th>
<th>操作</th>
</tr>
</thead>
<tbody id="dataTable"></tbody>
</table>
<div class="d-flex justify-content-between align-items-center mt-3">
<div><span id="currentPageInfo">1</span> 頁,共 <span id="totalPagesInfo">1</span></div>
<div>
<button id="prevPageBtn" class="btn btn-sm btn-outline-secondary me-1" onclick="prevPage()">上一頁</button>
<button id="nextPageBtn" class="btn btn-sm btn-outline-secondary" onclick="nextPage()">下一頁</button>
</div>
</div>
</div>
<script>
const pageSize = 5;
let currentPage = 1;
let currentFilteredData = [];
const localStorageKey = 'visitorRolesData';
let dataItems = [];
function loadData() {
const storedData = localStorage.getItem(localStorageKey);
if (storedData) {
dataItems = JSON.parse(storedData);
} else {
// Default data if localStorage is empty
dataItems = [
{ id: 1, name: "朋友" },
{ id: 2, name: "親戚" },
{ id: 3, name: "廠商" },
{ id: 4, name: "外送員 (Foodpanda)" },
{ id: 5, name: "外送員 (Uber Eats)" },
{ id: 6, name: "快遞員 (黑貓宅急便)" },
{ id: 7, name: "維修人員 (水電)" },
{ id: 8, name: "清潔人員" },
{ id: 9, name: "業務代表" },
{ id: 10, name: "應徵者" },
{ id: 11, name: "其他" }
];
localStorage.setItem(localStorageKey, JSON.stringify(dataItems));
}
}
function saveData() {
localStorage.setItem(localStorageKey, JSON.stringify(dataItems));
}
function renderTable(dataToRender) {
const tbody = document.getElementById("dataTable");
tbody.innerHTML = "";
const start = (currentPage - 1) * pageSize;
const pageData = dataToRender.slice(start, start + pageSize);
pageData.forEach(item => {
const row = document.createElement("tr");
row.innerHTML = `
<td><input type="checkbox" class="row-checkbox" data-id="${item.id}"></td>
<td>${item.name}</td>
<td>
<a href="visitor_role_edit.html?id=${item.id}" class="btn btn-outline-secondary btn-sm">編輯</a>
<button class="btn btn-outline-danger btn-sm" onclick="deleteItem(${item.id})">刪除</button>
</td>
`;
tbody.appendChild(row);
});
updatePaginationControls(dataToRender.length);
if (document.getElementById("selectAll")) {
document.getElementById("selectAll").checked = false;
}
}
function updatePaginationControls(totalItems) {
const totalPages = Math.ceil(totalItems / pageSize) || 1;
const currentPageDisplay = document.getElementById("currentPageInfo");
const totalPagesDisplay = document.getElementById("totalPagesInfo");
const prevButton = document.getElementById("prevPageBtn");
const nextButton = document.getElementById("nextPageBtn");
if (currentPageDisplay) currentPageDisplay.textContent = currentPage;
if (totalPagesDisplay) totalPagesDisplay.textContent = totalPages;
if (prevButton) prevButton.disabled = currentPage === 1;
if (nextButton) nextButton.disabled = currentPage === totalPages || totalItems === 0;
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
renderTable(currentFilteredData);
} else if (totalItems === 0 && currentPage !==1) {
currentPage = 1;
renderTable(currentFilteredData);
} else if (totalItems === 0) {
if (currentPageDisplay) currentPageDisplay.textContent = 1;
if (totalPagesDisplay) totalPagesDisplay.textContent = 1;
if (prevButton) prevButton.disabled = true;
if (nextButton) nextButton.disabled = true;
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function nextPage() {
const totalPages = Math.ceil(currentFilteredData.length / pageSize);
if (currentPage < totalPages) {
currentPage++;
renderTable(currentFilteredData);
window.scrollTo(0, 0);
}
}
function performSearch() {
loadData(); // Ensure data is fresh from localStorage
const nameInput = document.getElementById("searchName").value.toLowerCase();
currentFilteredData = dataItems.filter(item =>
item.name.toLowerCase().includes(nameInput)
);
currentPage = 1;
renderTable(currentFilteredData);
}
function toggleAll(source) {
const checkboxes = document.querySelectorAll(".row-checkbox");
checkboxes.forEach(cb => cb.checked = source.checked);
}
function deleteSelected() {
if (!confirm("確定要刪除勾選的身分資料嗎?")) return;
const selected = document.querySelectorAll(".row-checkbox:checked");
if (selected.length === 0) {
alert("請先勾選要刪除的身分資料");
return;
}
const idsToDelete = Array.from(selected).map(cb => parseInt(cb.dataset.id));
dataItems = dataItems.filter(item => !idsToDelete.includes(item.id));
saveData();
performSearch(); // Re-filter and re-render
}
function deleteItem(id) {
if (!confirm("確定要刪除此筆身分資料嗎?")) return;
dataItems = dataItems.filter(item => item.id !== id);
saveData();
performSearch(); // Re-filter and re-render
}
// Initial load
document.addEventListener('DOMContentLoaded', performSearch);
</script>
</body>
</html>