後台調整
This commit is contained in:
parent
a560065aec
commit
6e1b511d19
122
Backstage/admin_add.html
Normal file
122
Backstage/admin_add.html
Normal 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
159
Backstage/admin_edit.html
Normal 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
265
Backstage/admin_list.html
Normal 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>
|
194
Backstage/announcement_add.html
Normal file
194
Backstage/announcement_add.html
Normal 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>
|
219
Backstage/announcement_edit.html
Normal file
219
Backstage/announcement_edit.html
Normal 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>
|
271
Backstage/announcement_list.html
Normal file
271
Backstage/announcement_list.html
Normal 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>
|
55
Backstage/announcement_title_add.html
Normal file
55
Backstage/announcement_title_add.html
Normal 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>
|
84
Backstage/announcement_title_edit.html
Normal file
84
Backstage/announcement_title_edit.html
Normal 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>
|
240
Backstage/announcement_title_list.html
Normal file
240
Backstage/announcement_title_list.html
Normal 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>
|
185
Backstage/community_edit.html
Normal file
185
Backstage/community_edit.html
Normal 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
279
Backstage/fee_add.html
Normal 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
327
Backstage/fee_edit.html
Normal 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
280
Backstage/fee_list.html
Normal 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>
|
@ -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">
|
||||
|
140
Backstage/payment_item_add.html
Normal file
140
Backstage/payment_item_add.html
Normal 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>
|
91
Backstage/payment_item_edit.html
Normal file
91
Backstage/payment_item_edit.html
Normal 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>
|
214
Backstage/payment_item_list.html
Normal file
214
Backstage/payment_item_list.html
Normal 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>
|
53
Backstage/purpose_add.html
Normal file
53
Backstage/purpose_add.html
Normal 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>
|
80
Backstage/purpose_edit.html
Normal file
80
Backstage/purpose_edit.html
Normal 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
204
Backstage/purpose_list.html
Normal 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>
|
47
Backstage/quick_message_add.html
Normal file
47
Backstage/quick_message_add.html
Normal 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>
|
89
Backstage/quick_message_edit.html
Normal file
89
Backstage/quick_message_edit.html
Normal 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>
|
219
Backstage/quick_message_list.html
Normal file
219
Backstage/quick_message_list.html
Normal 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>
|
@ -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"> </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
293
Backstage/visitor_add.html
Normal 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
297
Backstage/visitor_edit.html
Normal 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
279
Backstage/visitor_list.html
Normal 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>
|
55
Backstage/visitor_role_add.html
Normal file
55
Backstage/visitor_role_add.html
Normal 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>
|
81
Backstage/visitor_role_edit.html
Normal file
81
Backstage/visitor_role_edit.html
Normal 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>
|
204
Backstage/visitor_role_list.html
Normal file
204
Backstage/visitor_role_list.html
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user