classes/monthly-log.js

import { Database } from '../classes/database.js';
import { IDConverter } from './IDConverter.js';
import { indicateDate } from '../scripts/script.js';

// tell the linter that Chart is defined by a previous script
/* global Chart */

/**
 * This class contains functions to construct and edit the monthly log custom HTML element.
 *
 * @classdesc
 * @example <caption>Monthly Log class</caption>
 * // Example of a monthly JSON object used to generate a monthly-log element
 * const exampleMonthlyJSON = {
 *   sections: [
 *     {
 *       id: '00',
 *       name: 'Monthly Goals',
 *       type: 'log',
 *       bulletIDs: [
 *         'B 2105 00 00',
 *         'B 2105 00 01'
 *       ],
 *       nextBulNum: 2
 *     },
 *     {
 *       id: '01',
 *       name: 'Monthly Notes',
 *       type: 'log',
 *       bulletIDs: [
 *         'B 2105 01 00',
 *         'B 2105 01 01'
 *       ],
 *       nextBulNum: 2
 *     }
 *   ]
 * }
 * // Create a new monthly log HTML element using the object
 * let monthly = document.createElement('monthly-log');
 * monthly.data = exampleMonthlyJSON;
 */

class MonthlyLog extends HTMLElement {
  // -------------------------------------- Start Constructor -------------------------------------

  /**
   * Constructs a blank monthly log HTML element using the defined HTML template
   *
   * @constructor
   */
  constructor () {
    super();

    const template = document.createElement('template');

    template.innerHTML = `
      <link rel="stylesheet" href="../style/style.css">
      <link rel="stylesheet" href="../assets/css/all.css">
      <div class="monthly">
        <section class="header" id="monthly-header">
          <h1></h1>
        </section>
        <section id="monthly-calendar"></section>
        <section id="monthly-checks"></section>
        <section id="monthly-charts"></section>
      </div>
    `;

    this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }

  // --------------------------------------- End Constructor --------------------------------------

  // ---------------------------------- Start Get/Set Functions -----------------------------------

  /**
   * This function returns the data stored in this monthly element as a JSON object.
   *
   * @returns {Object} JSON representation of data used to generate this monthly log element
   */
  get data () {
    return JSON.parse(this.getAttribute('data'));
  }

