feat: 重构前端界面并优化API集成

- 添加vite环境类型定义文件
- 优化考勤记录视图
- 修复后端时间戳处理问题
- 重构管理仪表盘响应式布局
- 改进工人历史视图卡片式布局
- 优化人员管理组件表格响应式
- 增强二维码管理组件移动端适配
- 重构考勤报表组件添加全选功能
This commit is contained in:
sudomarcma
2025-06-17 17:09:04 +08:00
parent 6b2b95ce8b
commit 4a04cfe15b
14 changed files with 614 additions and 312 deletions
+303 -151
View File
@@ -1,43 +1,55 @@
<template>
<div class="attendance-reporting-layout">
<div class="selection-panel">
<section class="card" style="height: 70vh; display: flex; flex-direction: column">
<h3 class="panel-header">1. Select Workers</h3>
<div class="search-box">
<input
type="text"
v-model="searchQuery"
placeholder="Search by name..."
class="form-input"
@keydown.enter.prevent="handleSearch"
@keydown.down.prevent="navigateResults(1)"
@keydown.esc.prevent="clearSearch"
/>
<div v-if="searchResults.length > 0" class="search-results-list">
<ul>
<li
v-for="(worker, index) in searchResults"
:key="worker.id"
:class="{ highlighted: index === highlightedIndex }"
@click="selectWorker(worker)"
@mouseenter="highlightedIndex = index"
>
{{ worker.full_name }}
</li>
</ul>
<section class="card">
<h2 class="panel-header">1. Select Workers</h2>
<div class="selection-controls">
<div class="search-box">
<input
type="text"
v-model="searchQuery"
placeholder="Search for a worker..."
class="form-input"
@keydown.enter.prevent="handleSearch"
@keydown.down.prevent="navigateResults(1)"
@keydown.up.prevent="navigateResults(-1)"
@keydown.esc.prevent="clearSearch"
/>
<div v-if="searchResults.length > 0" class="search-results-list">
<ul>
<li
v-for="(worker, index) in searchResults"
:key="worker.id"
:class="{ highlighted: index === highlightedIndex }"
@click="selectWorker(worker)"
@mouseenter="highlightedIndex = index"
>
{{ worker.full_name }}
</li>
</ul>
</div>
</div>
<button @click="handleSelectAll" class="button-secondary select-all-btn">
Select All
</button>
</div>
<div
class="selected-workers-list"
style="flex-grow: 1; display: flex; flex-direction: column; overflow: hidden"
>
<div class="selected-workers-list">
<h4>Selected for Report ({{ selectedWorkers.length }})</h4>
<ul v-if="selectedWorkers.length > 0" style="flex-grow: 1; overflow-y: auto">
<ul v-if="isSelectAllActive">
<li>
<span>All Workers ({{ selectedWorkers.length }}) Selected</span>
<button @click="clearAllSelection" class="remove-btn">×</button>
</li>
</ul>
<ul v-else-if="selectedWorkers.length > 0">
<li v-for="worker in selectedWorkers" :key="worker.id">
<span>{{ worker.full_name }}</span>
<button @click="removeWorker(worker.id)" class="remove-btn">×</button>
</li>
</ul>
<p v-else class="empty-state">No workers selected.</p>
</div>
</section>
@@ -45,7 +57,7 @@
<div class="results-panel">
<section class="card">
<h3 class="panel-header">2. Report Settings</h3>
<h2 class="panel-header">2. Report Settings</h2>
<div class="filters">
<div class="form-group">
<label for="start-date">Start Date</label>
@@ -58,10 +70,9 @@
</div>
<div class="overtime-settings-section" v-if="selectedWorkers.length > 0">
<h4 class="settings-header">Overtime Settings</h4>
<div class="worker-salaries">
<h5>Monthly Salary (RM)</h5>
<p class="subtitle">Enter the monthly salary to be applied to all selected workers.</p>
<h4>Monthly Salary (RM)</h4>
<p class="subtitle">Applied to all selected workers.</p>
<div class="form-group">
<input
id="monthly-salary"
@@ -72,11 +83,11 @@
/>
</div>
</div>
<div class="factors-and-holidays">
<div class="factor-settings">
<h4>OT Factors</h4>
<div class="factor-input">
<h5>OT Factors</h5>
<div class="form-group">
<label for="rest-day-factor">Rest Day OT Factor</label>
<p class="subtitle">Weekend Factor</p>
<input
id="rest-day-factor"
type="number"
@@ -85,7 +96,7 @@
/>
</div>
<div class="form-group">
<label for="holiday-factor">Holiday Day OT Factor</label>
<p class="subtitle">Holiday Factor</p>
<input
id="holiday-factor"
type="number"
@@ -94,32 +105,33 @@
/>
</div>
</div>
<div class="holiday-picker">
<h5>Select Public Holidays</h5>
<div class="calendar">
<div class="calendar-header">
<button @click="changeMonth(-1)"></button>
<span>{{ calendarGrid.monthName }} {{ calendarGrid.year }}</span>
<button @click="changeMonth(1)"></button>
</div>
<div class="holiday-picker">
<h5>Select Public Holidays</h5>
<div class="calendar">
<div class="calendar-header">
<button @click="changeMonth(-1)"></button>
<span>{{ calendarGrid.monthName }} {{ calendarGrid.year }}</span>
<button @click="changeMonth(1)"></button>
</div>
<div class="calendar-weekdays">
<div v-for="day in calendarGrid.weekdayLabels" :key="day" class="weekday">
{{ day }}
</div>
<div class="calendar-weekdays">
<div v-for="day in calendarGrid.weekdayLabels" :key="day" class="weekday">
{{ day }}
</div>
</div>
<div class="calendar-days">
<div
v-for="(day, index) in calendarGrid.grid"
:key="index"
class="day-cell"
:class="{
padding: day.type === 'padding',
holiday: day.isHoliday,
}"
@click="day.type === 'day' && toggleHoliday(day.dateString)"
>
<span v-if="day.type === 'day'">{{ day.date }}</span>
</div>
</div>
<div class="calendar-days">
<div
v-for="(day, index) in calendarGrid.grid"
:key="index"
class="day-cell"
:class="{
padding: day.type === 'padding',
holiday: day.isHoliday,
}"
@click="day.type === 'day' && toggleHoliday(day.dateString)"
>
<span v-if="day.type === 'day'">{{ day.date }}</span>
</div>
</div>
</div>
@@ -127,7 +139,11 @@
</div>
<div class="action-buttons">
<button @click="generateReport" :disabled="!canGenerate">
<button
@click="generateReport"
:disabled="!canGenerate"
style="background-color: var(--c-success)"
>
Generate Attendance & OT Report
</button>
</div>
@@ -146,23 +162,24 @@
Export OT Summary (CSV)
</button>
</div>
<table>
<thead>
<tr>
<th>Worker</th>
<th>Total Hours Worked</th>
<th>Total OT Pay (RM)</th>
</tr>
</thead>
<tbody>
<tr v-for="(report, name) in overtimeReport" :key="name">
<td>{{ name }}</td>
<td>{{ report.totalHours.toFixed(2) }}</td>
<td>{{ report.totalOtPay.toFixed(2) }}</td>
</tr>
</tbody>
</table>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Worker</th>
<th>Total Hours Worked</th>
<th>Total OT Pay (RM)</th>
</tr>
</thead>
<tbody>
<tr v-for="(report, name) in overtimeReport" :key="name">
<td>{{ name }}</td>
<td>{{ report.totalHours.toFixed(2) }}</td>
<td>{{ report.totalOtPay.toFixed(2) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="raw-logs" v-if="reportData.length > 0">
@@ -211,6 +228,7 @@ import { ref, onMounted, computed } from 'vue'
const searchQuery = ref('')
const searchResults = ref([])
const highlightedIndex = ref(-1)
const isSelectAllActive = ref(false)
const selectedWorkers = ref([])
const filters = ref({ startDate: '', endDate: '' })
@@ -286,24 +304,43 @@ const calendarGrid = computed(() => {
}
})
// --- METHODS ---
const handleSearch = () => {
const handleSearch = async () => {
// SCENARIO 1: User presses Enter on a highlighted item in the list
if (highlightedIndex.value >= 0 && searchResults.value[highlightedIndex.value]) {
selectWorker(searchResults.value[highlightedIndex.value])
} else {
fetchWorkers()
}
// SCENARIO 2: User presses Enter in an empty search box
else if (searchQuery.value === '') {
await handleSelectAll()
searchResults.value = []
highlightedIndex.value = -1
}
// SCENARIO 3: User types a query and presses Enter for the first time
else {
// This fetches the list of workers so you can navigate it
await fetchWorkers()
}
}
const fetchWorkers = async () => {
const fetchWorkers = async (selectAll = false) => {
// ... this function's internal logic remains the same
try {
const res = await fetch(
`http://localhost:3000/api/managers/workers?search=${searchQuery.value}&limit=10`,
`${import.meta.env.VITE_API_BASE_URL}/api/managers/workers?search=${searchQuery.value}&limit=1000`,
)
if (res.ok) {
const data = await res.json()
searchResults.value = data.workers
highlightedIndex.value = data.workers.length > 0 ? 0 : -1
if (selectAll) {
data.workers.forEach((worker) => {
if (!selectedWorkers.value.some((w) => w.id === worker.id)) {
selectedWorkers.value.push(worker)
}
})
} else {
searchResults.value = data.workers
// Set highlighted index to the first item, or -1 if no results
highlightedIndex.value = data.workers.length > 0 ? 0 : -1
}
}
} catch (err) {
console.error('Failed to search workers', err)
@@ -323,16 +360,29 @@ const navigateResults = (direction) => {
highlightedIndex.value = newIndex
}
}
const handleSelectAll = async () => {
await fetchWorkers(true) // This function already fetches and selects all workers
isSelectAllActive.value = true // Activate the summary view
}
// vvv ADD THIS NEW METHOD vvv
const clearAllSelection = () => {
selectedWorkers.value = []
isSelectAllActive.value = false
}
const selectWorker = (worker) => {
if (!selectedWorkers.value.some((w) => w.id === worker.id)) {
selectedWorkers.value.push(worker)
}
clearSearch()
isSelectAllActive.value = false // Turn off "Select All" mode
clearSearch() // Clear the search box and results after selection
}
// vvv MODIFY THIS METHOD vvv
const removeWorker = (workerId) => {
selectedWorkers.value = selectedWorkers.value.filter((w) => w.id !== workerId)
isSelectAllActive.value = false // Deactivate summary view when removing individually
}
const generateReport = async () => {
@@ -346,7 +396,7 @@ const generateReport = async () => {
overtimeReport.value = null
const workerIds = selectedWorkers.value.map((w) => w.id).join(',')
const url = `http://localhost:3000/api/managers/attendance-records?workerIds=${workerIds}&startDate=${filters.value.startDate}&endDate=${filters.value.endDate}`
const url = `${import.meta.env.VITE_API_BASE_URL}/api/managers/attendance-records?workerIds=${workerIds}&startDate=${filters.value.startDate}&endDate=${filters.value.endDate}`
try {
const res = await fetch(url)
@@ -526,76 +576,180 @@ onMounted(() => {
</script>
<style scoped>
/* --- Existing Styles --- */
.attendance-reporting-layout {
display: grid;
grid-template-columns: 350px 1fr;
gap: 2rem;
align-items: flex-start;
/* Add these media queries */
@media (max-width: 768px) {
.attendance-reporting-layout {
flex-direction: column;
}
.selection-controls {
flex-direction: column;
align-items: stretch; /* Make items take full width */
}
.card {
width: 100%;
height: auto;
min-height: 300px;
margin-bottom: 1rem;
box-sizing: border-box;
}
.form-input,
button {
font-size: 16px; /* Prevent zooming on focus */
padding: 12px;
}
.remove-btn {
width: 30px;
height: 30px;
}
table {
font-size: 14px;
}
.factor-input {
display: flex;
flex-direction: column;
}
.filters {
flex-direction: column;
align-items: stretch;
}
/* Target the holiday picker section */
.holiday-picker {
margin-top: 2rem;
max-width: 100%; /* Ensure the container doesn't overflow */
overflow-x: hidden; /* Prevent horizontal scroll within this section */
}
/* Force the calendar itself to fit the screen */
.calendar {
/* This ensures the calendar itself shrinks to fit the screen */
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
}
.panel-header {
margin-top: 0;
@media (min-width: 769px) {
.selected-workers-list ul {
display: flex; /* 1. Turn the list container into a flexbox */
flex-wrap: wrap; /* 2. Allow items to wrap to the next line */
gap: 0.75rem; /* 3. Add spacing between the tags */
}
.selected-workers-list li {
margin-bottom: 0;
}
.factor-input {
display: flex;
flex-direction: row;
}
}
.selection-controls {
display: flex;
gap: 0.75rem;
align-items: center;
}
.search-box {
position: relative;
}
.search-results-list {
position: absolute;
width: 100%;
top: 100%; /* Places the list directly below the input */
left: 0;
right: 0;
background-color: var(--c-bg-secondary);
border: 1px solid var(--c-border);
border-radius: var(--radius);
margin-top: 0.5rem;
z-index: 10;
max-height: 200px;
border-top: none;
border-radius: 0 0 var(--radius) var(--radius);
z-index: 10; /* Ensures the list appears on top of other content */
max-height: 250px;
overflow-y: auto;
box-shadow: var(--shadow-md);
}
.search-results-list ul {
list-style: none;
margin: 0;
padding: 0.5rem;
}
.search-results-list li {
padding: 0.75rem 1rem;
cursor: pointer;
border-radius: 6px;
}
.search-results-list li.highlighted {
background-color: var(--c-primary);
color: var(--c-primary-text);
}
.selected-workers-list {
margin-top: 1.5rem;
}
.selected-workers-list ul {
list-style: none;
padding: 0;
margin: 0;
}
.search-results-list li {
padding: 12px 15px;
cursor: pointer;
border-bottom: 1px solid var(--c-border);
}
.search-results-list li:last-child {
border-bottom: none;
}
.search-results-list li.highlighted,
.search-results-list li:hover {
background-color: var(--c-bg-tertiary);
color: var(--c-text-primary);
}
.select-all-btn {
/* Prevents the button from shrinking */
flex-shrink: 0;
padding: 10px 16px; /* Match form-input padding from main.css */
}
/*
* STYLES FOR THE SELECTED WORKERS LIST
*/
.selected-workers-list ul {
list-style: none;
padding: 0;
margin-top: 1rem;
}
.selected-workers-list li {
/* Base styles for each worker "tag" */
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-radius: 6px;
}
.selected-workers-list li:nth-child(odd) {
background-color: var(--c-bg-primary);
}
.remove-btn {
background: none;
border: none;
color: var(--c-danger);
font-size: 1.5rem;
cursor: pointer;
}
.empty-state {
color: var(--c-text-secondary);
text-align: center;
padding: 1rem;
padding: 6px 12px;
background-color: var(--c-bg-tertiary);
border-radius: var(--radius);
border: 1px solid var(--c-border);
gap: 1rem; /* Space between name and remove button */
/* Spacing for the default (mobile) vertical layout */
margin-bottom: 0.5rem;
}
.remove-btn {
/* Making the remove button a bit softer */
background: none;
border: none;
font-size: 1.2rem;
color: var(--c-text-secondary);
cursor: pointer;
line-height: 1;
}
.remove-btn:hover {
color: var(--c-danger);
}
.card {
width: 100%;
height: auto;
min-height: 300px;
margin-bottom: 1rem;
box-sizing: border-box;
}
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch; /* Smooth scrolling for iOS */
}
/* Optional: Prevent table from collapsing on small screens */
.table-wrapper table {
min-width: 650px;
}
/* --- Refactored & New Styles --- */
.filters {
display: flex;
gap: 1rem;
@@ -606,7 +760,7 @@ onMounted(() => {
.overtime-settings-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
gap: 0.5rem;
}
.settings-header {
margin: 0;
@@ -617,25 +771,17 @@ onMounted(() => {
.factor-input h5,
.holiday-picker h5 {
margin-top: 0;
margin-bottom: 0.75rem;
}
.form-group {
margin-top: 1rem;
}
.subtitle {
font-size: 0.9rem;
color: var(--c-text-secondary);
margin-top: -0.5rem;
margin-bottom: 0.75rem;
}
.factors-and-holidays {
display: grid;
grid-template-columns: 200px 1fr;
gap: 2rem;
align-items: flex-start;
}
.factor-input {
display: flex;
flex-direction: column;
gap: 1rem;
}
.action-buttons {
margin-top: 1.5rem;
@@ -669,6 +815,12 @@ onMounted(() => {
width: 30px;
height: 30px;
cursor: pointer;
/* Add the following lines to center the text */
display: flex; /* Use flexbox */
justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically */
font-size: 1.2rem; /* Adjust font size if needed for better centering visually */
line-height: 1; /* Reset line-height to prevent extra spacing */
}
.calendar-weekdays,
.calendar-days {
+69 -30
View File
@@ -53,33 +53,37 @@
@keyup.enter="fetchWorkers(1)"
/>
</div>
<table>
<thead>
<tr>
<th>Full Name</th>
<th>Username</th>
<th>Date Joined</th>
<th class="actions-header">Actions</th>
</tr>
</thead>
<tbody>
<tr v-if="loading && workers.length === 0">
<td colspan="4" class="loading-state">Loading workers...</td>
</tr>
<tr v-if="!loading && workers.length === 0">
<td colspan="4" class="empty-state">No workers found.</td>
</tr>
<tr v-for="worker in workers" :key="worker.id">
<td>{{ worker.full_name }}</td>
<td>{{ worker.username }}</td>
<td>{{ new Date(worker.created_at).toLocaleDateString() }}</td>
<td class="actions-cell">
<button @click="viewRecords(worker.id)" class="button-secondary">View Records</button>
<button @click="deleteWorker(worker.id)" class="button-danger">Delete</button>
</td>
</tr>
</tbody>
</table>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Full Name</th>
<th>Username</th>
<th>Date Joined</th>
<th class="actions-header">Actions</th>
</tr>
</thead>
<tbody>
<tr v-if="loading && workers.length === 0">
<td colspan="4" class="loading-state">Loading workers...</td>
</tr>
<tr v-if="!loading && workers.length === 0">
<td colspan="4" class="empty-state">No workers found.</td>
</tr>
<tr v-for="worker in workers" :key="worker.id">
<td>{{ worker.full_name }}</td>
<td>{{ worker.username }}</td>
<td>{{ new Date(worker.created_at).toLocaleDateString() }}</td>
<td class="actions-cell">
<button @click="viewRecords(worker.id)" class="button-secondary">
View Records
</button>
<button @click="deleteWorker(worker.id)" class="button-danger">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination-controls" v-if="totalPages > 1">
<button @click="changePage(currentPage - 1)" :disabled="currentPage <= 1">Previous</button>
<span>Page {{ currentPage }} of {{ totalPages }}</span>
@@ -128,7 +132,7 @@ const fetchWorkers = async (page = currentPage.value) => {
loading.value = true
try {
const res = await fetch(
`http://localhost:3000/api/managers/workers?search=${searchQuery.value}&page=${page}&limit=${pageSize.value}`,
`${import.meta.env.VITE_API_BASE_URL}/api/managers/workers?search=${searchQuery.value}&page=${page}&limit=${pageSize.value}`,
)
if (res.ok) {
const data = await res.json()
@@ -155,7 +159,7 @@ const addWorker = async () => {
loading.value = true
errorMessage.value = ''
try {
const res = await fetch('http://localhost:3000/api/managers/workers', {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/workers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newWorker.value),
@@ -178,7 +182,7 @@ const addWorker = async () => {
const deleteWorker = async (id) => {
if (!confirm('Are you sure you want to delete this worker account?')) return
try {
const res = await fetch(`http://localhost:3000/api/managers/workers/${id}`, {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/workers/${id}`, {
method: 'DELETE',
})
if (res.ok) {
@@ -274,4 +278,39 @@ onMounted(() => {
padding-top: 1.5rem;
border-top: 1px solid var(--c-border);
}
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
min-width: 600px; /* Force horizontal scroll on mobile */
}
/* For tablet-sized screens */
@media (max-width: 992px) {
.add-worker-form {
grid-template-columns: 1fr 1fr; /* 2-column layout for the form */
}
.add-worker-form button {
grid-column: span 2; /* Make button span full width */
}
}
/* For mobile-sized screens */
@media (max-width: 768px) {
.add-worker-form {
grid-template-columns: 1fr; /* 1-column layout for the form */
}
.add-worker-form button {
grid-column: span 1; /* Reset span */
}
.actions-cell {
flex-wrap: wrap; /* Allow action buttons to wrap onto the next line */
}
.pagination-controls {
flex-direction: column; /* Stack pagination controls */
gap: 0.75rem;
}
}
</style>
+65 -37
View File
@@ -31,36 +31,42 @@
<section class="card">
<h2 class="card-header">Existing QR Codes</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th class="actions-header">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="qr in qrCodes" :key="qr.id">
<td>{{ qr.name }}</td>
<td>
<!-- FIX #1: Use qr.is_active instead of qr.isActive -->
<span class="status-badge" :class="qr.is_active ? 'active' : 'inactive'">
{{ qr.is_active ? 'Active' : 'Inactive' }}
</span>
</td>
<td class="actions-cell">
<button @click="downloadQrCode(qr)" class="button-secondary" title="Download QR Code">
<span></span> Download
</button>
<!-- FIX #1: Use qr.is_active instead of qr.isActive -->
<button @click="toggleQrStatus(qr)" class="button-secondary">
{{ qr.is_active ? 'Deactivate' : 'Activate' }}
</button>
<button @click="deleteQrCode(qr.id)" class="button-danger">Delete</button>
</td>
</tr>
</tbody>
</table>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th class="actions-header">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="qr in qrCodes" :key="qr.id">
<td>{{ qr.name }}</td>
<td>
<!-- FIX #1: Use qr.is_active instead of qr.isActive -->
<span class="status-badge" :class="qr.is_active ? 'active' : 'inactive'">
{{ qr.is_active ? 'Active' : 'Inactive' }}
</span>
</td>
<td class="actions-cell">
<button
@click="downloadQrCode(qr)"
class="button-secondary"
title="Download QR Code"
>
<span></span> Download
</button>
<!-- FIX #1: Use qr.is_active instead of qr.isActive -->
<button @click="toggleQrStatus(qr)" class="button-secondary">
{{ qr.is_active ? 'Deactivate' : 'Activate' }}
</button>
<button @click="deleteQrCode(qr.id)" class="button-danger">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</template>
@@ -87,7 +93,7 @@ onMounted(() => {
const fetchQrCodes = async () => {
try {
const res = await fetch('http://localhost:3000/api/managers/qr-codes')
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/qr-codes`)
qrCodes.value = await res.json()
} catch (err) {
console.error('Failed to fetch QR codes:', err)
@@ -97,7 +103,7 @@ const fetchQrCodes = async () => {
const addQrCode = async () => {
if (!newQrName.value) return
try {
const res = await fetch('http://localhost:3000/api/managers/qr-codes', {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/qr-codes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newQrName.value }),
@@ -124,7 +130,7 @@ const addQrCode = async () => {
const toggleQrStatus = async (qr) => {
try {
const res = await fetch(`http://localhost:3000/api/managers/qr-codes/${qr.id}`, {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/qr-codes/${qr.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
// Send the opposite of the current status
@@ -148,7 +154,7 @@ const deleteQrCode = async (id) => {
return
}
try {
const res = await fetch(`http://localhost:3000/api/managers/qr-codes/${id}`, {
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/qr-codes/${id}`, {
method: 'DELETE',
})
if (res.ok) {
@@ -257,7 +263,29 @@ const downloadQrCode = async (qr) => {
align-items: center;
gap: 0.5rem;
}
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
min-width: 500px; /* Force horizontal scroll */
}
.actions-cell {
flex-wrap: wrap; /* Allow buttons to wrap on smaller screens */
}
@media (max-width: 768px) {
.qr-add-form {
flex-direction: column; /* Stack form elements vertically */
align-items: stretch;
}
.actions-header {
text-align: left; /* Align header with wrapped buttons */
}
.actions-cell {
justify-content: flex-start;
}
}
</style>
``` These targeted fixes should resolve the issues completely. The component will now correctly
display the status from the database on initial load and will properly update the state when you
activate or deactivate a QR co