front_ponto_eletronico/src/views/Shifts.vue
2025-05-05 17:20:12 -03:00

421 lines
11 KiB
Vue

<template>
<v-container class="tab">
<div class="header-container">
<div class="header-left">
<div class="icon-wrapper">
<v-icon color="primary" size="32">mdi-clock-outline</v-icon>
</div>
<div class="header-title">
<h1>Sistema de Ponto | Admin</h1>
</div>
</div>
<div class="header-right">
<v-btn variant="text" prepend-icon="mdi-arrow-left" class="back-button" to="/dashboard">
Voltar para Dashboard
</v-btn>
<v-btn
color="primary"
prepend-icon="mdi-plus"
variant="elevated"
@click="openCreateShiftDialog"
>
Nova Escala
</v-btn>
<v-badge dot color="error" class="notification-badge">
<v-icon>mdi-bell</v-icon>
</v-badge>
<div class="admin-profile">
<v-avatar class="mr-2" color="primary" size="40">
<v-img src="/api/placeholder/40/40" alt="Administrador"></v-img>
</v-avatar>
<div class="admin-info">
<span class="admin-name">Administrador</span>
<span class="admin-role">Gestor</span>
</div>
<v-icon>mdi-chevron-down</v-icon>
</div>
</div>
</div>
<!-- Título e descrição da seção -->
<div class="section-header">
<h2>Escalas de Trabalho</h2>
<p class="section-description">Configure as escalas de trabalho dos colaboradores</p>
</div>
<!-- Tabela -->
<v-card class="mt-6">
<div class="list-header">
<h2 class="list-title">Lista de Escalas</h2>
<p class="list-subtitle">Gerencie as escalas de trabalho disponíveis no sistema</p>
</div>
<v-divider></v-divider>
<v-data-table
class="dados"
:headers="headers.shifts"
:items="filteredshifts"
:search="filters.shift"
:loading="loading.shifts"
:items-per-page="itemsPerPage"
:page.sync="page"
>
<template v-slot:item.horarios="{ item }">
<div>
{{ formatTimeRange(item.start_time, item.interval_start) }}<br>
{{ formatTimeRange(item.interval_end, item.end_time) }}
</div>
</template>
<template v-slot:item.almoco_type="{ item }">
{{ item.type_interval === 'automatic' ? 'Automático' : 'Manual' }}
</template>
<template v-slot:item.actions="{ item }">
<v-icon size="small" class="mr-2" @click="openEditShiftDialog(item.id)">mdi-pencil</v-icon>
<v-icon size="small" @click="confirmDelete('shift', item)">mdi-delete</v-icon>
</template>
</v-data-table>
</v-card>
<!-- MODAL DE CRIAÇÃO -->
<ShiftModal
v-model="dialogs.createShift"
@saved="handleCreatedShift"
/>
<!-- MODAL DE EDIÇÃO -->
<EditShiftModal
v-model="dialogs.editShift"
:shiftData="forms.shift"
@save="handleEditedShift"
/>
<!-- CONFIRMAÇÃO DE EXCLUSÃO -->
<v-dialog v-model="dialogs.delete" max-width="400px">
<v-card>
<v-card-title class="text-h5 bg-error text-white">Confirmar Exclusão</v-card-title>
<v-card-text class="pt-4">
<p v-if="itemToDelete && itemToDelete.name">
Tem certeza que deseja excluir a escala <strong>{{ itemToDelete.name }}</strong>?
</p>
<p v-else>Erro: Nenhum item selecionado para exclusão.</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" @click="dialogs.delete = false">Cancelar</v-btn>
<v-btn color="error" :loading="loading.delete" @click="deleteItem">Excluir</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- SNACKBAR -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
{{ snackbar.text }}
<template v-slot:actions>
<v-btn color="white" variant="text" @click="snackbar.show = false">Fechar</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
import ShiftModal from '../components/modals/ShiftModal.vue';
import EditShiftModal from '../components/modals/EditShiftModal.vue';
import { useShiftStore } from '../stores/shift';
export default {
components: { ShiftModal, EditShiftModal },
setup() {
const shiftStore = useShiftStore();
const page = ref(1);
const itemsPerPage = ref(10);
const dialogs = ref({ createShift: false, editShift: false, delete: false });
const filters = ref({ shift: '' });
const loading = ref({ shifts: false, delete: false });
const snackbar = ref({ show: false, text: '', color: 'success' });
const shifts = ref([]);
const itemToDelete = ref(null);
const forms = ref({
shift: {
id: null,
name: '',
//description: '',
day: '',
start_time: '',
end_time: '',
interval_start: '',
interval_end: '',
type_interval: '',
tolerance: ''
}
});
const formatTimeRange = (start, end) => (!start || !end) ? '' : `${start} - ${end}`;
const formatFullSchedule = (item) => {
let schedule = `${item.start_time} - ${item.end_time}`;
if (item.interval_start && item.interval_end) {
schedule += ` (${item.interval_start}-${item.interval_end})`;
}
return schedule;
};
const formatDays = (days) => {
const dayMap = {
'Segunda-feira': 'Seg', 'Terça-feira': 'Ter', 'Quarta-feira': 'Qua',
'Quinta-feira': 'Qui', 'Sexta-feira': 'Sex', 'Sábado': 'Sáb', 'Domingo': 'Dom'
};
return days?.map(day => dayMap[day.name] || day.name).join(', ') || '';
};
const fetchShifts = async () => {
loading.value.shifts = true;
try {
const data = await shiftStore.fetchShifts();
shifts.value = data.map(shift => ({
...shift,
formatted_days: formatDays(shift.time_schedules),
horarios: formatFullSchedule(shift)
}));
} catch {
showNotification('Erro ao carregar escalas', 'error');
} finally {
loading.value.shifts = false;
}
};
const openCreateShiftDialog = () => {
dialogs.value.createShift = true;
};
const openEditShiftDialog = async (id) => {
console.log('🔍 Abrindo edição para ID:', id); // <-- Teste
const data = await shiftStore.fetchShiftById(id);
forms.value.shift = {
id: data.id || null,
name: data.name || '',
day: data.day || '',
start_time: data.start_time || '',
end_time: data.end_time || '',
interval_start: data.interval_start || '',
interval_end: data.interval_end || '',
type_interval: data.type_interval || '',
tolerance: data.tolerance || '',
description: data.description || '' ,
time_schedules: data.time_schedules || []
};
dialogs.value.editShift = true;
};
const confirmDelete = (type, item) => {
itemToDelete.value = item;
dialogs.value.delete = true;
};
const deleteItem = async () => {
const id = itemToDelete.value.id;
await shiftStore.deleteShift(id);
shifts.value = shifts.value.filter(s => s.id !== id);
showNotification('Escala excluída com sucesso');
dialogs.value.delete = false;
};
const submitEdit = async (data) => {
//await shiftStore.updateShift(data.id, data);
await shiftStore.fetchShifts();
showNotification('Escala atualizada com sucesso');
dialogs.value.editShift = false;
};
const handleCreatedShift = async () => {
await fetchShifts(); // função que carrega os turnos da API
showNotification('Escala criada com sucesso!');
dialogs.value.createShift = false;
};
const handleEditedShift = async () => {
await fetchShifts(); // 🔁 recarrega os dados atualizados da API
dialogs.value.editShift = false; // ❌ fecha o modal
showNotification('Escala atualizada com sucesso!');
};
const showNotification = (msg, color = 'success') => {
snackbar.value = { show: true, text: msg, color };
};
const filteredshifts = computed(() => {
return shifts.value; // ou filtrado por nome etc.
});
const headers = ref({
shifts: [
{ title: 'Nome', key: 'name' },
//{ title: 'Descrição', key: 'description' },
{ title: 'Horários', key: 'horarios' },
{ title: 'Dias da Semana', key: 'formatted_days' },
{ title: 'Tolerância', key: 'tolerance' },
{ title: 'Almoço', key: 'almoco_type' },
{ title: 'Ações', key: 'actions', sortable: false }
]
});
onMounted(fetchShifts);
return {
dialogs, loading, filters, shifts, snackbar, forms,
page, itemsPerPage, headers, itemToDelete,
formatTimeRange, openCreateShiftDialog, openEditShiftDialog,
confirmDelete, deleteItem, submitEdit, handleCreatedShift,handleEditedShift, filteredshifts
};
}
};
</script>
<style scoped>
.header-container {
display: flex;
background-color: #ffffff;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
border-bottom: 1px solid #ffffff;
}
.header-left {
display: flex;
align-items: center;
}
.icon-wrapper {
background-color: #e3f2fd;
border-radius: 8px;
padding: 12px;
margin-right: 12px;
}
.header-title h1 {
font-size: 18px;
font-weight: 600;
margin: 0;
color: #1e293b;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.back-button {
color: #1976d2;
}
.new-holiday-btn {
background-color: #1976d2;
color: white;
}
.admin-profile {
display: flex;
align-items: center;
margin-left: 16px;
cursor: pointer;
}
.admin-info {
display: flex;
flex-direction: column;
margin: 0 8px;
}
.admin-name {
font-size: 14px;
font-weight: 500;
color: #1e293b;
}
.admin-role {
font-size: 12px;
color: #64748b;
}
.notification-badge {
margin: 0 8px;
}
/* Estilo da seção de título */
.section-header {
margin-bottom: 24px;
}
.section-header h2 {
font-size: 24px;
font-weight: 600;
margin: 0 0 8px 0;
color: #1e293b;
}
.section-description {
color: #64748b;
font-size: 14px;
margin: 0;
}
.main-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
.subtitle {
color: #666;
font-size: 16px;
}
.image-wrapper {
display: flex;
justify-content: flex-end;
}
.top-image {
max-width: 180px;
height: auto;
}
.list-header {
padding: 20px;
border-bottom: 1px solid #eee;
}
.list-title {
font-size: 22px;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
.list-subtitle {
color: #666;
font-size: 15px;
}
.dados {
font-size: 14px;
color: rgb(0, 0, 0);
background-color: #fff;
}
.v-card {
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
</style>