  /**
   * This function constructs the monthly log HTML element using the given ID and monthly log
   * object data. It starts by constructing and setting the header text for the element. It then
   * creates the buttons for the monthly calendar, and constructs the charts for the different
   * trackers (mood, sleep, etc). Next it goes through each attribute of the monthly log object
   * to construct the notes sections of the element. Each notes section is constructed by fetching
   * the data of each bullet in the section from the database, and creating a custom bullet-entry
   * HTML element that is appended to the section. Finally, the setAttribute function is called on
   * this element to set the 'data' attribute of the element to be the given JSON data, so that
   * the data can be retrieved from the element later if needed.
   *
   * @param {Array.<{id: string, jsonData: Object, callback: function}>} data - Array of three
   * elements (first element is the string ID of the object, second element is the JSON object
   * data, and the third element is the callback function for zooming into a certain date) that
   * is used to construct and set the data in this HTML element
   */
  set data ([id, jsonData, callback]) {
    // store this object in a variable so it can be passed to handlers later
    const monthlyLog = this;

    // set the id of the custon element to the given id
    this.id = id;

    // if the jsonData is an empty object, then we should create an empty monthly element
    if (Object.entries(jsonData).length === 0) {
      jsonData = {
        sections: [
          {
            id: '00',
            name: 'Monthly Goals',
            type: 'log',
            bulletIDs: [],
            nextBulNum: 0
          },
          {
            id: '01',
            name: 'Monthly Notes',
            type: 'log',
            bulletIDs: [],
            nextBulNum: 0
          }
        ]
      };
    }

    // get the shadow root of this custom HTML element and set its ID to the given ID
    const root = this.shadowRoot.querySelector('div.monthly');
    root.id = id;

    // get all information about the date that is needed for the header display
    const dateObj = IDConverter.getDateFromID(id, 'month');
    const month = IDConverter.getMonthFromDate(dateObj);
    const year = dateObj.getFullYear();
    const dateString = `${month} ${year}`;

    // get the header text of this custom HTML element and set its contents to the constructed date string
    const headerText = root.querySelector('#monthly-header > h1');
    headerText.innerText = dateString;

    const trackerHeader = document.createElement('h2');
    trackerHeader.className = 'tracker-header';
    trackerHeader.innerHTML = 'Trackers';
    root.appendChild(trackerHeader);

    // IDs to use for the charts
    const canvasIDs = ['mood-tracker', 'sleepq-tracker', 'calorie-tracker', 'money-tracker'];
    const charts = []; // array of all the charts

    // Make this cleaner (hopefully without hard-coding) if we have time
    // creates the canvas elements/charts for each tracker
    for (let i = 0; i <= 3; i++) {
      // create the canvas element and append it to the charts section
      const canvas = document.createElement('canvas');
      canvas.className = 'chart';
      canvas.id = canvasIDs[i];
      root.querySelector('#monthly-charts').appendChild(canvas);

      // set the label of the y-axis of the chart based on what kind of tracker this is
      let yAxisLabel = '';
      switch (i) {
        case 0:
          yAxisLabel = 'Mood';
          break;
        case 1:
          yAxisLabel = 'Sleep Quality';
          break;
        case 2:
          yAxisLabel = 'Calorie Intake';
          break;
        case 3:
          yAxisLabel = 'Money Spent';
          break;
      }

      // construct the chart and apply to canvas
      // get the canvas context and create the new chart
      // the chart starts off empty, but gets populated with each fetch call for daily data
      const ctx = canvas.getContext('2d');
      const chart = new Chart(ctx, {
        type: 'line', // line chart
        data: {
          labels: [], // x-axis labels
          datasets: [{
            data: [], // data points
            borderCapStyle: 'round',
            fill: false,
            borderColor: '#bfbfbf',
            borderWidth: 2,
            pointBackgroundColor: '#bfbfbf',
            pointRadius: 2,
            pointHoverRadius: 2,
            tension: 0.25
          }]
        },
        options: {
          scales: {
            x: { // x-axis contents
              axis: 'x',
              position: 'bottom',
              title: {
                display: true,
                text: 'Date'
              },
              grid: {
                borderColor: '#a3a3a3',
                display: false
              },
              ticks: {
                color: '#a3a3a3'
              }

            },
            y: { // y-axis contents
              axis: 'y',
              position: 'left',
              title: {
                display: true,
                text: yAxisLabel
              },
              grid: {
                borderColor: '#a3a3a3',
                display: false
              },
              ticks: {
                color: '#a3a3a3'
              },
              min: 0 // min-value of y-axis
              // potential max-value can be added here
            }
          },
          plugins: {
            legend: {
              display: false // don't show the legend
            }
          }
        }
      });
      chart.render(); // render the initial chart (will get updated when data is updated)
      charts.push(chart);
    }

    // 1: create buttons for each date in the monthly calendar
    // 2: fetch the daily object for each date, and add its data to the tracker charts
    const calendar = root.querySelector('#monthly-calendar');
    const numDays = dateObj.getDate();
    const dates = document.createElement('div');
    dates.className = 'dates';

    for (let i = 1; i <= 7; i++) {
      const dateName = document.createElement('h3');
      dateName.className = 'date-name';

      switch (i) {
        case 1:
          dateName.innerHTML = 'Sun';
          break;
        case 2:
          dateName.innerHTML = 'Mon';
          break;
        case 3:
          dateName.innerHTML = 'Tue';
          break;
        case 4:
          dateName.innerHTML = 'Wed';
          break;
        case 5:
          dateName.innerHTML = 'Thu';
          break;
        case 6:
          dateName.innerHTML = 'Fri';
          break;
        case 7:
          dateName.innerHTML = 'Sat';
          break;
        default:
          dateName.innerHTML = 'oops';
      }

      dates.appendChild(dateName);
    }

    calendar.appendChild(dates);

    // create the section for exercise checkboxes
    const exerciseTracker = document.createElement('div');
    root.querySelector('#monthly-checks').appendChild(exerciseTracker);
    exerciseTracker.style.display = 'flex';
    exerciseTracker.style.flexDirection = 'row';
    const exerciseTrackerHeading = document.createElement('div');
    exerciseTrackerHeading.innerText = 'Exercise:';
    exerciseTracker.appendChild(exerciseTrackerHeading);
    const exerciseTrackerBoxes = document.createElement('div');
    exerciseTrackerBoxes.className = 'tracking-boxes';
    exerciseTrackerBoxes.style.display = 'flex';
    exerciseTrackerBoxes.style.flexDirection = 'row';
    exerciseTracker.appendChild(exerciseTrackerBoxes);

    dateObj.setDate(1);
    for (let i = 0; i < dateObj.getDay(); i++) {
      // add fake buttons
      const blankButton = document.createElement('button');
      blankButton.className = 'blank-button';
      calendar.appendChild(blankButton);
    }

    for (let i = 1; i <= numDays; i++) {
      // date button creation
      const dateID = `D ${id.substring(2)}${IDConverter.stringifyNum(i)}`;
      const dateButton = document.createElement('button');
      dateButton.className = 'monthly-calendar-button' + i;
      dateButton.id = dateID;
      dateButton.innerText = String(i);
      dateButton.addEventListener('click', function (event) {
        callback(event);
      });
      calendar.appendChild(dateButton);

      // after pulling tracker data, adjust the charts with the new data
      // add the x-axis date labels to each chart
      for (const chart of charts) {
        chart.data.labels.push(i);
      }

      const exerciseTrackerBox = document.createElement('div');
      exerciseTrackerBox.style.display = 'flex';
      exerciseTrackerBox.style.flexDirection = 'column';
      exerciseTrackerBox.style.textAlign = 'center';
      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.disabled = 'true';
      checkbox.checked = false;
      checkbox.className = 'check';
      exerciseTrackerBox.appendChild(checkbox);
      const datePar1 = document.createElement('p');
      datePar1.style.margin = '0';
      datePar1.style.lineHeight = '0.9';
      datePar1.innerText = String(i).charAt(0);
      const datePar2 = document.createElement('p');
      datePar2.style.margin = '0';
      datePar2.style.lineHeight = '0.9';
      if (String(i).length > 1) {
        datePar2.innerText = String(i).charAt(1);
      }
      exerciseTrackerBox.appendChild(datePar1);
      exerciseTrackerBox.appendChild(datePar2);
      exerciseTrackerBoxes.appendChild(exerciseTrackerBox);

      // get the data for the chart
      Database.fetch(dateID, function (data, date) {
        // if data is present
        if (data) {
          for (const tracker of data.trackers) {
            // get chart for the tracker
            let trackerChart = null;
            switch (tracker.name) {
              case 'Mood':
                trackerChart = charts[0];
                break;
              case 'Sleep Quality':
                trackerChart = charts[1];
                break;
              case 'Calorie Intake':
                trackerChart = charts[2];
                break;
              case 'Money Spent':
                trackerChart = charts[3];
                break;
              case 'Exercise':
                checkbox.checked = tracker.value === 1;
                break;
            }

            // update chart data with tracker data
            if (trackerChart) {
              trackerChart.data.datasets[0].data[date - 1] = tracker.value;
            }
          }
        } else {
          for (const chart of charts) {
            chart.data.datasets[0].data[date - 1] = undefined;
          }
        }

        // update chart by the last day of the month
        if (date === numDays) {
          for (const chart of charts) {
            chart.update();
          }
        }
      }, i);
    }

    indicateDate(this.shadowRoot);

    const divElement = document.createElement('div');
    divElement.className = 'notes';

    root.appendChild(divElement);

    // loop through all sections in JSON data and construct and populate them
    if (jsonData.sections) {
      for (const section of jsonData.sections) {
        const sectionID = section.id;

        // construct section element
        const sectionElement = document.createElement('section');
        sectionElement.id = section.id;
        sectionElement.className = section.type;

        // added a margin to the bottom of each section
        sectionElement.style = 'margin-bottom: 1vw';

        // construct section header element
        const sectionHeader = document.createElement('h2');
        sectionHeader.innerText = section.name;
        sectionElement.appendChild(sectionHeader);

        // construct bullet elements
        for (const bulletID of section.bulletIDs) {
          // create bullet element and add the deletion event listeners to it
          const bulletElement = document.createElement('bullet-entry');
          bulletElement.shadowRoot.querySelector('.bullet-text').addEventListener('keydown', function (event) {
            // condition check to determine if the listener was triggered when backspace was pressed on an empty note
            if (event.keyCode === 8 && (event.target.innerText.length === 0 || event.target.innerText === '\n')) {
              monthlyLog.deleteNoteHandler(bulletElement);
            }
          });
          bulletElement.shadowRoot.querySelector('.bullet-remove').addEventListener('click', function (event) {
            monthlyLog.deleteNoteHandler(bulletElement);
          });

          sectionElement.appendChild(bulletElement);

          // fetch the bullet data and set the bullet element's data in the callback
          Database.fetch(bulletID, function (bulletData, bulletID, bulletElement, sectionID) {
            monthlyLog.setBulletData(bulletData, bulletID, bulletElement, sectionID);
          }, bulletID, bulletElement, sectionID);
        }

        // create a button to add new notes to the section and add the add new bullet event listener to it
        const newNoteButton = document.createElement('button');
        newNoteButton.className = 'new-bullet';
        newNoteButton.innerHTML = `
          <i class="fas fa-plus"></i>
        `;
        newNoteButton.addEventListener('click', function (event) {
          monthlyLog.newNoteHandler(event.target.closest('section'));
        });
        sectionElement.appendChild(newNoteButton);

        divElement.appendChild(sectionElement);
      }
    }

    // set the data attribute of this element to the given JSON data so it can be retrieved later
    this.setAttribute('data', JSON.stringify(jsonData));
  }

  // ----------------------------------- End Get/Set Functions ------------------------------------

  // ----------------------------------- Start Helper Functions -----------------------------------

  /**
   * This function is a helper function that is used as the callback for when we fetch bullet
   * data from the database or when we are creating a new bullet. The function first creates a
   * function that will be used by the created bullet object to update the bullet count in
   * the appropriate section of the monthly log and generate a bullet ID for sub-bullets (nested
   * bullets that are children of this created bullet). The function then checks if the data
   * is not null or undefined. If the data isn't null/undefined, the bullet element's data is
   * set to the given data. If the data is null/undefined, the bullet element's data is set to
   * an empty JSON object, which creates a blank bullet.
   *
   * @param {Object} bulletData - The JSON object data that will be stored in the bullet
   * @param {string} bulletID - The string ID of the bullet object
   * @param {HTMLElement} bulletElement - The bullet-entry element whose data will be set
   * @param {string} sectionID - The string ID of the section in which the bullet is being created
   */
  setBulletData (bulletData, bulletID, bulletElement, sectionID) {
    const monthlyLog = this;
    const newBulletID = function () {
      const newID = monthlyLog.generateBulletID(sectionID);
      return newID;
    };

    if (bulletData) {
      bulletElement.data = [bulletID, bulletData, newBulletID];
    } else {
      bulletElement.data = [bulletID, {}, newBulletID];
    }
  }

  /**
   * This function is a helper function that is used to create a new bullet ID. The given section
   * ID determines which section the bullet is being created in. The function then combines the
   * bullet count for that section, the section ID, and the date of the monthly object in which
   * the bullet is being added in order to create a new bullet ID, which is then returned.
   *
   * @param {string} sectionID - The string ID of the section in which the bullet is being created
   * @returns {string} The string ID to be used for the new bullet
   */
  generateBulletID (sectionID) {
    // get the data and find the section in which the bullet is being added to determine the new bullet number
    const data = this.data;
    let newBulNum;
    for (const section of data.sections) {
      if (section.id === sectionID) {
        newBulNum = section.nextBulNum;
        section.nextBulNum++;
      }
    }

    // store the udpated data
    this.setAttribute('data', JSON.stringify(data));
    Database.store(this.id, data);

    // generate the new bullet ID
    const bulletCount = IDConverter.stringifyNum(newBulNum);
    const monthlyID = this.shadowRoot.querySelector('div.monthly').id;

    return `B ${monthlyID.substring(2)} ${sectionID} ${bulletCount}`;
  }

  // ------------------------------------ End Helper Functions ------------------------------------

  // ------------------------------------ Start Event Handlers ------------------------------------

  /**
   * This function creates a new bullet. It first generates a bullet ID by combining the date of
   * the monthly log to which the bullet will belong, the ID of the section to which the bullet
   * is being added, and the ID of the new bullet, which is determined based on the number of
   * bullets currently in the section. It then adds the bullet ID to the monthly JSON object in
   * the appropriate section, and stores the updated monthly JSON object in the database. Lastly,
   * it creates a new bullet-entry HTML element, and adds the appropriate event listener to it
   * to allow for future deletion of the bullet element.
   *
   * @param {HTMLElement} sectionElement - The section element in which the new note button
   * was clicked to trigger the listener
   */
  newNoteHandler (sectionElement) {
    // generate a bullet ID
    const sectionID = sectionElement.id;
    const monthlyID = this.shadowRoot.querySelector('div.monthly').id;
    const bulletID = this.generateBulletID(sectionID);

    // add bullet ID to the monthly JSON object bulletIDs in the right section
    const data = this.data;
    for (const sec of data.sections) {
      if (sec.id === sectionID) {
        sec.bulletIDs.push(bulletID);
      }
    }
    this.setAttribute('data', JSON.stringify(data));

    // store the updated monthly JSON object in the database
    Database.store(monthlyID, data);

    // create a blank bullet element with the generated ID
    const bullet = document.createElement('bullet-entry');
    this.setBulletData({}, bulletID, bullet, sectionID);

    // add event listeners to the bullet element to handle deletion
    const monthlyLog = this;
    bullet.shadowRoot.querySelector('.bullet-text').addEventListener('keydown', function (event) {
      // condition check to determine if backspace was pressed on an empty note
      if (event.keyCode === 8 && (event.target.innerText.length === 0 || event.target.innerText === '\n')) {
        monthlyLog.deleteNoteHandler(bullet);
      }
    });
    bullet.shadowRoot.querySelector('.bullet-remove').addEventListener('click', function (event) {
      monthlyLog.deleteNoteHandler(bullet);
    });

    // add the insert the new bullet element child before the new note button
    const newNote = sectionElement.querySelector('button.new-bullet');
    sectionElement.insertBefore(bullet, newNote);

    // prompt user to start typing note
    bullet.shadowRoot.querySelector('.bullet-text').focus();
  }

  /**
   * This function handles the deletion of a bullet. The function looks through the monthly JSON
   * object to remove the ID of the bullet being deleted from the appropriate section, and then
   * stores the updated monthly JSON object in the database and removes the bullet-entry HTML
   * element from the section.
   *
   * @param {HTMLElement} bulletElement - The bullet-entry element that is being deleted
   */
  deleteNoteHandler (bulletElement) {
    // get the section element that the bullet is a child of
    const section = bulletElement.closest('section');

    // loop through the monthly JSON object to find the bullet ID to be deleted
    const sectionID = section.id;
    const data = this.data;
    for (const sec of data.sections) {
      // condition check to determine if this is the right section in the monthly JSON object
      if (sec.id === sectionID) {
        sec.bulletIDs = sec.bulletIDs.filter((bulletID) => bulletID !== bulletElement.id);
      }
    }
    this.setAttribute('data', JSON.stringify(data));

    // store the updated monthly JSON object in the database
    const monthlyID = this.shadowRoot.querySelector('div.monthly').id;
    Database.store(monthlyID, data);

    // delete the bullet from the database
    Database.delete(bulletElement.id);

    // remove the bullet-entry HTML element from the section
    section.removeChild(bulletElement);
  }

  // ------------------------------------- End Event Handlers -------------------------------------
} // end class MonthlyLog

// define a custom element for the MonthlyLog web component
customElements.define('monthly-log', MonthlyLog);