Código.gs
/**
* Gestão de Demandas - TUTORIAL APPS SCRIPT
*/
const SHEET_TAREFAS = 'Tarefas';
const HEADERS_TAREFAS = ["ID_Tarefa", "Titulo", "Descricao", "Status", "Responsavel", "Data_Limite"];
/**
* Ponto de entrada leve (doGet).
* Não executa rotinas pesadas para garantir o menor tempo de resposta inicial (TTFB).
*/
function doGet() {
return HtmlService.createTemplateFromFile('Index')
.evaluate()
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no')
.setTitle('Gestão de Demandas');
}
/**
* Garante a existência da aba e estrutura de dados.
* Executado estritamente nas rotinas de gravação (CRUD) para segurança.
*/
function setupSheet() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(SHEET_TAREFAS);
if (!sheet) {
sheet = ss.insertSheet(SHEET_TAREFAS);
sheet.appendRow(HEADERS_TAREFAS);
sheet.setFrozenRows(1);
}
return sheet;
}
/**
* Leitura de dados otimizada (Não chama o setupSheet para evitar API calls redundantes).
*/
function getTasks() {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_TAREFAS);
if (!sheet) return [];
const lastRow = sheet.getLastRow();
if (lastRow <= 1) return [];
const values = sheet.getRange(2, 1, lastRow - 1, HEADERS_TAREFAS.length).getValues();
return values.map(row => {
const task = {};
HEADERS_TAREFAS.forEach((header, index) => {
let val = row[index];
if (val instanceof Date) {
task[header] = Utilities.formatDate(val, Session.getScriptTimeZone(), "yyyy-MM-dd");
} else {
task[header] = val;
}
});
return task;
}).filter(t => t.ID_Tarefa);
} catch (e) {
Logger.log(`Erro ao buscar tarefas: ${e.message}`);
return { error: e.message };
}
}
/**
* Salva ou edita uma tarefa.
*/
function saveTask(taskData) {
try {
const sheet = setupSheet();
const isEdit = taskData.ID_Tarefa ? true : false;
const taskId = isEdit ? taskData.ID_Tarefa : "TSK_" + new Date().getTime();
// Normaliza a data recebida (YYYY-MM-DD) para salvar no formato padrão string (DD/MM/YYYY)
let formattedDate = taskData.Data_Limite;
if (formattedDate && formattedDate.includes('-')) {
const [y, m, d] = formattedDate.split('-');
formattedDate = `${d}/${m}/${y}`;
}
const taskToSave = {
ID_Tarefa: taskId,
Titulo: taskData.Titulo,
Descricao: taskData.Descricao,
Status: taskData.Status || "A Fazer",
Responsavel: taskData.Responsavel,
Data_Limite: formattedDate
};
if (isEdit) {
const { rowIndex } = findRowById(sheet, taskId);
if (rowIndex === -1) throw new Error("Tarefa não encontrada.");
const updatedRow = HEADERS_TAREFAS.map(h => taskToSave[h]);
sheet.getRange(rowIndex, 1, 1, HEADERS_TAREFAS.length).setValues([updatedRow]);
} else {
const newRow = HEADERS_TAREFAS.map(h => taskToSave[h]);
sheet.appendRow(newRow);
}
return { success: true, taskId: taskId };
} catch (e) {
return { error: e.message };
}
}
/**
* Atualização rápida de status (Disparado nativamente no evento 'drop' do front-end).
*/
function updateTaskStatus(taskId, newStatus) {
try {
const sheet = setupSheet();
const { rowIndex } = findRowById(sheet, taskId);
if (rowIndex === -1) throw new Error("Tarefa não encontrada.");
const statusColIndex = HEADERS_TAREFAS.indexOf("Status") + 1;
sheet.getRange(rowIndex, statusColIndex).setValue(newStatus);
return { success: true };
} catch (e) {
return { error: e.message };
}
}
/**
* Exclui uma tarefa.
*/
function deleteTask(taskId) {
try {
const sheet = setupSheet();
const { rowIndex } = findRowById(sheet, taskId);
if (rowIndex === -1) throw new Error("Tarefa não encontrada.");
sheet.deleteRow(rowIndex);
return { success: true };
} catch (e) {
return { error: e.message };
}
}
/**
* Função utilitária para localizar a linha do registro.
*/
function findRowById(sheet, id) {
const data = sheet.getDataRange().getValues();
const idColIndex = HEADERS_TAREFAS.indexOf("ID_Tarefa");
for (let i = 1; i < data.length; i++) {
if (String(data[i][idColIndex]).trim() === String(id).trim()) {
return { rowIndex: i + 1 };
}
}
return { rowIndex: -1 };
}
Index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestão de Demandas</title>
<!-- Ícones do Google -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
/* --- DESIGN VIBRANT TECH LIGHT (NEON & TINTED SENSATIONS) --- */
:root {
--bg-body: #f4f7fc; /* Fundo azul-claro tecnológico, zero cinza */
--card-bg: #ffffff; /* Branco puro brilhante para os cartões */
--text-main: #0f172a; /* Azul escuro quase preto */
--text-muted: #475569; /* Slate escuro para descrições */
/* Cores Vibrantes Neon para Light Mode */
--primary: #4f46e5; /* Indigo brilhante */
--primary-glow: rgba(79, 70, 229, 0.15);
--todo-color: #ff4d4d; /* Vermelho vibrante */
--todo-bg: #fff5f5; /* Fundo sutil rosado */
--doing-color: #ff9f00; /* Laranja dourado */
--doing-bg: #fffbeb; /* Fundo sutil âmbar */
--done-color: #00ca84; /* Verde esmeralda vivo */
--done-bg: #f0fdf4; /* Fundo sutil menta */
--border-radius: 14px;
--transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: var(--bg-body);
color: var(--text-main);
padding: 30px 20px;
min-height: 100vh;
/* Gradiente de fundo de alta tecnologia */
background-image:
radial-gradient(circle at 10% 20%, rgba(79, 70, 229, 0.05) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(0, 202, 132, 0.04) 0%, transparent 40%);
}
/* --- CABEÇALHO GLOSSY --- */
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto 35px auto;
flex-wrap: wrap;
gap: 15px;
}
.app-header h1 {
font-size: 2rem;
font-weight: 900;
letter-spacing: -0.7px;
display: flex;
align-items: center;
gap: 12px;
/* Gradiente no título para impacto imediato */
background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border: 1px solid transparent;
border-radius: var(--border-radius);
cursor: pointer;
font-weight: 700;
font-size: 0.95rem;
transition: var(--transition);
}
.btn-primary {
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
color: white;
box-shadow: 0 4px 15px rgba(79, 70, 229, 0.25);
}
.btn-primary:hover {
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4);
transform: translateY(-2px);
}
.btn-secondary {
background-color: #ffffff;
color: var(--text-main);
border-color: #e2e8f0;
}
.btn-secondary:hover {
background-color: #f8fafc;
border-color: #cbd5e1;
}
/* --- GRID KANBAN VIBRANTE --- */
.kanban-board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
max-width: 1200px;
margin: 0 auto;
align-items: start;
}
.kanban-column {
border-radius: var(--border-radius);
padding: 22px;
min-height: 600px;
border: 1px solid rgba(226, 232, 240, 0.8);
transition: var(--transition);
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.02);
}
/* Cores e fundos customizados para quebrar a frieza do cinza */
.kanban-column[data-status="A Fazer"] {
background-color: var(--todo-bg);
border-top: 4px solid var(--todo-color);
}
.kanban-column[data-status="Em Andamento"] {
background-color: var(--doing-bg);
border-top: 4px solid var(--doing-color);
}
.kanban-column[data-status="Concluido"] {
background-color: var(--done-bg);
border-top: 4px solid var(--done-color);
}
.kanban-column.drag-over {
transform: scale(1.01);
box-shadow: 0 10px 30px rgba(79, 70, 229, 0.08);
}
.column-header {
margin-bottom: 22px;
font-size: 1.1rem;
font-weight: 800;
display: flex;
justify-content: space-between;
align-items: center;
}
.kanban-column[data-status="A Fazer"] .column-header { color: #b91c1c; }
.kanban-column[data-status="Em Andamento"] .column-header { color: #b45309; }
.kanban-column[data-status="Concluido"] .column-header { color: #047857; }
.column-body {
display: flex;
flex-direction: column;
gap: 16px;
min-height: 500px;
}
/* --- CARDS DE TAREFA COLOR-BAR (ESTILO METODOLOGIA ÁGIL) --- */
.task-card {
background-color: var(--card-bg);
border: 1px solid rgba(226, 232, 240, 0.8);
border-radius: var(--border-radius);
padding: 18px;
cursor: grab;
transition: var(--transition);
position: relative;
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.03);
}
/* Borda lateral grossa que identifica o status e traz cor imediata */
.task-card[data-status="A Fazer"] { border-left: 5px solid var(--todo-color); }
.task-card[data-status="Em Andamento"] { border-left: 5px solid var(--doing-color); }
.task-card[data-status="Concluido"] { border-left: 5px solid var(--done-color); }
.task-card:hover {
transform: translateY(-4px);
/* Glow shadow sutil combinando com o status */
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
}
.task-card.dragging {
opacity: 0.4;
transform: scale(0.97);
}
.task-title {
font-size: 1.05rem;
font-weight: 800;
color: var(--text-main);
margin-bottom: 8px;
padding-right: 65px;
}
.task-description {
font-size: 0.88rem;
color: var(--text-muted);
margin-bottom: 15px;
line-height: 1.5;
}
.task-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
border-top: 1px solid rgba(226, 232, 240, 0.6);
padding-top: 12px;
}
.task-assignee {
font-weight: 700;
color: #334155;
display: inline-flex;
align-items: center;
gap: 6px;
background: #f1f5f9;
padding: 5px 12px;
border-radius: 12px;
}
/* --- BADGES VIBRANTES COM FUNDO SUAVE --- */
.badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 800;
display: inline-flex;
align-items: center;
gap: 4px;
}
.badge-success { background-color: rgba(16, 185, 129, 0.15); color: #047857; }
.badge-future { background-color: rgba(79, 70, 229, 0.1); color: #4f46e5; }
.badge-today { background-color: rgba(245, 158, 11, 0.15); color: #b45309; }
.badge-overdue { background-color: rgba(239, 68, 68, 0.12); color: #b91c1c; }
/* --- BOTÕES DE AÇÕES MODERNOS --- */
.task-actions {
position: absolute;
top: 16px;
right: 16px;
display: flex;
gap: 4px;
}
.btn-icon {
background: #ffffff;
border: 1px solid #e2e8f0;
cursor: pointer;
color: #94a3b8;
border-radius: 50%;
padding: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.btn-icon:hover {
background-color: #f8fafc;
border-color: #cbd5e1;
transform: scale(1.1);
}
.btn-icon.edit:hover { color: var(--primary); }
.btn-icon.delete:hover { color: var(--todo-color); }
.mobile-arrows {
display: none; /* Escondido por padrão no Desktop */
gap: 6px;
}
.btn-arrow {
background: #f1f5f9;
border: 1px solid #cbd5e1;
color: #334155;
border-radius: 8px;
padding: 4px 8px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.btn-arrow:active {
background-color: #cbd5e1;
}
/* --- MODAIS COM BLUR --- */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.4);
backdrop-filter: blur(5px);
z-index: 1000;
display: none;
}
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: var(--border-radius);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
z-index: 1001;
width: 95%;
max-width: 450px;
padding: 25px;
display: none;
}
.modal.open, .modal-backdrop.open {
display: block;
}
.modal-header {
font-size: 1.3rem;
font-weight: 900;
margin-bottom: 20px;
border-bottom: 1px solid #f1f5f9;
padding-bottom: 12px;
color: var(--text-main);
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
font-size: 0.85rem;
font-weight: 700;
margin-bottom: 6px;
color: #334155;
}
.form-control {
width: 100%;
padding: 10px 14px;
background: #ffffff;
border: 1px solid #cbd5e1;
border-radius: var(--border-radius);
font-size: 0.95rem;
outline: none;
color: var(--text-main);
transition: var(--transition);
}
.form-control:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 25px;
}
/* --- FEEDBACKS VISUAIS --- */
#loader {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(244, 247, 252, 0.6);
backdrop-filter: blur(4px);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
#loader.hidden { display: none; }
.spinner {
width: 40px;
height: 40px;
border: 3px solid #e2e8f0;
border-left-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.toast {
position: fixed;
top: 25px;
left: 50%;
transform: translateX(-50%) translateY(-100px);
background: #0f172a;
color: white;
padding: 14px 28px;
border-radius: 30px;
font-weight: 700;
z-index: 9999;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.toast.show {
transform: translateX(-50%) translateY(0);
}
.toast.success { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; }
.toast.error { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); color: white; }
/* --- RESPONSIVIDADE --- */
@media (max-width: 768px) {
body { padding: 15px 10px; }
.app-header h1 { font-size: 1.6rem; }
.kanban-board { grid-template-columns: 1fr; gap: 20px; }
.kanban-column { min-height: auto; padding: 15px; }
.column-body { min-height: 150px; }
.mobile-arrows {
display: inline-flex;
}
}
</style>
</head>
<body>
<!-- Bloqueador de Tela contra Cliques Duplos -->
<div id="loader">
<div class="spinner"></div>
</div>
<!-- Alerta Flutuante (Toast) -->
<div id="toast" class="toast">Mensagem</div>
<!-- Cabeçalho Principal -->
<header class="app-header">
<h1><span class="material-icons" style="color: var(--primary)">assignment</span> Gestão de Demandas</h1>
<button class="btn btn-primary" onclick="openFormModal()"><span class="material-icons">add</span> Nova Tarefa</button>
</header>
<!-- Quadro Kanban Principal -->
<main class="kanban-board">
<!-- Coluna: A Fazer -->
<div class="kanban-column" data-status="A Fazer">
<div class="column-header">📋 A Fazer</div>
<div class="column-body" id="col-todo"></div>
</div>
<!-- Coluna: Em Andamento -->
<div class="kanban-column" data-status="Em Andamento">
<div class="column-header">⚡ Em Andamento</div>
<div class="column-body" id="col-doing"></div>
</div>
<!-- Coluna: Concluído -->
<div class="kanban-column" data-status="Concluido">
<div class="column-header">✅ Concluído</div>
<div class="column-body" id="col-done"></div>
</div>
</main>
<!-- ========================================================================== -->
<!-- MODAIS E COMPONENTES -->
<!-- ========================================================================== -->
<div class="modal-backdrop" id="modalBackdrop" onclick="closeAllModals()"></div>
<!-- Modal do Formulário de Tarefa -->
<div class="modal" id="taskModal">
<div class="modal-header" id="modalTitle">Nova Tarefa</div>
<form id="taskForm" onsubmit="handleFormSubmit(event)">
<input type="hidden" id="ID_Tarefa">
<div class="form-group">
<label for="Titulo">Título da Tarefa</label>
<input type="text" id="Titulo" class="form-control" required placeholder="Ex: Criar modelo de relatório">
</div>
<div class="form-group">
<label for="Descricao">Descrição</label>
<textarea id="Descricao" class="form-control" rows="3" placeholder="Detalhes da entrega..."></textarea>
</div>
<div class="form-group">
<label for="Responsavel">Responsável</label>
<input type="text" id="Responsavel" class="form-control" required placeholder="Nome do executor">
</div>
<div class="form-group">
<label for="Data_Limite">Data de Entrega</label>
<input type="date" id="Data_Limite" class="form-control" required>
</div>
<div class="form-group" id="statusGroup" style="display: none;">
<label for="Status">Status</label>
<select id="Status" class="form-control">
<option value="A Fazer">A Fazer</option>
<option value="Em Andamento">Em Andamento</option>
<option value="Concluido">Concluído</option>
</select>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="closeAllModals()">Cancelar</button>
<button type="submit" class="btn btn-primary">Salvar</button>
</div>
</form>
</div>
<!-- Modal de Confirmação de Exclusão -->
<div class="modal" id="confirmModal">
<div class="modal-header">Excluir Tarefa?</div>
<p style="font-size: 0.95rem; margin-bottom: 20px;">Esta ação removerá a tarefa definitivamente da sua planilha de dados. Deseja continuar?</p>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeAllModals()">Não</button>
<button class="btn btn-primary" style="background-color: #e74c3c" id="btnConfirmDelete">Sim, Excluir</button>
</div>
</div>
<!-- ========================================================================== -->
<!-- JAVASCRIPT FRONT-END -->
<!-- ========================================================================== -->
<script>
let localTasks = []; // Memória local de tarefas (SPA State)
let currentDeleteId = null;
// Inicialização ao carregar o DOM
document.addEventListener('DOMContentLoaded', () => {
loadTasks();
setupDragAndDropEvents();
});
/**
* Carrega e renderiza as tarefas vindas da planilha.
*/
function loadTasks() {
showLoader(true);
google.script.run
.withSuccessHandler(tasks => {
showLoader(false);
if (tasks.error) {
showToast(tasks.error, 'error');
return;
}
localTasks = tasks;
renderKanban();
})
.withFailureHandler(err => {
showLoader(false);
showToast("Erro ao se comunicar com o servidor: " + err.message, 'error');
})
.getTasks();
}
/**
* Renderiza os cards nas colunas corretas e ativa as interações de Drag.
*/
function renderKanban() {
const colTodo = document.getElementById('col-todo');
const colDoing = document.getElementById('col-doing');
const colDone = document.getElementById('col-done');
colTodo.innerHTML = '';
colDoing.innerHTML = '';
colDone.innerHTML = '';
localTasks.forEach(task => {
const cardHtml = createTaskCard(task);
if (task.Status === 'A Fazer') colTodo.innerHTML += cardHtml;
else if (task.Status === 'Em Andamento') colDoing.innerHTML += cardHtml;
else if (task.Status === 'Concluido') colDone.innerHTML += cardHtml;
});
// Anexa os eventos de drag nos novos elementos gerados
attachDragToCards();
}
/**
* Gera a estrutura HTML de um cartão de tarefa com os selos de prazo inteligentes e setas mobile.
*/
function createTaskCard(task) {
const deadlineBadge = getDeadlineBadge(task.Status, task.Data_Limite);
// Controla a visibilidade das setas mobile dependendo do status atual
const showLeft = task.Status !== "A Fazer" ? 'inline-flex' : 'none';
const showRight = task.Status !== "Concluido" ? 'inline-flex' : 'none';
const mobileArrowsHtml = `
<div class="mobile-arrows" onclick="event.stopPropagation()">
<button class="btn-arrow" style="display: ${showLeft};" onclick="moveTaskMobile('${task.ID_Tarefa}', 'left', event)" title="Voltar Etapa">
<span class="material-icons" style="font-size: 0.95rem">arrow_back</span>
</button>
<button class="btn-arrow" style="display: ${showRight};" onclick="moveTaskMobile('${task.ID_Tarefa}', 'right', event)" title="Avançar Etapa">
<span class="material-icons" style="font-size: 0.95rem">arrow_forward</span>
</button>
</div>
`;
return `
<div class="task-card" draggable="true" data-id="${task.ID_Tarefa}" data-status="${task.Status}">
<div class="task-title">${task.Titulo}</div>
<p class="task-description">${task.Descricao || 'Sem descrição.'}</p>
<div class="task-footer">
<div class="task-assignee">
<span class="material-icons" style="font-size: 1rem; color: #7f8c8d;">person</span>
${task.Responsavel}
</div>
<div style="display: flex; align-items: center; gap: 8px;">
${deadlineBadge}
${mobileArrowsHtml}
</div>
</div>
<div class="task-actions">
<button class="btn-icon edit" onclick="openFormModal('${task.ID_Tarefa}')" title="Editar"><span class="material-icons" style="font-size: 1.1rem">edit</span></button>
<button class="btn-icon delete" onclick="confirmDelete('${task.ID_Tarefa}')" title="Excluir"><span class="material-icons" style="font-size: 1.1rem">delete</span></button>
</div>
</div>
`;
}
/**
* Calcula dinamicamente o status do prazo baseado na data de entrega (DD/MM/YYYY).
*/
function getDeadlineBadge(status, limitDateStr) {
if (status === "Concluido") {
return '<span class="badge badge-success">✔️ Entregue</span>';
}
if (!limitDateStr) return '';
// Transforma DD/MM/YYYY em um Date object funcional
const parts = limitDateStr.split('/');
if (parts.length !== 3) return '';
const limitDate = new Date(parts[2], parts[1] - 1, parts[0], 12, 0, 0);
// Normaliza a data atual
const today = new Date();
today.setHours(12, 0, 0, 0);
const diffTime = limitDate - today;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays > 1) {
return `<span class="badge badge-future">🟢 Faltam ${diffDays} dias</span>`;
} else if (diffDays === 1) {
return '<span class="badge badge-future">🟢 Falta 1 dia</span>';
} else if (diffDays === 0) {
return '<span class="badge badge-today">🟡 Vence hoje!</span>';
} else {
const absoluteDays = Math.abs(diffDays);
return `<span class="badge badge-overdue">🔴 Atrasada há ${absoluteDays} dia${absoluteDays > 1 ? 's' : ''}</span>`;
}
}
// ==========================================================================
// LÓGICA DO ARRASTAR E SOLTAR (HTML5 DRAG AND DROP NATIVO)
// ==========================================================================
function setupDragAndDropEvents() {
const columns = document.querySelectorAll('.kanban-column');
columns.forEach(col => {
col.addEventListener('dragover', function(e) {
e.preventDefault(); // Necessário para habilitar o Drop
});
col.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
col.addEventListener('dragleave', function() {
this.classList.remove('drag-over');
});
col.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
const taskId = e.dataTransfer.getData('text/plain');
const card = document.querySelector(`.task-card[data-id="${taskId}"]`);
if (!card) return;
const newStatus = this.dataset.status;
const oldStatus = card.dataset.status;
// Se soltou na mesma coluna, cancela o processo
if (oldStatus === newStatus) return;
// Executa a transição visual imediata no front-end para maior fluidez
this.querySelector('.column-body').appendChild(card);
card.dataset.status = newStatus;
// Bloqueia interações e salva a mudança no banco de dados (planilha)
showLoader(true);
google.script.run
.withSuccessHandler(response => {
showLoader(false);
if (response.error) {
showToast("Erro ao mover: " + response.error, "error");
loadTasks(); // Recarrega para voltar à posição original
} else {
showToast("Tarefa atualizada!", "success");
loadTasks(); // Recarrega para recalcular prazos/badges dinâmicos
}
})
.withFailureHandler(err => {
showLoader(false);
showToast("Falha de rede: " + err.message, "error");
loadTasks();
})
.updateTaskStatus(taskId, newStatus);
});
});
}
function attachDragToCards() {
const cards = document.querySelectorAll('.task-card');
cards.forEach(card => {
card.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', this.dataset.id);
this.classList.add('dragging');
});
card.addEventListener('dragend', function() {
this.classList.remove('dragging');
});
});
}
/**
* Lógica para mover tarefas via cliques nas setas (Mobile-friendly).
*/
function moveTaskMobile(taskId, direction, event) {
if (event) event.stopPropagation(); // Impede abertura de modais ou disparos de cliques indesejados
const task = localTasks.find(t => t.ID_Tarefa === taskId);
if (!task) return;
let newStatus = task.Status;
if (direction === 'right') {
if (task.Status === 'A Fazer') newStatus = 'Em Andamento';
else if (task.Status === 'Em Andamento') newStatus = 'Concluido';
} else if (direction === 'left') {
if (task.Status === 'Concluido') newStatus = 'Em Andamento';
else if (task.Status === 'Em Andamento') newStatus = 'A Fazer';
}
if (newStatus === task.Status) return;
showLoader(true);
google.script.run
.withSuccessHandler(response => {
showLoader(false);
if (response.error) {
showToast("Erro ao mover: " + response.error, "error");
} else {
showToast("Demanda movimentada!", "success");
loadTasks();
}
})
.withFailureHandler(err => {
showLoader(false);
showToast("Falha de rede: " + err.message, "error");
})
.updateTaskStatus(taskId, newStatus);
}
// ==========================================================================
// OPERAÇÕES DO FORMULÁRIO (CRIAR / EDITAR / DELETAR)
// ==========================================================================
function openFormModal(taskId = '') {
const form = document.getElementById('taskForm');
form.reset();
document.getElementById('ID_Tarefa').value = taskId;
const statusGroup = document.getElementById('statusGroup');
if (taskId) {
// Modo Edição
document.getElementById('modalTitle').innerText = "Editar Tarefa";
statusGroup.style.display = 'block';
const task = localTasks.find(t => t.ID_Tarefa === taskId);
if (task) {
document.getElementById('Titulo').value = task.Titulo;
document.getElementById('Descricao').value = task.Descricao;
document.getElementById('Responsavel').value = task.Responsavel;
document.getElementById('Status').value = task.Status;
// Transforma data no formato DD/MM/YYYY para YYYY-MM-DD para o seletor HTML
if (task.Data_Limite) {
const parts = task.Data_Limite.split('/');
if (parts.length === 3) {
document.getElementById('Data_Limite').value = `${parts[2]}-${parts[1]}-${parts[0]}`;
}
}
}
} else {
// Modo Nova Tarefa
document.getElementById('modalTitle').innerText = "Nova Tarefa";
statusGroup.style.display = 'none';
document.getElementById('Data_Limite').valueAsDate = new Date();
}
document.getElementById('modalBackdrop').classList.add('open');
document.getElementById('taskModal').classList.add('open');
document.getElementById('Titulo').focus();
}
function closeAllModals() {
document.getElementById('modalBackdrop').classList.remove('open');
document.getElementById('taskModal').classList.remove('open');
document.getElementById('confirmModal').classList.remove('open');
}
function handleFormSubmit(e) {
e.preventDefault();
closeAllModals();
showLoader(true);
const taskData = {
ID_Tarefa: document.getElementById('ID_Tarefa').value,
Titulo: document.getElementById('Titulo').value,
Descricao: document.getElementById('Descricao').value,
Responsavel: document.getElementById('Responsavel').value,
Data_Limite: document.getElementById('Data_Limite').value,
Status: document.getElementById('Status').value
};
google.script.run
.withSuccessHandler(response => {
showLoader(false);
if (response.error) {
showToast("Erro ao salvar: " + response.error, "error");
} else {
showToast("Tarefa salva com sucesso!", "success");
loadTasks();
}
})
.withFailureHandler(err => {
showLoader(false);
showToast("Falha de rede: " + err.message, "error");
})
.saveTask(taskData);
}
function confirmDelete(taskId) {
currentDeleteId = taskId;
document.getElementById('modalBackdrop').classList.add('open');
document.getElementById('confirmModal').classList.add('open');
}
document.getElementById('btnConfirmDelete').addEventListener('click', () => {
closeAllModals();
if (!currentDeleteId) return;
showLoader(true);
google.script.run
.withSuccessHandler(response => {
showLoader(false);
if (response.error) {
showToast("Erro ao excluir: " + response.error, "error");
} else {
showToast("Tarefa excluída com sucesso!", "success");
loadTasks();
}
})
.withFailureHandler(err => {
showLoader(false);
showToast("Falha de rede: " + err.message, "error");
})
.deleteTask(currentDeleteId);
});
// ==========================================================================
// UTILIÁRIOS VISUAIS
// ==========================================================================
function showLoader(show) {
const loader = document.getElementById('loader');
if (show) loader.classList.remove('hidden');
else loader.classList.add('hidden');
}
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.innerText = message;
toast.className = `toast show ${type}`;
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
/**
* Interpretador de datas à prova de falhas (Lida com YYYY-MM-DD e DD/MM/YYYY).
*/
function parseDateSafe(dateStr) {
if (!dateStr) return null;
let d, m, y;
if (typeof dateStr === 'string' && dateStr.includes('/')) {
[d, m, y] = dateStr.split('/');
} else if (typeof dateStr === 'string' && dateStr.includes('-')) {
const parts = dateStr.split('T')[0].split('-');
[y, m, d] = parts;
} else if (dateStr instanceof Date) {
return dateStr;
} else {
return null;
}
return new Date(y, m - 1, d, 12, 0, 0);
}
/**
* Calcula e monta o selo de prazos de forma inteligente.
*/
function getDeadlineBadge(status, limitDateStr) {
if (status === "Concluido") {
return '<span class="badge badge-success"><span class="material-icons" style="font-size:0.9rem;">check_circle</span> Concluído</span>';
}
const limitDate = parseDateSafe(limitDateStr);
if (!limitDate) return '';
const today = new Date();
today.setHours(12, 0, 0, 0);
const diffTime = limitDate - today;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays > 1) {
return `<span class="badge badge-future"><span class="material-icons" style="font-size:0.9rem;">schedule</span> Faltam ${diffDays} dias</span>`;
} else if (diffDays === 1) {
return '<span class="badge badge-future"><span class="material-icons" style="font-size:0.9rem;">schedule</span> Falta 1 dia</span>';
} else if (diffDays === 0) {
return '<span class="badge badge-today"><span class="material-icons" style="font-size:0.9rem;">priority_high</span> Vence hoje!</span>';
} else {
const absoluteDays = Math.abs(diffDays);
return `<span class="badge badge-overdue"><span class="material-icons" style="font-size:0.9rem;">warning</span> Atrasado há ${absoluteDays} dia${absoluteDays > 1 ? 's' : ''}</span>`;
}
}
</script>
</body>
</html>