14d412544e
- 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.
194 lines
7.8 KiB
Vue
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>
|