Meeting Action Item Tracker

Meeting Context (Optional)

Add / Edit Action Item

Action Items List

Action Item Owner(s) Due Date Status Actions

Error: Tool UI elements failed to load correctly. Check HTML IDs.

'; return; } // --- State & localStorage --- const ITEMS_STORAGE_KEY = 'actionItemTracker_items'; let actionItems = []; // { id, description, owner, dueDate, status, meetingContext, dateAdded } let currentSort = { key: 'dueDate', order: 'asc' }; // Default sort // --- Utility Functions --- // (Same as previous - loadData, saveData, generateId, sanitizeHTML, isValidDateString, formatDate..., dateDiff..., addDays...) const loadData = () => { try { const si = JSON.parse(localStorage.getItem(ITEMS_STORAGE_KEY)); if(Array.isArray(si)) actionItems=si; console.log("Data loaded:", { items: actionItems.length }); } catch(e){ console.error("Err load data:", e); actionItems=[]; }}; const saveData = () => { try { localStorage.setItem(ITEMS_STORAGE_KEY, JSON.stringify(actionItems)); console.log("Data saved."); } catch(e){ console.error("Err save data:", e); alert("Error saving data.");}}; const generateId = () =>'_'+Math.random().toString(36).substr(2,9); const sanitizeHTML = (str)=>(temp=>{temp.textContent=str; return temp.innerHTML;})(document.createElement('div')); const isValidDateString = (ds)=>{if(!ds||!/^\d{4}-\d{2}-\d{2}$/.test(ds))return false; const d=new Date(ds+'T00:00:00Z'); if(isNaN(d.getTime()))return false; const p=ds.split('-'); return d.getUTCFullYear()===parseInt(p[0],10)&&d.getUTCMonth()===parseInt(p[1],10)-1&&d.getUTCDate()===parseInt(p[2],10);}; const formatDateForDisplay = (ds)=>{if(!isValidDateString(ds))return'N/A';try{const d=new Date(ds+'T00:00:00Z');return d.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric',timeZone:'UTC'});}catch(e){return'Invalid';}}; const formatDateForInput = (ds)=>(isValidDateString(ds)?ds:''); const getTodayDateString = ()=>new Date().toISOString().split('T')[0]; const dateDiffInDays = (ds1,ds2)=>{if(!isValidDateString(ds1)||!isValidDateString(ds2))return null;try{const t1=new Date(ds1+'T00:00:00Z').getTime();const t2=new Date(ds2+'T00:00:00Z').getTime();if(isNaN(t1)||isNaN(t2))return null;return Math.round((t2-t1)/(1000*60*60*24));}catch(e){return null;}}; const addDaysToDate = (ds,days)=>{if (!isValidDateString(ds)) return null; try { const d=new Date(ds+'T00:00:00Z'); if(isNaN(d.getTime()))return null; d.setUTCDate(d.getUTCDate()+days); if(isNaN(d.getTime()))return null; return d.toISOString().split('T')[0];} catch(e){return null;}}; // --- Form Handling --- // (Same as previous - clearItemForm, populateItemForm) const clearItemForm = ()=>{ editItemIdInput.value=''; itemDescriptionInput.value=''; itemOwnerInput.value=''; itemDueDateInput.value=''; itemStatusSelect.value='Open'; meetingTopicInput.value=''; meetingDateInput.value=''; addUpdateItemBtn.textContent='Add Action Item'; cancelEditItemBtn.style.display='none'; itemDescriptionInput.focus(); }; const populateItemForm = (itemId)=>{ const item=actionItems.find(i=>i.id===itemId); if(item){ editItemIdInput.value=item.id; itemDescriptionInput.value=item.description; itemOwnerInput.value=item.owner; itemDueDateInput.value=formatDateForInput(item.dueDate); itemStatusSelect.value=item.status; if(item.meetingContext){const ctxParts=item.meetingContext.split(' - '); meetingTopicInput.value=ctxParts[0]||''; if(ctxParts[1]&&isValidDateString(ctxParts[1])){meetingDateInput.value=ctxParts[1];}else{meetingDateInput.value='';}}else{meetingTopicInput.value=''; meetingDateInput.value='';} addUpdateItemBtn.textContent='Update Action Item'; cancelEditItemBtn.style.display='inline-block'; itemDescriptionInput.focus();} }; // --- DEBUG: Attach listener to form submit --- console.log("Attempting to attach submit listener to item form..."); itemForm.addEventListener('submit', (e) => { console.log("DEBUG: Item form submitted!"); // Check if this appears e.preventDefault(); const id = editItemIdInput.value; const description = itemDescriptionInput.value.trim(); const owner = itemOwnerInput.value.trim(); const dueDate = itemDueDateInput.value || null; const status = itemStatusSelect.value; const meetingTopic = meetingTopicInput.value.trim(); const meetingDate = meetingDateInput.value; let meetingContext = meetingTopic; if (meetingTopic && meetingDate && isValidDateString(meetingDate)) { meetingContext = `${meetingTopic} - ${meetingDate}`; } else if (meetingDate && isValidDateString(meetingDate)) { meetingContext = `Meeting - ${meetingDate}`; } // --- DEBUG: Log values before checks --- console.log("DEBUG: Item Form values -", { id, description, owner, dueDate, status, meetingContext }); if (!description || !owner) { alert("Please enter Action Item and Owner."); return; } if (dueDate && !isValidDateString(dueDate)) { alert("Please enter a valid Due Date (YYYY-MM-DD) or leave it blank."); return; } if (id) { // Update console.log("Updating item:", id); actionItems = actionItems.map(item => item.id === id ? { ...item, description, owner, dueDate, status, meetingContext } : item ); } else { // Add console.log("Adding new item"); actionItems.push({ id: generateId(), description, owner, dueDate, status, meetingContext, dateAdded: new Date().toISOString() }); } saveData(); clearItemForm(); renderActionItemsTable(); console.log("Item added/updated."); }); console.log("Item form submit listener attached."); cancelEditItemBtn.addEventListener('click', clearItemForm); // --- Table Rendering & Interactions --- // (Same as previous - applyFiltersAndSort, renderActionItemsTable, handleStatusChange, table header click listener, updateSortIndicators, tableBody click listener for edit/delete) const applyFiltersAndSort = (items)=>{ const fv=filterStatusSelect.value; let filt=items; if(fv==='Open')filt=items.filter(i=>i.status==='Open'); else if(fv==='In Progress')filt=items.filter(i=>i.status==='In Progress'); else if(fv==='Done')filt=items.filter(i=>i.status==='Done'); else if(fv==='Cancelled')filt=items.filter(i=>i.status==='Cancelled'); else if(fv==='OpenInProgress')filt=items.filter(i=>i.status==='Open'||i.status==='In Progress'); const{key,order}=currentSort; if(key!=='none'){filt.sort((a,b)=>{let vA,vB; if(key==='dueDate'){vA=a.dueDate?new Date(a.dueDate+'T00:00:00Z').getTime():(order==='asc'?Infinity:-Infinity);vB=b.dueDate?new Date(b.dueDate+'T00:00:00Z').getTime():(order==='asc'?Infinity:-Infinity);}else{vA=(a[key]||'').toLowerCase();vB=(b[key]||'').toLowerCase();} let comp=0; if(vA>vB)comp=1; else if(vA{ console.log("Rendering table..."); tableBody.innerHTML=''; const items=applyFiltersAndSort(actionItems); const todayS=getTodayDateString(); if(items.length===0){tableBody.innerHTML='No items match filter.';}else{items.forEach(item=>{const tr=document.createElement('tr');tr.dataset.id=item.id; const isOv=item.dueDate&&isValidDateString(item.dueDate)&&dateDiffInDays(item.dueDate,todayS)>0&&item.status!=='Done'&&item.status!=='Cancelled'; if(isOv){tr.classList.add('overdue');tr.title="Overdue!";} const dCell=document.createElement('td');dCell.setAttribute('data-label','Action Item');dCell.innerHTML=`${sanitizeHTML(item.description)} ${item.meetingContext?`(${sanitizeHTML(item.meetingContext)})`:''}`; tr.appendChild(dCell); const oCell=document.createElement('td');oCell.setAttribute('data-label','Owner(s)');oCell.textContent=item.owner;tr.appendChild(oCell); const ddCell=document.createElement('td');ddCell.setAttribute('data-label','Due Date');ddCell.textContent=formatDateForDisplay(item.dueDate);ddCell.classList.add('due-date-cell');tr.appendChild(ddCell); const sCell=document.createElement('td');sCell.setAttribute('data-label','Status');const sSel=document.createElement('select');sSel.className='status-select';sSel.dataset.id=item.id;['Open','In Progress','Done','Cancelled'].forEach(s=>{const opt=document.createElement('option');opt.value=s;opt.textContent=s;opt.selected=(item.status===s);sSel.appendChild(opt);});sSel.addEventListener('change',handleStatusChange);sCell.appendChild(sSel);tr.appendChild(sCell); const aCell=document.createElement('td');aCell.setAttribute('data-label','Actions');aCell.className='actions-cell';aCell.innerHTML=` `; tr.appendChild(aCell); tableBody.appendChild(tr);});} updateSortIndicators(); }; function handleStatusChange(event){const sel=event.target;const itemId=sel.dataset.id;const newStatus=sel.value;console.log(`Status changed for ${itemId} to ${newStatus}`);actionItems=actionItems.map(item=>item.id===itemId?{...item,status:newStatus}:item);saveData();renderActionItemsTable();} filterStatusSelect.addEventListener('change', renderActionItemsTable); tableHeaders.forEach(header=>{header.addEventListener('click',()=>{const sortKey=header.dataset.sort;if(sortKey==='none')return;if(currentSort.key===sortKey){currentSort.order=currentSort.order==='asc'?'desc':'asc';}else{currentSort.key=sortKey;currentSort.order='asc';} renderActionItemsTable();});}); function updateSortIndicators(){tableHeaders.forEach(th=>{const sk=th.dataset.sort;th.classList.remove('sorted-asc','sorted-desc');let ar=th.querySelector('.sort-arrow');if(!ar&&sk!=='none'){ar=document.createElement('span');ar.className='sort-arrow';th.appendChild(ar);}if(ar){if(sk===currentSort.key){th.classList.add(currentSort.order==='asc'?'sorted-asc':'sorted-desc');ar.innerHTML=currentSort.order==='asc'?' â–²':' â–¼';}else{ar.innerHTML=' â—‡';}}});} tableBody.addEventListener('click', (e)=>{ if(e.target.matches('.edit-item-btn, .edit-item-btn *')){const btn=e.target.closest('.edit-item-btn');const itemId=btn?.dataset.id;if(itemId){console.log("Edit clicked:",itemId);populateItemForm(itemId);window.scrollTo({top:itemForm.offsetTop-20,behavior:'smooth'});}} else if(e.target.matches('.delete-item-btn, .delete-item-btn *')){const btn=e.target.closest('.delete-item-btn');const itemId=btn?.dataset.id;if(itemId&&confirm('Delete item?')){console.log("Delete clicked:",itemId);actionItems=actionItems.filter(i=>i.id!==itemId);saveData();if(editItemIdInput.value===itemId){clearItemForm();} renderActionItemsTable();}}}); // --- PDF Download Logic (Using jsPDF-AutoTable) --- const preparePdfData = () => { // Changed name slightly const pdfStatusFilter = pdfFilterStatusSelect.value; let pdfItems = actionItems; if (pdfStatusFilter === 'Open') pdfItems = actionItems.filter(i => i.status === 'Open'); else if (pdfStatusFilter === 'In Progress') pdfItems = actionItems.filter(i => i.status === 'In Progress'); else if (pdfStatusFilter === 'OpenInProgress') pdfItems = actionItems.filter(i => i.status === 'Open' || i.status === 'In Progress'); else if (pdfStatusFilter === 'Done') pdfItems = actionItems.filter(i => i.status === 'Done'); const { key, order } = currentSort; // Use current table sort for PDF if (key !== 'none') { pdfItems.sort((a,b)=>{ let vA,vB; if(key==='dueDate'){vA=a.dueDate?new Date(a.dueDate+'T00:00:00Z').getTime():(order==='asc'?Infinity:-Infinity);vB=b.dueDate?new Date(b.dueDate+'T00:00:00Z').getTime():(order==='asc'?Infinity:-Infinity);}else{vA=(a[key]||'').toLowerCase();vB=(b[key]||'').toLowerCase();} let comp=0;if(vA>vB)comp=1;else if(vAnew Date(b.dateAdded||0).getTime()-new Date(a.dateAdded||0).getTime()); } const head = [['Action Item', 'Owner(s)', 'Due Date', 'Status', 'Meeting Context']]; const body = pdfItems.map(item => [ item.description, item.owner, formatDateForDisplay(item.dueDate), item.status, item.meetingContext || '-' ]); const name = employeeName || 'Unknown Employee'; const filterText = pdfStatusFilter === 'All' ? 'All Statuses' : pdfStatusFilter; const generatedDate = new Date().toLocaleDateString(); const title = `Action Item Report - ${name}`; const subtitle = `Generated: ${generatedDate} | Filter: ${filterText}`; return { head, body, title, subtitle, pdfItems }; // Return items too for styling hook }; // --- DEBUG: Attach listener using delegation --- console.log("Attempting to attach delegated click listener to container..."); container.addEventListener('click', async (e) => { // Check if the clicked element or its parent is the download button if (e.target.matches('#download-report-pdf') || e.target.closest('#download-report-pdf')) { console.log("DEBUG: Delegated Download PDF button click detected!"); // Prevent default if it's somehow submitting a form e.preventDefault(); const { jsPDF } = window.jspdf; // Ensure jsPDF is loaded if (!jsPDF || !jsPDF.API || !jsPDF.API.autoTable) { console.error("jsPDF or jsPDF-AutoTable not loaded correctly!"); alert("Error: PDF generation library not loaded."); return; } console.log("DEBUG: Preparing PDF data..."); const pdfData = preparePdfData(); if (!pdfData) { console.warn("DEBUG: PDF generation prevented - Data preparation failed."); alert("Could not prepare PDF data. Check filters."); return; } console.log(`DEBUG: PDF Data Prepared - Rows: ${pdfData.body.length}`); const { head, body, title, subtitle, pdfItems } = pdfData; // Destructure prepared data const todayStr = getTodayDateString(); try { console.log("DEBUG: Initializing jsPDF document..."); const doc = new jsPDF({ orientation: 'landscape' }); // Add Title and Subtitle doc.setFontSize(16); doc.text(title, doc.internal.pageSize.getWidth() / 2, 40, { align: 'center' }); doc.setFontSize(10); doc.setTextColor(100); doc.text(subtitle, doc.internal.pageSize.getWidth() / 2, 55, { align: 'center' }); console.log("DEBUG: Calling jsPDF.autoTable..."); doc.autoTable({ head: head, body: body, startY: 70, theme: 'grid', styles: { fontSize: 8, cellPadding: 3, valign: 'middle' }, headStyles: { fillColor: [13, 110, 253], textColor: [255, 255, 255], fontStyle: 'bold' }, // Use var(--action-primary) blue columnStyles: { 0: { cellWidth: 'auto' }, // Action Item 1: { cellWidth: 100 }, // Owner 2: { cellWidth: 70 }, // Due Date 3: { cellWidth: 60 }, // Status 4: { cellWidth: 120 } // Meeting Context }, didParseCell: function (data) { try { // Add try-catch within hook for safety // Color status cell if (data.column.index === 3 && data.cell.section === 'body') { const status = data.cell.raw?.toString(); if (status === 'Done') data.cell.styles.fillColor = '#d1e7dd'; // Light green else if (status === 'In Progress') data.cell.styles.fillColor = '#fff3cd'; // Light yellow else if (status === 'Cancelled') data.cell.styles.fillColor = '#e9ecef'; // Light gray else if (status === 'Open') data.cell.styles.fillColor = '#cff4fc'; // Light cyan } // Highlight overdue text if (data.column.index === 2 && data.cell.section === 'body' && data.row.index < pdfItems.length) { // Check index validity const item = pdfItems[data.row.index]; const isOverdue = item?.dueDate && isValidDateString(item.dueDate) && dateDiffInDays(item.dueDate, todayStr) > 0 && item.status !== 'Done' && item.status !== 'Cancelled'; if (isOverdue) { data.cell.styles.textColor = [220, 53, 69]; data.cell.styles.fontStyle = 'bold'; } } } catch (hookError) { console.error("Error in autoTable didParseCell hook:", hookError); } } }); console.log("DEBUG: autoTable finished."); const filename = `Action_Items_${name.replace(/ /g,'_')}_${todayStr}.pdf`; console.log(`Saving PDF: ${filename}`); doc.save(filename); console.log("PDF Saved."); } catch (error) { console.error("Error during PDF generation process:", error); alert(`Error generating PDF: ${error.message}. Check console.`); } } }); console.log("Delegated listener attached to container."); // --- Initial Load --- const initializeTool = () => { console.log("Running initializeTool..."); loadData(); employeeNameInput.value = employeeName; meetingDateInput.value = getTodayDateString(); dateSelectInput.value = getTodayDateString(); updateDateLabel(); renderActionItemsTable(); console.log("Initialization complete."); }; if (window.jspdf && window.jspdf.jsPDF && window.jspdf.jsPDF.API && window.jspdf.jsPDF.API.autoTable) { initializeTool(); } else { console.error("jsPDF or AutoTable not loaded!"); alert("Error: PDF Libs not loaded."); downloadPdfBtn.disabled = true; downloadPdfBtn.textContent = "PDF Lib Error"; } }); // --- END OF JAVASCRIPT ---

