Created
September 21, 2024 11:37
-
-
Save dantetesta/6230af964f9f3956f72cbe4188eb2b59 to your computer and use it in GitHub Desktop.
Cocôladora 1.0 - Dante Testa - Calcula suas cagadas!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="pt-BR"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Cocôladora - by Dante Testa</title> | |
<!-- Incluindo o Bootstrap CSS --> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
<!-- Incluindo o Google Fonts --> | |
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"> | |
<!-- Incluindo a biblioteca Font Awesome para os ícones --> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" crossorigin="anonymous" /> | |
<style> | |
/* Estilos Gerais */ | |
body { | |
font-family: 'Roboto', sans-serif; | |
background-color: #f4f1ea; | |
color: #333; | |
margin: 0; | |
padding: 0; | |
} | |
header { | |
background-color: #8B4513; | |
padding: 20px; | |
color: #fff; | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
} | |
header h1 { | |
margin: 0; | |
font-weight: 700; | |
display: flex; | |
align-items: center; | |
} | |
header h1 i { | |
margin-right: 10px; | |
} | |
/* Seção de Perfil */ | |
.profile-section { | |
display: flex; | |
align-items: center; | |
} | |
.profile-section span { | |
margin-right: 10px; | |
font-size: 0.9em; | |
font-weight: 500; | |
} | |
/* Estilo para a imagem de perfil */ | |
.profile-pic { | |
width: 50px; | |
height: 50px; | |
border-radius: 50%; | |
object-fit: cover; | |
border: 2px solid #fff; | |
} | |
/* Totalizadores Estilizados */ | |
.totals .card { | |
background-color: #fff; | |
border: none; | |
border-radius: 12px; | |
box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
margin-bottom: 20px; | |
} | |
.totals .card-body { | |
display: flex; | |
align-items: center; | |
} | |
.totals .card i { | |
font-size: 2.0em; | |
margin-right: 15px; | |
color: #8B4513; | |
} | |
.totals .card span { | |
font-size: 1.75em; | |
font-weight: 700; | |
} | |
/* Botões de Ação */ | |
.action-buttons .btn { | |
width: 48%; | |
font-weight: 500; | |
} | |
.btn-primary { | |
background-color: #A0522D; | |
border-color: #A0522D; | |
} | |
.btn-primary:hover { | |
background-color: #8B4513; | |
border-color: #8B4513; | |
} | |
.btn-success { | |
background-color: #CD853F; | |
border-color: #CD853F; | |
} | |
.btn-success:hover { | |
background-color: #A0522D; | |
border-color: #A0522D; | |
} | |
/* Estilização do Modal */ | |
.modal-header { | |
background-color: #8B4513; | |
color: #fff; | |
border-top-left-radius: 12px; | |
border-top-right-radius: 12px; | |
} | |
.modal-content { | |
border-radius: 12px; | |
} | |
.modal-footer .btn { | |
width: 100%; | |
font-weight: 500; | |
} | |
/* Tabs Personalizadas */ | |
.nav-tabs .nav-link.active { | |
background-color: #fff; | |
border-color: #dee2e6 #dee2e6 #fff; | |
color: #8B4513; | |
font-weight: 500; | |
} | |
.nav-tabs .nav-link { | |
color: #8B4513; | |
font-weight: 500; | |
} | |
/* Tabela Estilizada */ | |
table { | |
margin-top: 20px; | |
} | |
table th { | |
background-color: #e0d7c6; | |
color: #8B4513; | |
font-weight: 500; | |
} | |
table tbody tr:hover { | |
background-color: #f1f1f1; | |
} | |
/* Responsividade da Tabela */ | |
@media (max-width: 768px) { | |
.table thead th:nth-child(2), | |
.table thead th:nth-child(3), | |
.table tbody td:nth-child(2), | |
.table tbody td:nth-child(3) { | |
display: none; | |
} | |
} | |
/* Rodapé */ | |
footer { | |
background-color: #8B4513; | |
color: #fff; | |
text-align: center; | |
padding: 15px; | |
margin-top: 30px; | |
} | |
footer a { | |
color: #fff; | |
text-decoration: underline; | |
} | |
/* Botões no Rodapé */ | |
.footer-buttons { | |
display: flex; | |
justify-content: center; | |
flex-wrap: wrap; | |
gap: 10px; | |
margin-top: 15px; | |
} | |
.footer-buttons .btn { | |
font-size: 0.9em; | |
padding: 10px 20px; | |
background-color: #A0522D; | |
border-color: #A0522D; | |
color: #fff; | |
flex: 1 1 200px; | |
max-width: 300px; | |
} | |
.footer-buttons .btn:hover { | |
background-color: #8B4513; | |
border-color: #8B4513; | |
} | |
/* Estilo para mensagens de importação/exportação */ | |
.import-export-message { | |
margin-top: 10px; | |
} | |
</style> | |
</head> | |
<body> | |
<header> | |
<h1> | |
<i class="fas fa-poop"></i> Cocôladora | |
</h1> | |
<!-- Seção de Perfil --> | |
<div class="profile-section"> | |
<span>By: Dante Testa</span> | |
<a href="https://www.dantetesta.com.br" target="_blank"> | |
<img src="https://scontent.fcpq7-1.fna.fbcdn.net/v/t39.30808-6/316180582_10231057886298414_6381379649509653345_n.jpg?_nc_cat=101&ccb=1-7&_nc_sid=6ee11a&_nc_eui2=AeGmVRO4jXRHuRlvwwBXAGhGTdMuPWkADWBN0y49aQANYDjh0tXYpk1RqX0I-ILFNkc&_nc_ohc=yXUQlpx2hAwQ7kNvgEpZOoL&_nc_ht=scontent.fcpq7-1.fna&oh=00_AYC5QYszFmSzYc5PEpmFcCN75KQ4amhBvmn1PxJhC2p0Rg&oe=66F42D3D" alt="Dante Testa" class="profile-pic"> | |
</a> | |
</div> | |
</header> | |
<div class="container my-4"> | |
<!-- Totalizadores Estilizados --> | |
<div class="row totals"> | |
<div class="col-md-6"> | |
<div class="card"> | |
<div class="card-body"> | |
<i class="fas fa-calendar-alt"></i> | |
<div> | |
<span id="monthTotal">Total no Mês: R$ 0,00</span> | |
<p id="currentMonthYear" class="mb-0"></p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-6"> | |
<div class="card"> | |
<div class="card-body"> | |
<i class="fas fa-coins"></i> | |
<div> | |
<span id="overallTotal">Total Geral: R$ 0,00</span> | |
<p class="mb-0">Acumulado</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Botões de Ação --> | |
<div class="d-flex justify-content-between action-buttons mb-4"> | |
<button class="btn btn-primary" onclick="openModal('entryModal')"><i class="fas fa-plus"></i> Adicionar uma Cagada</button> | |
<button class="btn btn-success" onclick="openModal('chartModal')"><i class="fas fa-chart-bar"></i> Mostrar Gráfico</button> | |
</div> | |
<!-- Abas dos Meses --> | |
<ul class="nav nav-tabs" id="monthTabs"> | |
<!-- Abas serão geradas dinamicamente --> | |
</ul> | |
<!-- Contador de Lançamentos --> | |
<div id="entryCount" class="mt-3"> | |
<!-- Contador será gerado dinamicamente --> | |
</div> | |
<!-- Tabela de Registros --> | |
<div id="entriesTable"> | |
<!-- Tabela será gerada dinamicamente --> | |
</div> | |
</div> | |
<!-- Modal do Formulário --> | |
<div class="modal fade" id="entryModal" tabindex="-1" aria-labelledby="entryModalLabel" aria-hidden="true"> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Registrar Ida ao Banheiro</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body"> | |
<!-- Formulário --> | |
<div class="mb-3"> | |
<label for="salary" class="form-label">Salário Mensal (R$)</label> | |
<input type="text" class="form-control" id="salary" placeholder="Ex: R$ 3.000,00"> | |
<div class="text-danger" id="salaryError"></div> | |
</div> | |
<div class="mb-3"> | |
<label for="entryTime" class="form-label">Horário de Entrada</label> | |
<input type="time" class="form-control" id="entryTime"> | |
<div class="text-danger" id="entryTimeError"></div> | |
</div> | |
<div class="mb-3"> | |
<label for="exitTime" class="form-label">Horário de Saída</label> | |
<input type="time" class="form-control" id="exitTime"> | |
<div class="text-danger" id="exitTimeError"></div> | |
</div> | |
</div> | |
<div class="modal-footer"> | |
<button onclick="addEntry()" class="btn btn-primary">Adicionar Registro</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Modal do Gráfico --> | |
<div class="modal fade" id="chartModal" tabindex="-1" aria-labelledby="chartModalLabel" aria-hidden="true"> | |
<div class="modal-dialog modal-lg"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Ganhos Mensais</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body"> | |
<canvas id="earningsChart" width="400" height="200"></canvas> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Modal de Importação/Exportação --> | |
<div class="modal fade" id="importExportModal" tabindex="-1" aria-labelledby="importExportModalLabel" aria-hidden="true"> | |
<div class="modal-dialog modal-lg"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Importar/Exportar Dados</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body"> | |
<div class="row"> | |
<!-- Importar --> | |
<div class="col-md-6 mb-4"> | |
<h6>Importar CSV</h6> | |
<input type="file" id="importCSV" accept=".csv" class="form-control"> | |
<button class="btn btn-primary mt-2" onclick="handleImportCSV()">Importar</button> | |
<div id="importMessage" class="import-export-message"></div> | |
</div> | |
<!-- Exportar --> | |
<div class="col-md-6 mb-4"> | |
<h6>Exportar CSV</h6> | |
<button class="btn btn-success mt-2" onclick="exportToCSV()">Exportar</button> | |
<div id="exportMessage" class="import-export-message"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Rodapé --> | |
<footer> | |
<p>Cocôladora - by Dante Testa</p> | |
<p>Dante Testa - <a href="https://www.dantetesta.com.br" target="_blank">www.dantetesta.com.br</a></p> | |
<p>Feito com ChatGPT o1-preview - 21/09/2024</p> | |
<!-- Botões do Rodapé --> | |
<div class="footer-buttons"> | |
<button class="btn btn-secondary" onclick="generateDemoData()">Generate Demo</button> | |
<button class="btn btn-info" onclick="openModal('importExportModal')"><i class="fas fa-file-import"></i> Importar/Exportar</button> | |
<button class="btn btn-danger" onclick="resetData()">Reset All Data</button> | |
</div> | |
</footer> | |
<!-- Incluindo as bibliotecas necessárias --> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/cleave.js@1/dist/cleave.min.js"></script> | |
<script> | |
let entries = JSON.parse(localStorage.getItem('entries')) || []; | |
let selectedMonth = ''; | |
let selectedYear = new Date().getFullYear(); | |
// Variável para armazenar o salário | |
let savedSalary = localStorage.getItem('salary') || ''; | |
// Instância dos Modais | |
const entryModal = new bootstrap.Modal(document.getElementById('entryModal')); | |
const chartModal = new bootstrap.Modal(document.getElementById('chartModal')); | |
const importExportModal = new bootstrap.Modal(document.getElementById('importExportModal')); | |
function openModal(modalId) { | |
if (modalId === 'entryModal') { | |
if (savedSalary) { | |
salaryInput.setRawValue(savedSalary); | |
} | |
entryModal.show(); | |
} | |
if (modalId === 'chartModal') { | |
updateChart(); | |
chartModal.show(); | |
} | |
if (modalId === 'importExportModal') { | |
// Limpar mensagens anteriores | |
document.getElementById('importMessage').innerText = ''; | |
document.getElementById('exportMessage').innerText = ''; | |
importExportModal.show(); | |
} | |
} | |
function closeModal(modalId) { | |
if (modalId === 'entryModal') { | |
entryModal.hide(); | |
clearErrors(); | |
} | |
if (modalId === 'chartModal') { | |
chartModal.hide(); | |
} | |
if (modalId === 'importExportModal') { | |
importExportModal.hide(); | |
} | |
} | |
// Aplicando máscara monetária ao campo de salário | |
const salaryInputElement = document.getElementById('salary'); | |
const salaryInput = new Cleave(salaryInputElement, { | |
numeral: true, | |
numeralThousandsGroupStyle: 'thousand', | |
numeralDecimalMark: ',', | |
delimiter: '.', | |
numeralDecimalScale: 2, | |
prefix: 'R$ ', | |
noImmediatePrefix: true, | |
rawValueTrimPrefix: true | |
}); | |
function addEntry() { | |
// Limpar mensagens de erro | |
clearErrors(); | |
const entryTimeInput = document.getElementById('entryTime'); | |
const exitTimeInput = document.getElementById('exitTime'); | |
const salary = parseFloat(salaryInput.getRawValue().replace(',', '.')); | |
const entryTime = entryTimeInput.value; | |
const exitTime = exitTimeInput.value; | |
const date = new Date(); | |
let hasError = false; | |
if (!salary || salary <= 0) { | |
showError('salaryError', 'Informe um salário válido.'); | |
hasError = true; | |
} | |
if (!entryTime) { | |
showError('entryTimeError', 'Informe o horário de entrada.'); | |
hasError = true; | |
} | |
if (!exitTime) { | |
showError('exitTimeError', 'Informe o horário de saída.'); | |
hasError = true; | |
} | |
if (hasError) return; | |
// Salvar o salário no localStorage | |
savedSalary = salaryInput.getRawValue(); | |
localStorage.setItem('salary', savedSalary); | |
const start = new Date(`1970-01-01T${entryTime}:00`); | |
const end = new Date(`1970-01-01T${exitTime}:00`); | |
let diff = (end - start) / (1000 * 60); // Diferença em minutos | |
if (diff < 0) { | |
diff += 1440; // Ajuste para horários após meia-noite | |
} | |
const minutesPerMonth = 160 * 60; // Considerando 160 horas mensais de trabalho | |
const valuePerMinute = salary / minutesPerMonth; | |
const earned = (diff * valuePerMinute); | |
const entry = { | |
dateTime: date.toISOString(), // Armazenar data e hora completa | |
entryTime, | |
exitTime, | |
duration: diff, | |
earned, | |
month: date.getMonth(), | |
year: date.getFullYear() | |
}; | |
entries.push(entry); | |
localStorage.setItem('entries', JSON.stringify(entries)); | |
// Limpar campos após adicionar (mantendo o salário) | |
entryTimeInput.value = ''; | |
exitTimeInput.value = ''; | |
closeModal('entryModal'); | |
generateTabs(); | |
} | |
function clearErrors() { | |
document.getElementById('salaryError').innerText = ''; | |
document.getElementById('entryTimeError').innerText = ''; | |
document.getElementById('exitTimeError').innerText = ''; | |
} | |
function showError(elementId, message) { | |
document.getElementById(elementId).innerText = message; | |
} | |
function generateTabs() { | |
const tabsContainer = document.getElementById('monthTabs'); | |
tabsContainer.innerHTML = ''; | |
const months = [...new Set(entries | |
.filter(e => e.year === selectedYear) | |
.map(e => `${e.month}-${e.year}`))]; | |
months.sort((a, b) => { | |
const [monthA, yearA] = a.split('-').map(Number); | |
const [monthB, yearB] = b.split('-').map(Number); | |
return new Date(yearB, monthB) - new Date(yearA, monthA); | |
}); | |
if (months.length === 0) { | |
document.getElementById('entriesTable').innerHTML = '<p class="text-center mt-4">Nenhum registro disponível.</p>'; | |
document.getElementById('monthTotal').innerText = 'Total no Mês: R$ 0,00'; | |
document.getElementById('overallTotal').innerText = 'Total Geral: R$ 0,00'; | |
document.getElementById('entryCount').innerText = ''; | |
return; | |
} | |
months.forEach((monthYear, index) => { | |
const [month, year] = monthYear.split('-').map(Number); | |
const monthEntries = entries.filter(e => e.month === month && e.year === year); | |
if (monthEntries.length === 0) return; // Não exibe abas vazias | |
const tab = document.createElement('li'); | |
tab.className = 'nav-item'; | |
const tabLink = document.createElement('a'); | |
tabLink.className = 'nav-link'; | |
tabLink.innerText = getMonthName(monthYear); | |
tabLink.href = '#'; | |
tabLink.onclick = () => selectMonth(tabLink, monthYear); | |
if (selectedMonth === '' || selectedMonth === monthYear) { | |
selectedMonth = monthYear; | |
tabLink.classList.add('active'); | |
} | |
tab.appendChild(tabLink); | |
tabsContainer.appendChild(tab); | |
}); | |
showEntries(selectedMonth); | |
generateOverallTotal(); | |
} | |
function selectMonth(tabElement, monthYear) { | |
selectedMonth = monthYear; | |
document.querySelectorAll('.nav-link').forEach(tab => tab.classList.remove('active')); | |
tabElement.classList.add('active'); | |
showEntries(monthYear); | |
} | |
function getMonthName(monthYear) { | |
const [month, year] = monthYear.split('-').map(Number); | |
const date = new Date(year, month); | |
const shortMonth = date.toLocaleString('pt-BR', { month: 'short' }).replace('.', '').toUpperCase(); | |
const shortYear = year.toString().slice(-2); | |
return `${shortMonth} ${shortYear}`; | |
} | |
function showEntries(monthYear) { | |
const [month, year] = monthYear.split('-').map(Number); | |
const filteredEntries = entries | |
.filter(e => e.month === month && e.year === year) | |
.sort((a, b) => new Date(b.dateTime) - new Date(a.dateTime)); // Ordenação decrescente | |
const tableContainer = document.getElementById('entriesTable'); | |
tableContainer.innerHTML = ''; | |
const entryCountContainer = document.getElementById('entryCount'); | |
if (filteredEntries.length === 0) { | |
tableContainer.innerHTML = '<p class="text-center mt-4">Nenhum registro para este mês.</p>'; | |
document.getElementById('monthTotal').innerText = 'Total no Mês: R$ 0,00'; | |
entryCountContainer.innerText = ''; | |
return; | |
} | |
// Mostrar contador de lançamentos | |
entryCountContainer.innerHTML = `<p><strong>Total de Lançamentos no Mês:</strong> ${filteredEntries.length}</p>`; | |
const table = document.createElement('table'); | |
table.className = 'table table-striped'; | |
const header = table.createTHead(); | |
const headerRow = header.insertRow(); | |
headerRow.innerHTML = ` | |
<th>Data e Hora</th> | |
<th class="d-none d-md-table-cell">Entrada</th> | |
<th class="d-none d-md-table-cell">Saída</th> | |
<th>Duração</th> | |
<th>Valor</th> | |
<th>Ação</th> | |
`; | |
const tbody = table.createTBody(); | |
let monthTotal = 0; | |
filteredEntries.forEach((entry) => { | |
const row = tbody.insertRow(); | |
row.innerHTML = ` | |
<td>${formatDateTime(entry.dateTime)}</td> | |
<td class="d-none d-md-table-cell">${entry.entryTime}</td> | |
<td class="d-none d-md-table-cell">${entry.exitTime}</td> | |
<td>${entry.duration}min</td> | |
<td>${formatCurrency(entry.earned)}</td> | |
<td><button class="btn btn-sm btn-danger" onclick="removeEntry(${entries.indexOf(entry)})"><i class="fas fa-trash-alt"></i></button></td> | |
`; | |
monthTotal += entry.earned; | |
}); | |
tableContainer.appendChild(table); | |
document.getElementById('monthTotal').innerText = `Total no Mês: ${formatCurrency(monthTotal)}`; | |
document.getElementById('currentMonthYear').innerText = getFullMonthYear(month, year); | |
} | |
function generateOverallTotal() { | |
let overallTotal = entries.reduce((sum, entry) => sum + entry.earned, 0); | |
document.getElementById('overallTotal').innerText = `Total Geral: ${formatCurrency(overallTotal)}`; | |
} | |
function removeEntry(index) { | |
if (confirm('Tem certeza de que deseja remover este registro?')) { | |
entries.splice(index, 1); | |
localStorage.setItem('entries', JSON.stringify(entries)); | |
generateTabs(); | |
} | |
} | |
function formatDateTime(dateTimeStr) { | |
const date = new Date(dateTimeStr); | |
return date.toLocaleString('pt-BR', { | |
day: '2-digit', | |
month: '2-digit', | |
year: 'numeric', | |
hour: '2-digit', | |
minute: '2-digit', | |
hour12: false | |
}); | |
} | |
function formatCurrency(value) { | |
return value.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); | |
} | |
function getFullMonthYear(month, year) { | |
const date = new Date(year, month); | |
return date.toLocaleString('pt-BR', { month: 'long', year: 'numeric' }).replace(/^\w/, c => c.toUpperCase()); | |
} | |
// Função para resetar os dados | |
function resetData() { | |
if (confirm('Tem certeza de que deseja resetar todos os dados? Esta ação não pode ser desfeita.')) { | |
localStorage.clear(); | |
entries = []; | |
savedSalary = ''; | |
location.reload(); | |
} | |
} | |
// Função para gerar dados de demonstração corrigida | |
function generateDemoData() { | |
if (confirm('Isso irá gerar dados de demonstração e substituir seus dados atuais. Deseja continuar?')) { | |
// Limpar dados existentes | |
localStorage.clear(); | |
entries = []; | |
savedSalary = '3000,00'; | |
localStorage.setItem('salary', savedSalary); | |
const salary = 3000.00; // Salário fixo para a demonstração | |
const minutesPerMonth = 160 * 60; | |
const valuePerMinute = salary / minutesPerMonth; | |
const currentDate = new Date(); | |
for (let m = 0; m < 12; m++) { | |
let month = currentDate.getMonth() - m; | |
let year = currentDate.getFullYear(); | |
if (month < 0) { | |
month += 12; | |
year -= 1; | |
} | |
const monthDate = new Date(year, month, 1); | |
const daysInMonth = new Date(year, month + 1, 0).getDate(); | |
// Simulando de 1 a 3 cagadas por dia útil | |
for (let d = 1; d <= daysInMonth; d++) { | |
const dayOfWeek = new Date(year, month, d).getDay(); | |
if (dayOfWeek === 0 || dayOfWeek === 6) continue; // Pular finais de semana | |
const numCagadas = Math.floor(Math.random() * 3) + 1; | |
for (let c = 0; c < numCagadas; c++) { | |
const entryHour = Math.floor(Math.random() * 8) + 8; // Entre 8h e 16h | |
const entryMinute = Math.floor(Math.random() * 60); | |
const duration = Math.floor(Math.random() * 15) + 5; // Duração entre 5 e 20 minutos | |
let exitHour = entryHour + Math.floor((entryMinute + duration) / 60); | |
let exitMinute = (entryMinute + duration) % 60; | |
if (exitHour >= 24) { // Ajustar caso ultrapasse 24h | |
exitHour -= 24; | |
} | |
const entryTime = `${String(entryHour).padStart(2, '0')}:${String(entryMinute).padStart(2, '0')}`; | |
const exitTime = `${String(exitHour).padStart(2, '0')}:${String(exitMinute).padStart(2, '0')}`; | |
const earned = duration * valuePerMinute; | |
const date = new Date(year, month, d, entryHour, entryMinute); | |
const entry = { | |
dateTime: date.toISOString(), | |
entryTime, | |
exitTime, | |
duration, | |
earned, | |
month: date.getMonth(), | |
year: date.getFullYear() | |
}; | |
entries.push(entry); | |
} | |
} | |
} | |
localStorage.setItem('entries', JSON.stringify(entries)); | |
generateTabs(); | |
alert('Dados de demonstração gerados com sucesso!'); | |
} | |
} | |
// Função para atualizar o gráfico | |
function updateChart() { | |
const ctx = document.getElementById('earningsChart').getContext('2d'); | |
const monthlyEarnings = Array(12).fill(0); | |
entries.forEach(entry => { | |
if (entry.year === selectedYear) { | |
monthlyEarnings[entry.month] += entry.earned; | |
} | |
}); | |
const data = { | |
labels: ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'], | |
datasets: [{ | |
label: 'Ganhos Mensais', | |
data: monthlyEarnings, | |
backgroundColor: '#8B4513', | |
}] | |
}; | |
if (window.myChart instanceof Chart) { | |
window.myChart.destroy(); | |
} | |
window.myChart = new Chart(ctx, { | |
type: 'bar', | |
data: data, | |
options: { | |
plugins: { | |
tooltip: { | |
callbacks: { | |
label: function(context) { | |
return formatCurrency(context.parsed.y); | |
} | |
} | |
} | |
}, | |
scales: { | |
y: { | |
ticks: { | |
callback: function(value) { | |
return formatCurrency(value); | |
} | |
} | |
} | |
} | |
} | |
}); | |
} | |
// Função para exportar os dados em CSV | |
function exportToCSV() { | |
if (entries.length === 0) { | |
document.getElementById('exportMessage').innerText = 'Nenhum dado para exportar.'; | |
document.getElementById('exportMessage').style.color = 'red'; | |
return; | |
} | |
const header = ['dateTime', 'entryTime', 'exitTime', 'duration', 'earned', 'month', 'year']; | |
const csvRows = []; | |
csvRows.push(header.join(',')); | |
entries.forEach(entry => { | |
const values = header.map(field => { | |
let val = entry[field]; | |
// Escape double quotes | |
if (typeof val === 'string') { | |
val = val.replace(/"/g, '""'); | |
} | |
// Enclose in double quotes if necessary | |
if (typeof val === 'string' && (val.includes(',') || val.includes('"') || val.includes('\n'))) { | |
val = `"${val}"`; | |
} | |
return val; | |
}); | |
csvRows.push(values.join(',')); | |
}); | |
const csvContent = csvRows.join('\n'); | |
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); | |
const url = URL.createObjectURL(blob); | |
const link = document.createElement("a"); | |
link.setAttribute("href", url); | |
const currentDate = new Date(); | |
const timestamp = currentDate.toISOString().slice(0,10); | |
link.setAttribute("download", `Cocoladora_Data_${timestamp}.csv`); | |
link.style.visibility = 'hidden'; | |
document.body.appendChild(link); | |
link.click(); | |
document.body.removeChild(link); | |
document.getElementById('exportMessage').innerText = 'Dados exportados com sucesso!'; | |
document.getElementById('exportMessage').style.color = 'green'; | |
} | |
// Função para importar dados a partir de um CSV | |
function handleImportCSV() { | |
const fileInput = document.getElementById('importCSV'); | |
const file = fileInput.files[0]; | |
const importMessage = document.getElementById('importMessage'); | |
if (!file) { | |
importMessage.innerText = 'Por favor, selecione um arquivo CSV para importar.'; | |
importMessage.style.color = 'red'; | |
return; | |
} | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
const text = e.target.result; | |
const parsedEntries = parseCSV(text); | |
if (parsedEntries.error) { | |
importMessage.innerText = parsedEntries.error; | |
importMessage.style.color = 'red'; | |
return; | |
} | |
// Validar e adicionar entradas | |
const newEntries = parsedEntries.data; | |
let valid = true; | |
newEntries.forEach(entry => { | |
if (!entry.dateTime || !entry.entryTime || !entry.exitTime || typeof entry.duration !== 'number' || typeof entry.earned !== 'number' || typeof entry.month !== 'number' || typeof entry.year !== 'number') { | |
valid = false; | |
} | |
}); | |
if (!valid) { | |
importMessage.innerText = 'O arquivo CSV possui um formato inválido.'; | |
importMessage.style.color = 'red'; | |
return; | |
} | |
// Atualizar entries e localStorage | |
entries = entries.concat(newEntries); | |
localStorage.setItem('entries', JSON.stringify(entries)); | |
generateTabs(); | |
importMessage.innerText = 'Dados importados com sucesso!'; | |
importMessage.style.color = 'green'; | |
// Limpar o input de arquivo | |
fileInput.value = ''; | |
}; | |
reader.onerror = function() { | |
importMessage.innerText = 'Erro ao ler o arquivo.'; | |
importMessage.style.color = 'red'; | |
}; | |
reader.readAsText(file); | |
} | |
// Função auxiliar para parsear CSV | |
function parseCSV(text) { | |
const lines = text.split('\n').filter(line => line.trim() !== ''); | |
if (lines.length < 2) { | |
return { error: 'O arquivo CSV deve conter pelo menos um registro.' }; | |
} | |
const header = lines[0].split(',').map(h => h.trim()); | |
const requiredFields = ['dateTime', 'entryTime', 'exitTime', 'duration', 'earned', 'month', 'year']; | |
// Verificar se todos os campos necessários estão presentes | |
for (let field of requiredFields) { | |
if (!header.includes(field)) { | |
return { error: `Campo "${field}" ausente no cabeçalho do CSV.` }; | |
} | |
} | |
const data = []; | |
for (let i = 1; i < lines.length; i++) { | |
const line = lines[i]; | |
const values = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g); | |
if (!values || values.length !== header.length) { | |
return { error: `Erro na linha ${i + 1}: número de campos inválido.` }; | |
} | |
const entry = {}; | |
for (let j = 0; j < header.length; j++) { | |
let value = values[j]; | |
if (value.startsWith('"') && value.endsWith('"')) { | |
value = value.slice(1, -1).replace(/""/g, '"'); | |
} | |
if (header[j] === 'duration' || header[j] === 'earned' || header[j] === 'month' || header[j] === 'year') { | |
value = Number(value); | |
if (isNaN(value)) { | |
return { error: `Valor inválido para "${header[j]}" na linha ${i + 1}.` }; | |
} | |
} | |
entry[header[j]] = value; | |
} | |
data.push(entry); | |
} | |
return { data }; | |
} | |
window.onload = function() { | |
generateTabs(); | |
}; | |
</script> | |
<center> | |
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4762717586329786" | |
crossorigin="anonymous"></script> | |
<!-- Blog-DanteTesta-News --> | |
<ins class="adsbygoogle" | |
style="display:block" | |
data-ad-client="ca-pub-4762717586329786" | |
data-ad-slot="7391598107" | |
data-ad-format="auto" | |
data-full-width-responsive="true"></ins> | |
<script> | |
(adsbygoogle = window.adsbygoogle || []).push({}); | |
</script> | |
</center> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment