421 lines
11 KiB
Vue
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> |