Meetings are essential for collaboration and decision-making, but their true value often hinges on what happens after they conclude. Without a clear system for capturing and tracking commitments, vital action items can easily fall through the cracks, leading to delays and missed objectives. The WorkToolz Meeting Action Item Tracker is your indispensable online tool for ensuring that every decision made and every task assigned during a meeting is diligently recorded, managed, and followed through to completion. It transforms chaotic post-meeting follow-ups into a streamlined, efficient process, maximizing the productivity of your team.

This intuitive tracker allows you to start by providing essential meeting context, such as the topic name and date. This helps to organize your action items by associating them directly with a specific discussion, making it easy to reference past meetings and understand the origin of each task. Once the meeting context is set, you can effortlessly add individual action items. For each item, you’ll clearly describe the action needed, assign an owner responsible for its completion, set a realistic due date, and update its current status. This structured approach ensures that accountability is clear and deadlines are transparent, fostering a proactive environment where tasks are owned and progress is visible.

The WorkToolz Meeting Action Item Tracker goes beyond simple listing; it provides a dynamic overview of all your pending tasks. As action items move through their lifecycle, you can easily update their status—from ‘Open’ to ‘In Progress’ to ‘Closed’—giving you a real-time snapshot of your team’s progress. The ability to filter the action items list by status further enhances clarity, allowing you to quickly identify what needs attention, what’s underway, and what has been successfully completed. This level of oversight is crucial for project managers, team leads, and anyone responsible for ensuring that meeting outcomes translate into tangible results.

Furthermore, this powerful tool empowers you to generate comprehensive reports of your action items. With just a few clicks, you can download a PDF summary that can include action items of all statuses or specific ones, based on your filtering preference. This feature is invaluable for distributing meeting minutes, providing updates to stakeholders, or simply keeping a formal record of commitments. It eliminates the tedious manual compilation of notes, ensuring accuracy and saving valuable time. By integrating the WorkToolz Meeting Action Item Tracker into your workflow, you’re not just tracking tasks; you’re building a culture of accountability, improving communication, and consistently achieving your meeting objectives.

Scroll to Top