Files
Nilai_Clock/src/components/KillSwitchManagement.vue
T
sudomarcma 14d412544e feat: Implement worker authentication and device validation in backend
- Added login route with JWT token generation and device validation for non-manager roles.
- Implemented clocking functionality with geofence validation and distance calculation.
- Created routes for managing workers, including password change and device registration.
- Added security status and app blacklist retrieval endpoints.

feat: Develop Geofence Management component

- Created a Vue component for managing geofences with map integration using Leaflet.
- Implemented functionality to draw, save, activate/deactivate, and delete geofences.
- Added UI for displaying existing geofences in a table format.

feat: Introduce Kill Switch Management component

- Developed a calendar-based UI for managing enabled/disabled dates.
- Implemented functionality to apply or discard changes to the work schedule.
- Added visual indicators for pending changes in the calendar.

feat: Create Warning Reporting component

- Implemented a reporting interface for failed clock records with search and filter options.
- Added detail modal for viewing specific failed record details.
- Implemented sorting functionality for the records table.
2025-07-16 17:57:25 +08:00

194 lines
7.8 KiB
Vue

<template>
<div class="grid grid-cols-1 xl:grid-cols-4 gap-8 items-start">
<div class="xl:col-span-3 bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white">
{{ monthYear }}
</h2>
<div class="flex items-center gap-2">
<button @click="prevMonth" class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
</button>
<button @click="nextMonth" class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<svg class="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</button>
</div>
</div>
<div class="grid grid-cols-7 gap-1 text-center text-sm font-semibold text-gray-500 dark:text-gray-400 mb-2">
<div v-for="day in weekDays" :key="day" class="py-2">{{ day }}</div>
</div>
<div class="grid grid-cols-7 gap-1">
<div v-for="day in calendarGrid" :key="day.id"
@click="day.isCurrentMonth && onDayClick(day)"
:class="getDayClasses(day)">
{{ day.date }}
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 sticky top-4">
<h3 class="text-lg font-semibold mb-4 text-gray-800 dark:text-white">{{ $t('pendingChanges') }}</h3>
<div v-if="!hasPendingChanges" class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ $t('noPendingChanges') }}
</div>
<div v-else class="space-y-4 max-h-80 overflow-y-auto pr-2">
<div v-if="datesToEnable.size > 0">
<h4 class="font-semibold text-green-600 dark:text-green-400 mb-2">{{ $t('datesToEnable') }}</h4>
<ul class="space-y-1">
<li v-for="date in sortedEnableList" :key="date" class="text-sm text-gray-700 dark:text-gray-300">
{{ formatDate(date) }}
</li>
</ul>
</div>
<div v-if="datesToDisable.size > 0">
<h4 class="font-semibold text-red-600 dark:text-red-400 mb-2">{{ $t('datesToDisable') }}</h4>
<ul class="space-y-1">
<li v-for="date in sortedDisableList" :key="date" class="text-sm text-gray-700 dark:text-gray-300">
{{ formatDate(date) }}
</li>
</ul>
</div>
</div>
<div class="mt-6 flex flex-col sm:flex-row gap-3">
<button @click="applyChanges" :disabled="!hasPendingChanges" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold px-4 py-2 rounded-md disabled:opacity-50 disabled:cursor-not-allowed">
{{ $t('applyChanges') }}
</button>
<button @click="discardChanges" :disabled="!hasPendingChanges" class="w-full bg-gray-500 hover:bg-gray-600 text-white font-semibold px-4 py-2 rounded-md disabled:opacity-50 disabled:cursor-not-allowed">
{{ $t('discardChanges') }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { apiFetch } from '@/api.js';
const viewDate = ref(new Date());
const todayStr = new Date().toISOString().slice(0, 10);
const originalEnabledDates = ref(new Set());
const datesToEnable = ref(new Set());
const datesToDisable = ref(new Set());
const hasPendingChanges = computed(() => datesToEnable.value.size > 0 || datesToDisable.value.size > 0);
const sortedEnableList = computed(() => Array.from(datesToEnable.value).sort());
const sortedDisableList = computed(() => Array.from(datesToDisable.value).sort());
const monthYear = computed(() => viewDate.value.toLocaleString('default', { month: 'long', year: 'numeric' }));
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const calendarGrid = computed(() => {
const year = viewDate.value.getFullYear();
const month = viewDate.value.getMonth();
const firstDayOfMonth = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const grid = [];
for (let i = 0; i < firstDayOfMonth; i++) {
grid.push({ id: `prev-${i}`, isCurrentMonth: false });
}
for (let i = 1; i <= daysInMonth; i++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`;
grid.push({ id: dateStr, date: i, isCurrentMonth: true });
}
return grid;
});
const getDayClasses = (day) => {
if (!day.isCurrentMonth) return 'h-20';
const dateStr = day.id;
const classes = ['h-20', 'flex', 'items-center', 'justify-center', 'text-lg', 'rounded-lg', 'cursor-pointer', 'transition-colors', 'relative'];
let isEnabled = originalEnabledDates.value.has(dateStr);
if (datesToEnable.value.has(dateStr)) isEnabled = true;
if (datesToDisable.value.has(dateStr)) isEnabled = false;
const isPendingEnable = datesToEnable.value.has(dateStr);
const isPendingDisable = datesToDisable.value.has(dateStr);
if (isPendingEnable) {
classes.push('bg-blue-500', 'text-white', 'font-bold');
} else if (isPendingDisable) {
classes.push('bg-red-200', 'dark:bg-red-800', 'text-red-700', 'dark:text-red-200');
classes.push('after:content-[\'\']', 'after:absolute', 'after:w-3/4', 'after:h-0.5', 'after:bg-red-500', 'after:left-1/2', 'after:top-1/2', 'after:-translate-x-1/2', 'after:-translate-y-1/2', 'after:rotate-[-10deg]');
} else if (isEnabled) {
classes.push('bg-green-100', 'dark:bg-green-800', 'text-green-800', 'dark:text-green-200');
} else {
classes.push('bg-white', 'dark:bg-gray-800', 'hover:bg-gray-100', 'dark:hover:bg-gray-700');
}
// Add a yellow ring for today's date
if (dateStr === todayStr) {
classes.push('ring-2', 'ring-yellow-400', 'dark:ring-yellow-500');
}
return classes;
};
function onDayClick(day) {
const dateStr = day.id;
const isOriginallyEnabled = originalEnabledDates.value.has(dateStr);
if (isOriginallyEnabled) {
datesToDisable.value.has(dateStr)
? datesToDisable.value.delete(dateStr)
: datesToDisable.value.add(dateStr);
} else {
datesToEnable.value.has(dateStr)
? datesToEnable.value.delete(dateStr)
: datesToEnable.value.add(dateStr);
}
}
async function applyChanges() {
if (!confirm('Are you sure you want to apply these changes to the work schedule?')) return;
try {
await apiFetch('/api/managers/enabled-dates/update', {
method: 'POST',
body: JSON.stringify({
datesToEnable: Array.from(datesToEnable.value),
datesToDisable: Array.from(datesToDisable.value),
}),
});
await fetchEnabledDates();
discardChanges();
alert('Work schedule updated successfully!');
} catch (error) {
console.error('Failed to apply changes:', error);
alert('Failed to update schedule. Please try again.');
}
}
function discardChanges() {
datesToEnable.value.clear();
datesToDisable.value.clear();
}
const prevMonth = () => viewDate.value = new Date(viewDate.value.setMonth(viewDate.value.getMonth() - 1));
const nextMonth = () => viewDate.value = new Date(viewDate.value.setMonth(viewDate.value.getMonth() + 1));
const formatDate = (dateStr) => new Date(dateStr + 'T00:00:00').toLocaleDateString(undefined, {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
});
async function fetchEnabledDates() {
try {
const dates = await apiFetch('/api/managers/enabled-dates');
originalEnabledDates.value = new Set(dates);
} catch (error) {
console.error('Failed to fetch enabled dates:', error);
}
}
onMounted(fetchEnabledDates);
</script>