/**
 * @file DTPS assignment functions
 * @author jottocraft
 * 
 * @copyright Copyright (c) 2018-2023 jottocraft
 * @license MIT
 */

/**
 * Renders HTML for an assignment item in a list
 * 
 * @param {Assignment} assignment The assignment object to render
 * @param {string} [childDisplay] Text to display for specifying the child or children this assignment is for
 * @return {string} Assignment HTML for use in a list
 */
dtps.renderAssignment = function (assignment, childDisplay) {
    //Render points/letter score
    var scoreHTML = dtps.renderAssignmentScore(assignment);

    let highlightUnusualDate = false;
    if (assignment.dueAt && (fluid.get("pref-unusualDueDates") != "false")) {
        const dayTimeMinutes = dtps.dayTimeMinutes(assignment.dueAt);
        const range = dtps.classes[assignment.class].usualDueRange;
        if (range && !isNaN(range[0]) && !isNaN(range[1]) && (dayTimeMinutes !== null)) {
            if ((dayTimeMinutes < range[0]) || (dayTimeMinutes > range[1])) highlightUnusualDate = true;
        }
    }

    var HTML = /*html*/`
        <div 
            onclick="${`dtps.assignment('` + assignment.id + `', ` + assignment.class + `, ` + (!isNaN(childDisplay) ? true : false) + `)`}" 
            class="card ${scoreHTML ? "graded assignment" : "assignment"}"
            style="${'--classColor: ' + dtps.classes[assignment.class].color}"
        >

            <!-- Color bar for the dashboard -->
            <div class="colorBar"></div>

            <!-- Assignment title and points -->
            <h4>
                <span>${assignment.title}</span>

                <!-- Points display -->
                ${scoreHTML ? `<div class="points">${scoreHTML}</div>` : ``}
            </h4>

            <h5>
                <!-- Assignment info -->
                <div class="info">
                    ${assignment.error ? `<div style="color: #f2392c;" class="infoChip weighted"><i class="fluid-icon">error</i> Error</div>` : ""}
                    ${assignment.dueAt ? `<div ${highlightUnusualDate ? `style="font-weight: bold;color: var(--themeText);background-color: var(--theme);padding: 0px 8px;border-radius: 10px;"` : ""} class="infoChip"><i style="margin-top: -2px;" class="fluid-icon">alarm</i> Due ` + dtps.formatDate(assignment.dueAt) + `</div>` : ""}
                    ${assignment.outcomes ? `<div class="infoChip weighted"><i class="fluid-icon">adjust</i>` + assignment.outcomes.length + `</div>` : ""}
                    ${assignment.category ? `<div class="infoChip weighted"><i class="fluid-icon">category</i> ` + assignment.category + `</div>` : ""}
                    ${childDisplay ? `<div class="infoChip weighted"><i class="fluid-icon">person</i> ` + childDisplay + `</div>` : ""}
                </div>

                <!-- Status icons -->
                <div class="status">
                    ${assignment.turnedIn ? `<i title="Assignment submitted" class="fluid-icon statusIcon" style="color: #0bb75b;">assignment_turned_in</i>` : ``}
                    ${assignment.missing ? `<i title="Assignment is missing" class="fluid-icon statusIcon" style="color: #c44848;">remove_circle_outline</i>` : ``}
                    ${assignment.late ? `<i title="Assignment is late" class="fluid-icon statusIcon" style="color: #c44848;">assignment_late</i>` : ``}
                    ${assignment.locked ? `<i title="Assignment submissions are locked" class="fluid-icon statusIcon" style="color: var(--secText, gray);">lock_outline</i>` : ``}
                    ${assignment.pending ? `<i title="Grade is pending review" class="fluid-icon statusIcon" style="color: #b3b70b;">rate_review</i>` : ``}
                </div>
            </h5>
        </div>
    `;

    return HTML;
}

/**
* Renders HTML for an assignment score if the assignment is graded
* 
* @param {Assignment} assignment The assignment object to render a score for
* @return {string} Assignment score HTML
*/
dtps.renderAssignmentScore = function (assignment) {
    var scoreHTML = "";

    const useRubricGrades = dtpsLMS.useRubricGrades instanceof Array ? dtpsLMS.useRubricGrades.includes(dtps.classes[assignment.class].id) : dtpsLMS.useRubricGrades;

    //Use rubric score over points score if possible
    if (useRubricGrades && assignment.rubric) {
        var rubricHTML = [];

        assignment.rubric.forEach(rubricItem => {
            if (rubricItem.score !== undefined) {
                rubricHTML.push(/*html*/`
                    <div title="${rubricItem.title}" style="color: ${rubricItem.color || "var(--text)"};">
                        ${rubricItem.score}
                    </div>
                `);
            }
        });

        if (rubricHTML.length) scoreHTML = `<div class="dtpsRubricScore">${rubricHTML.join("")}</div>`;

    } else if (!useRubricGrades && (assignment.grade || (assignment.grade == 0))) {
        scoreHTML = /*html*/`
            <div class="assignmentGrade">
                <div class="grade">${Number(assignment.grade.toFixed(2))}</div>
                <div class="value">/${Number(assignment.value.toFixed(2))}</div>
                ${assignment.letter ? `<div class="letter">${assignment.letter.replace("incomplete", `<i class="fluid-icon">cancel</i>`).replace("complete", `<i class="fluid-icon">done</i>`)}</div>` : ""}
                <div class="percentage">${Math.round((assignment.grade / assignment.value) * 100)}%</div>
            </div>
        `;
    }

    return scoreHTML;
}

/**
* Merges and renders assignments for children in the same class
* 
* @param {Assignment[]} assignments An array of assignment objects to merge
* @return {string} HTML with the assignments merged and labelled
*/
dtps.mergeAndRenderChildAssignments = function (assignments) {
    var assignmentGroups = [];

    assignments.forEach(assignment => {
        var matchedGroup = false;
        assignmentGroups.forEach(group => {
            var groupAssignment = group[0];

            //Check if assignment matches group assignment
            var matchKeys = ["id", "dueAt", "locked", "category", "turnedIn", "late", "missing", "grade", "value", "letter"];
            var matches = 0;
            matchKeys.forEach(k => {
                if (assignment[k] == groupAssignment[k]) matches++;
            });

            if (matches == matchKeys.length) {
                matchedGroup = true;
                group.push(assignment);
            }
        });

        if (!matchedGroup) {
            assignmentGroups.push([assignment]);
        }
    });

    return assignmentGroups.map(g => {
        return dtps.renderAssignment(g[0], g.length == 1 ? dtps.user.children.find(c => c.id == dtps.classes[g[0].class].userID).name : g.length);
    }).join("");
}

/**
 * Renders the DTPS dashboard and calls dtps.renderCalendar, dtps.renderUpdates, and dtps.renderUpcoming
 */
dtps.mainStream = function () {
    //Ensure classes are rendered in the sidebar
    dtps.presentClass("dash");
    dtps.showClasses();

    //Clear active state from all tabs
    $("#dtpsTabBar .btn").removeClass("active");

    //Count amount of classes that are loaded
    var ready = 0;
    dtps.classes.forEach(course => {
        if (course.assignments) ready++;
    });

    //Check if all classes are loaded
    var doneLoading = ready == dtps.classes.length;

    //Returns dashboard item container HTML for an item
    function dashboardContainerHTML(dashboardItem) {
        if (dashboardItem.id == "dtps.calendar") {
            return window.FullCalendar ? `<div id="calendar" class="card" style="padding: 20px;"></div>` : "";
        } else if (dashboardItem.id == "dtps.updates") {
            return `<div class="updatesStream recentlyGraded announcements"></div>`;
        } else if (dashboardItem.id == "dtps.dueToday") {
            return `<div style="padding: 20px 0px;" class="dueToday"></div>`;
        } else if (dashboardItem.id == "dtps.upcoming") {
            return `<div style="padding: 20px 0px;" class="assignmentStream"></div>`;
        }
    }

    //Render HTML with loading placeholder
    if (dtps.selectedClass == "dash") {
        jQuery(".classContent").html(/*html*/`
            <div style="letter-spacing: 0px;">
                <div style="float: left; width: 45%; display: inline-block;" class="dash">
                    <div id="remoteUpdateImportant"></div>
                    ${dtps.leftDashboard.map(dashboardItem => {
            return dashboardContainerHTML(dashboardItem);
        }).join("")}
                </div>

                <div style="float: left; width: 55%; display: inline-block;" class="dash">
                    ${!doneLoading ? `<div style="margin: 75px 25px 10px 75px;"><div class="spinner"></div></div>` : ""}
                    ${dtps.rightDashboard.map(dashboardItem => {
            return dashboardContainerHTML(dashboardItem);
        }).join("")}
                </div>
            </div>
        `);
    }

    //Render updates stream
    dtps.renderUpdates();

    //Render upcoming assignments stream
    dtps.renderUpcoming();

    //Render calendar
    dtps.calendar(doneLoading);

    //Render due today
    dtps.renderDueToday(doneLoading);
}

/**
 * Checks if a due date is on a date
 * 
 * @param {Date} date Date to check 
 * @param {Date} [onDate] Date checked against (defaults to today)
 */
dtps.isDueOnDate = function (date, onDate = new Date()) {
    const d1 = new Date(date);

    const isOnDate = d1.getFullYear() === onDate.getFullYear() &&
        d1.getMonth() === onDate.getMonth() &&
        d1.getDate() === onDate.getDate();

    return isOnDate;
}

/**
 * Compiles and displays due today / to-do stream
 * 
 * @param {boolean} doneLoading True if all classes have finished loading their assignment lists
 * @param {Date} [fromDate] The date to display assignments from. Defaults to the current date.
 */
dtps.renderDueToday = function (doneLoading, fromDate) {
    //Combine class stream arrays
    var combinedStream = [];
    if (dtps.classes) {
        dtps.classes.forEach(course => {
            if (course.assignments) {
                //If course has assignments, add them to the combined stream array
                course.assignments.forEach(assignment => {
                    //Only assignments that are due today
                    if (dtps.isDueOnDate(assignment.dueAt, fromDate)) {
                        combinedStream.push(assignment);
                    }
                })
            }
        })
    }

    //Sort combined stream by date
    combinedStream.sort(function (a, b) {
        var keyA = new Date(a.dueAt).getTime(),
            keyB = new Date(b.dueAt).getTime();

        // Compare the 2 dates
        if (keyA > keyB) return 1;
        if (keyA < keyB) return -1;
        return 0;
    });

    //Get due today stream HTML
    if (dtps.user.parent) {
        var combinedHTML = dtps.mergeAndRenderChildAssignments(combinedStream);
    } else {
        var combinedHTML = combinedStream.map(assignment => {
            return dtps.renderAssignment(assignment);
        }).join("");
    }

    if (combinedStream.length == 0) {
        //Nothing due today
        if (doneLoading) {
            combinedHTML = `<p style="text-align: center;margin: 10px 0px; font-size: 18px;"><i class="fluid-icon">done</i> ${fromDate ? "Nothing is due on " + fromDate.toLocaleString("en", { weekday: 'short', month: 'short', day: 'numeric' }) : "Nothing is due today"}</p>`;
        } else {
            combinedHTML = ``;
        }
    } else {
        //Add header
        combinedHTML = /*html*/`
            <h5 style="text-align: center; margin: 10px 0px; font-weight: bold;">${fromDate ? "Due on " + fromDate.toLocaleString("en", { weekday: 'short', month: 'short', day: 'numeric' }) : "Due Today"}</h5>
        ` + combinedHTML;
    }

    if (dtps.selectedClass == "dash") {
        jQuery(".classContent .dash .dueToday").html(combinedHTML);
    }
}

/**
 * Compiles and displays upcoming assignments stream
 * 
 * @param {Date} [fromDate] The date to display assignments from. Defaults to the current date.
 */
dtps.renderUpcoming = function (fromDate) {
    //Combine class stream arrays
    var today = fromDate ? new Date(fromDate) : new Date();
    var combinedStream = [];
    if (dtps.classes) {
        dtps.classes.forEach(course => {
            if (course.assignments) {
                //If course has assignments, add them to the combined stream array
                course.assignments.forEach(assignment => {
                    //Only include upcoming assignments that aren't due today
                    if ((new Date(assignment.dueAt).getTime() > today.getTime()) && !dtps.isDueOnDate(assignment.dueAt, today)) {
                        combinedStream.push(assignment);
                    }
                })
            }
        })
    }

    //Sort combined stream by date
    combinedStream.sort(function (a, b) {
        var keyA = new Date(a.dueAt).getTime(),
            keyB = new Date(b.dueAt).getTime();

        // Compare the 2 dates
        if (keyA > keyB) return 1;
        if (keyA < keyB) return -1;
        return 0;
    });

    //Limit stream length to 30 assignments
    if (combinedStream.length > 30) {
        combinedStream.length = 30;
    }

    //Render upcoming assignments stream
    if (dtps.user.parent) {
        var combinedHTML = dtps.mergeAndRenderChildAssignments(combinedStream);
    } else {
        var combinedHTML = combinedStream.map(assignment => {
            return dtps.renderAssignment(assignment);
        }).join("");
    }

    //No upcoming assignments
    if (combinedStream.length == 0) {
        combinedHTML = "";
    }

    if (dtps.selectedClass == "dash") {
        jQuery(".classContent .dash .assignmentStream").html(combinedHTML);
    }
}

/**
 * Renders updates stream (recently graded & announcements)
 * 
 * @param {boolean} [dateSelected] True if a date is selected in the dashboard
 */
dtps.renderUpdates = function (dateSelected) {
    //Get updates HTML
    var updatesHTML = "";

    if (dtps.remoteConfig.remoteUpdate.active && (dtps.remoteConfig.remoteUpdate.host ? dtps.remoteConfig.remoteUpdate.host == window.location.host : true)) {
        $("#remoteUpdateImportant").html(/*html*/`
            <div style="cursor: auto; padding: 20px; --classColor: var(--secText);" class="announcement card open">
                <div class="className">
                    <img src="${dtps.baseURL + "/icon.svg"}" style="vertical-align: middle;width: 22px;margin-right: 5px;" />
                    <span style="font-weight: bold; vertical-align: middle; color: var(--text); font-size: 18px;">${dtps.remoteConfig.remoteUpdate.title}</span>
                </div>
                ${dtps.remoteConfig.remoteUpdate.html}
            </div>
        `);
    }

    if (dateSelected && (dtps.selectedClass == "dash")) {
        return jQuery(".classContent .dash .updatesStream").html(/*html*/`
            <div style="cursor: auto; padding: 20px; --classColor: var(--secText);" class="announcement card open">
                <div class="className">Date Selected</div>
                <p>Updates are not shown while a date is selected</p>
            </div>
        `);
    }

    dtps.updates.forEach(update => {
        if (update.type == "announcement") {
            updatesHTML += /*html*/`
                <div onclick="$(this).toggleClass('open');" style="cursor: pointer; padding: 20px; --classColor: ${dtps.classes[update.class].color};" class="announcement card">
                    <div class="className">${update.title}</div>
                    ${update.body}
                </div>
            `;
        } else if (update.type == "assignment") {
            var scoreHTML = dtps.renderAssignmentScore(update);
            updatesHTML += /*html*/`
                <div onclick="dtps.assignment('${update.id}', ${update.class})" style="--classColor: ${dtps.classes[update.class].color};" class="card recentGrade">
                    <h5>
                        <span>${update.title}</span>

                        <!-- Points display -->
                        ${scoreHTML ? `<div class="points">${scoreHTML}</div>` : ``}
                    </h5>

                    <p>${dtps.user.parent ? "Graded for " + dtps.user.children.find(c => c.id == dtps.classes[update.class].userID).name : "Graded"} at ${dtps.formatDate(update.gradedAt)}</p>
                </div>
            `;
        }
    })

    if (dtps.selectedClass == "dash") {
        jQuery(".classContent .dash .updatesStream").html(updatesHTML);
    }
}


/**
 * Compiles and displays the assignment calendar
 * 
 * @param {boolean} doneLoading True if all classes have finished loading their assignment lists
 */
dtps.calendar = function (doneLoading) {
    if (dtps.selectedClass == "dash") {
        //Calendar events array
        var calEvents = [];

        //Add assignments from every class to calEvents array
        dtps.classes.forEach((course, courseIndex) => {
            if (course.assignments) {
                //Class has assignments
                course.assignments.forEach(assignment => {
                    var date = new Date(assignment.dueAt);

                    var existingEvent = calEvents.find(e => e.id == assignment.id);
                    if (existingEvent && assignment.missing) {
                        //Mark existing event as missing and link to missing assignment
                        existingEvent.missing = true;
                        existingEvent.borderColor = "#c44848";
                        existingEvent.assignmentID = assignment.id;
                        existingEvent.classNum = courseIndex;
                    } else if (!existingEvent) {
                        calEvents.push({
                            title: assignment.title,
                            start: date,
                            id: assignment.id,
                            allDay: true,
                            backgroundColor: course.color,
                            borderColor: (assignment.missing === true) ? '#c44848' : 'transparent',
                            textColor: "white",
                            classNum: courseIndex,
                            assignmentID: assignment.id,
                            missing: assignment.missing
                        });
                    }
                });
            }
        });

        //Render calendar
        if (window.FullCalendar) {
            var calendarEl = document.getElementById('calendar');
            calendar = new FullCalendar.Calendar(calendarEl, {
                locale: "en",
                initialView: 'dayGridMonth',
                events: calEvents,
                eventContent: info => {
                    const { missing } = info.event.extendedProps;
                    const html = /*html*/`
                        <div class='fc-event-main-frame'>
                            <div class='fc-event-title-container'>
                                <div class='fc-event-title fc-sticky'>
                                    ${missing ? `<i title="Assignment is missing" class="fluid-icon statusIcon">remove_circle_outline</i>` : ``}
                                    <span style="vertical-align: middle;">${info.event.title}</span>
                                </div>
                            </div>
                        </div>
                    `;
                    return { html };
                },
                contentHeight: 0,
                handleWindowResize: false,
                headerToolbar: {
                    start: 'title',
                    center: '',
                    end: 'prev,next'
                },
                dateClick: function (info) {
                    if ($(info.dayEl).hasClass("fc-day-today")) return fluid.screen();

                    //Update calendar
                    $("#calendar .fc-daygrid-day").removeClass("active");
                    $(info.dayEl).addClass("active");
                    $("#calendar").addClass("dateSelected");

                    //Display selected date
                    $(".headerArea .contentLabel i").text("event");
                    $(".headerArea .contentLabel span").text("Showing assignments from " + info.date.toLocaleString("en", { weekday: 'short', month: 'short', day: 'numeric' }));
                    $(".headerArea .contentLabel").show();

                    //Re-render dashboard from the selected date
                    dtps.renderDueToday(doneLoading, info.date);
                    dtps.renderUpcoming(info.date);
                    dtps.renderUpdates(true);
                },
                eventClick: function (info) {
                    dtps.assignment(info.event.extendedProps.assignmentID, info.event.extendedProps.classNum);
                }
            });
            calendar.render();
        }
    }
}

/**
 * Shows the assignments stream for a class
 * 
 * @param {string} classID The class ID to show assignments for
 * @param {Assignment[]} [searchResults] An array of assignemnts to render instead of course.assignments. Used for assignment search.
 */
dtps.classStream = function (classID, searchResults) {
    //Get class index and set as selected class
    var classNum = dtps.classes.map(course => course.id).indexOf(classID);
    dtps.selectedClass = classNum;

    //Set stream as the selected content
    dtps.selectedContent = "stream";
    $("#dtpsTabBar .btn").removeClass("active");
    $("#dtpsTabBar .btn.stream").addClass("active");

    //Save coursework stream preference
    window.localStorage.setItem("courseworkPref-" + classID, "stream");

    //Load class color, name, etc.
    dtps.presentClass(classNum);

    //Ensure classes are shown in the sidebar
    dtps.showClasses();

    if (classNum == -1) {
        //Class does not exist
        dtps.error("The selected class doesn't exist", "classNum check failed @ dtps.classStream");
    }

    //Use search results to render or render all class assignments
    var assignments = searchResults || dtps.classes[classNum].assignments;

    //Render assignments
    if (!assignments) {
        //Assignments are still loading
        if ((dtps.selectedClass == classNum) && (dtps.selectedContent == "stream")) {
            jQuery(".classContent").html(dtps.renderStreamTools(classNum, "stream") + [1, 2, 3, 4].map(() => (
                /*html*/`
                    <div class="card assignment graded">
                        <h4>
                            <span style="width: 450px;" class="shimmer">Assignment Title</span>
                            <div class="points shimmer">00/00</div>
                        </h4>

                        <h5 style="white-space: nowrap; overflow: hidden;">
                            <div style="width: 200px;" class="infoChip shimmer"></div>
                            <i class="fluid-icon statusIcon shimmer">more_horiz</i>
                        </h5>
                    </div>
                `
            )).join(""));
        }
    } else if (assignments.length == 0) {
        //This class doesn't have any assignments
        if ((dtps.selectedClass == classNum) && (dtps.selectedContent == "stream")) {
            $(".classContent").html(dtps.renderStreamTools(classNum, "stream") + /*html*/`
                <div style="text-align: center;">
                    <div style="margin: 60px; padding: 20px 40px; display: inline-block; border: 2px solid var(--elements); border-radius: var(--elementRadius);">
                        <h5>No Assignments</h5>
                        <p>There aren't any assignments in this class yet.</p>
                    </div>
                </div>
            `);
        }
    } else {
        //Sort assignments
        assignments.sort(function (a, b) {
            var keyA = new Date(a.dueAt).getTime(),
                keyB = new Date(b.dueAt).getTime();

            var now = new Date().getTime();

            //Put assignments without a due date at the end
            if (!a.dueAt) { keyA = 0; }
            if (!b.dueAt) { keyB = 0; }

            //Put upcoming assignments at the top
            if ((keyA < now) || (keyB < now)) {
                // Sort upcoming assignments from earliest -> latest
                if (keyA < keyB) return 1;
                if (keyA > keyB) return -1;
                return 0;
            } else {
                // Sort old assignments from latest -> earliest
                if (keyA > keyB) return 1;
                if (keyA < keyB) return -1;
                return 0;
            }
        });

        //Render assignments
        var prevAssignment = null;
        var streamHTML = assignments.map(assignment => {
            var divider = "";

            if (!assignment.dueAt) {
                if (prevAssignment && (prevAssignment !== "undated")) {
                    divider = `<h5 style="margin: 75px 20px 10px 20px;font-weight: bold;">Undated Assignments</h5>`;
                }

                prevAssignment = "undated";
            } else if (new Date(assignment.dueAt) < new Date()) {
                if (prevAssignment && (prevAssignment !== "old")) {
                    divider = `<h5 style="margin: 75px 20px 10px 20px;font-weight: bold;">Old Assignments</h5>`;
                }

                prevAssignment = "old";
            } else {
                prevAssignment = "upcoming";
            }

            return divider + dtps.renderAssignment(assignment);
        }).join("");

        //Add stream header with class info buttons and search box
        streamHTML = dtps.renderStreamTools(classNum, "stream") + streamHTML;

        if ((dtps.selectedClass == classNum) && (dtps.selectedContent == "stream")) {
            $(".classContent").html(streamHTML);
        }
    }
}

/**
 * Shows details for an assignment given the assignment ID and class number
 * 
 * @param {string} id Assignment ID
 * @param {number} classNum Assignment class number
 * @param {boolean} [generic] True if user-specific details, such as grades, should not be displayed
 */
dtps.assignment = function (id, classNum, generic) {
    //Get assignment from the id prop
    var assignmentIDs = dtps.classes[classNum].assignments.map(assignment => assignment.id);
    var assignment = dtps.classes[classNum].assignments[assignmentIDs.indexOf(id)];

    //The assignment body is rendered in an iFrame to keep its content and styling isolated from the rest of the page
    var assignmentBodyHTML = null;
    if (assignment.body) {
        //Get computed background and text color to style the iFrame with
        var computedBackgroundColor = getComputedStyle($(".card.details")[0]).getPropertyValue("--cards");
        var computedTextColor = getComputedStyle($(".card.details")[0]).getPropertyValue("--text");

        var body = assignment.body;
        if ($("body").hasClass("dark")) {
            body = dtps.brightenTextForDarkMode(assignment.body, computedBackgroundColor);
        }

        //Generate a blob with the assignment body and get its data URL
        var blob = new Blob([`
                    <base target="_blank" /> 
                    <link type="text/css" rel="stylesheet" href="https://cdn.jottocraft.com/CanvasCSS.css" media="screen,projection"/>
                    <style>body {background-color: ${computedBackgroundColor}; color: ${computedTextColor};}</style>
                    ${body}
                `], { type: 'text/html' });
        assignmentBodyHTML = `<iframe id="assignmentIframe" onload="dtps.iframeLoad('assignmentIframe')" style="margin: 10px 0px; width: 100%; border: none; outline: none;" src="${window.URL.createObjectURL(blob)}" />`;

    }

    //Get assignment rubric HTML
    var assignmentRubricHTML = "";
    if (assignment.rubric) {
        var assignmentRubricHTML = assignment.rubric.map(function (rubricItem) {
            return /*html*/`
                    <div class="dtpsAssignmentRubricItem">
                        <h5>${rubricItem.title}</h5>
                        <div class="score">
                            <p style="color: ${(!generic && rubricItem.color) ? rubricItem.color : "var(--secText)"};" class="scoreName">
                                ${generic ? "" : (rubricItem.score !== undefined ? rubricItem.scoreName || "" : "Not assessed")}

                                <div class="points">
                                    <p class="earned">${generic || (rubricItem.score == undefined) ? "" : rubricItem.score}</p>
                                    <p class="possible">${"/" + rubricItem.value}</p>
                                </div>
                            </p>
                        </div>
                    </div>
                `
        }).join("");
    }

    //Get assignment feedback HTML
    var assignmentFeedbackHTML = "";
    if (assignment.feedback) {
        var assignmentFeedbackHTML = assignment.feedback.map(feedback => {
            return /*html*/`
                    <div class="dtpsSubmissionComment">
                        ${feedback.author ? /*html*/`
                            <img src="${feedback.author.photoURL}" />
                            <h5>${feedback.author.name}</h5>
                        ` : ``}

                        <p>${feedback.comment}</p>
                    </div>
                `
        }).join("");
    }

    //Get assignment score HTML
    var scoreHTML = dtps.renderAssignmentScore(assignment);

    //Render assignment details
    $(".card.details").html(/*html*/`
            <i onclick="fluid.cards.close('.card.details'); $('.card.details').html('');" class="fluid-icon close">close</i>

            <h4 style="font-weight: bold;">${assignment.title}</h4>

            <div>
                ${assignment.dueAt ? `<div class="assignmentChip"><i class="fluid-icon">alarm</i><span>Due ${dtps.formatDate(assignment.dueAt)}</span></div>` : ""}
                ${assignment.turnedIn && !generic ? `<div title="Assignment submitted" class="assignmentChip" style="color: #0bb75b"><i class="fluid-icon">assignment_turned_in</i></div>` : ""}
                ${assignment.missing && !generic ? `<div  title="Assignment is missing" class="assignmentChip" style="color: #c44848"><i class="fluid-icon">remove_circle_outline</i></div>` : ""}
                ${assignment.late && !generic ? `<div title="Assignment is late" class="assignmentChip" style="color: #c44848"><i class="fluid-icon">assignment_late</i></div>` : ""}
                ${assignment.locked && !generic ? `<div title="Assignment submissions are locked" class="assignmentChip" style="color: var(--secText, gray);"><i class="fluid-icon">lock_outline</i></div>` : ""}
                ${(dtps.user.parent && !generic) ? `<div class="assignmentChip"><i class="fluid-icon">person</i><span>${dtps.user.children.find(c => c.id == dtps.classes[assignment.class].userID).name}</span></div>` : ""}
                ${generic ? "" : scoreHTML}
            </div>

            ${assignment.error ? /*html*/`
                <div>
                    <div class="alert error"> 
                        <div class="header"> 
                          <h5><i class="fluid-icon">error</i> Assignment Error</h5> 
                        </div> 

                        <p>${assignment.error}</p> 
                    </div>
                </div>
            ` : ``}

            <div style="margin-top: 20px;" class="assignmentBody">
                ${assignment.body ? assignmentBodyHTML : ""}
            </div>

            ${assignment.body ? `<div style="margin: 5px 0px; background-color: var(--darker); height: 1px; width: 100%;" class="divider"></div>` : ""}

            <div style="width: calc(40% - 2px); margin-top: 20px; display: inline-block; overflow: hidden; vertical-align: top;">
                ${assignment.publishedAt ? `<p style="color: var(--secText); margin: 5px 0px;"><i style="vertical-align: middle;" class="fluid-icon">add_box</i> Published: ${dtps.formatDate(assignment.publishedAt)}</p>` : ""}
                ${assignment.dueAt ? `<p style="color: var(--secText); margin: 5px 0px;"><i style="vertical-align: middle;" class="fluid-icon">alarm</i> Due: ${dtps.formatDate(assignment.dueAt)}</p>` : ""}
                ${assignment.value ? `<p style="color: var(--secText); margin: 5px 0px;"><i style="vertical-align: middle;" class="fluid-icon">bar_chart</i> Point value: ${assignment.value}</p>` : ""}
                ${(assignment.grade || (assignment.grade == 0)) && !generic ? `<p style="color: var(--secText); margin: 5px 0px;"><i style="vertical-align: middle;" class="fluid-icon">assessment</i> Points earned: ${assignment.grade}</p>` : ""}
                ${assignment.letter && !generic ? `<p style="color: var(--secText); margin: 5px 0px;"><i style="vertical-align: middle;" class="fluid-icon">font_download</i> Letter grade: ${assignment.letter}</p>` : ""}
                ${assignment.category ? `<p style="color: var(--secText); margin: 5px 0px;"><i style="vertical-align: middle;" class="fluid-icon">category</i> Category: ${assignment.category}</p>` : ""}
                ${assignment.rubric ? assignment.rubric.map(function (rubricItem) { return `<p style="color: var(--secText); margin: 5px 0px;"><i style="vertical-align: middle;" class="fluid-icon">adjust</i> ${rubricItem.title}</p>`; }).join("") : ""}
                <p style="color: var(--secText); margin: 5px 0px;"><i style="vertical-align: middle;" class="fluid-icon">class</i> Class: ${dtps.classes[assignment.class].subject}</p>
                ${dtps.env == "dev" ? `<p style="color: var(--secText); margin: 5px 0px;"><i style="vertical-align: middle;" class="fluid-icon">code</i> Assignment ID: ${assignment.id}</p>` : ""}

                <br />
                <div class="row">
                    ${assignment.url ? `<button class="btn small outline" onclick="window.open('${assignment.url}')"><i class="fluid-icon">open_in_new</i> Open in ${dtpsLMS.shortName || dtpsLMS.name}</button>` : ``}
                </div>
            </div>

            <div style="width: calc(60% - 10px); margin-top: 20px; margin-left: 5px; display: inline-block; overflow: hidden; vertical-align: middle;">
                ${!generic ? assignmentFeedbackHTML : ""}

                ${assignmentRubricHTML}
            </div>
        `);

    //Close other active cards and open the assignment details card
    fluid.cards.close(".card.focus");
    fluid.cards(".card.details");
}

/**
 * Shows the module stream for a class
 * 
 * @param {string} classID Class number to fetch modules for
 */
dtps.moduleStream = function (classID) {
    //Get class index and set as selected class
    var classNum = dtps.classes.map(course => course.id).indexOf(classID);
    dtps.selectedClass = classNum;

    //Set stream as the selected content
    dtps.selectedContent = "moduleStream";
    $("#dtpsTabBar .btn").removeClass("active");
    $("#dtpsTabBar .btn.stream").addClass("active");

    //Save coursework stream preference
    window.localStorage.setItem("courseworkPref-" + classID, "moduleStream");

    //Load class color, name, etc.
    dtps.presentClass(classNum);

    //Ensure classes are shown in the sidebar
    dtps.showClasses();

    if (classNum == -1) {
        //Class does not exist
        dtps.error("The selected class doesn't exist", "classNum check failed @ dtps.moduleStream");
    }

    //Show loading indicator
    if ((dtps.selectedClass == classNum) && (dtps.selectedContent == "moduleStream")) {
        jQuery(".classContent").html(dtps.renderStreamTools(classNum, "modules") + /*html*/`
            <div class="module card collapsed">
                <h4>
                    <i class="fluid-icon collapseIcon shimmer">star</i>
                    <span class="shimmer">[SHIMMER] Module title (collapsed)</span>
                </h4>
                <div class="moduleContents" style="padding-top: 10px;"></div>
            </div>
            <div class="module card">
                <h4>
                    <i class="fluid-icon collapseIcon shimmer">star</i>
                    <span class="shimmer">[SHIMMER] Module title</span>
                </h4>
        
                <div class="moduleContents" style="padding-top: 10px;">
                    <div class="moduleItem shimmer">[SHMMER] module item</div>
                    <div class="moduleItem shimmer">[SHMMER] module item</div>
                    <div class="moduleItem shimmer">[SHMMER] module item</div>
                    <div class="moduleItem shimmer">[SHMMER] module item</div>
                    <div class="moduleItem shimmer">[SHMMER] module item</div>
                </div>
            </div>
        `);
    }

    new Promise(resolve => {
        if (dtps.classes[classNum].modules && (dtps.classes[classNum].modules !== true)) {
            resolve(dtps.classes[classNum].modules);
        } else {
            dtpsLMS.fetchModules(dtps.user.id, dtps.classes[classNum].lmsID).then(data => resolve(data));
        }
    }).then(data => {
        //Save modules data in the class object for future use
        dtps.classes[classNum].modules = data;

        var modulesHTML = dtps.renderStreamTools(classNum, "modules");
        var allCollapsed = true;

        data.forEach(module => {
            var moduleItemHTML = "";
            if (!module.collapsed) allCollapsed = false;

            //Get HTML for each module item
            module.items.forEach(item => {
                //Get module item icon
                var icon = "list";
                if (item.type == "assignment") icon = "assignment";
                if (item.type == "page") icon = "insert_drive_file";
                if (item.type == "discussion") icon = "forum";
                if (item.type == "url") icon = "open_in_new";
                if (item.type == "header") icon = "format_size";
                if (item.type == "embed") icon = "web";

                //Get module action
                var action = "";
                if (item.type == "assignment") action = "dtps.assignment('" + item.id + "', " + classNum + ")";
                if (item.type == "page") {
                    action = "fluid.screen('pages', '" + classID + "|" + item.id + "|true')";
                }
                if (item.type == "discussion") {
                    action = "fluid.screen('discussions', '" + classID + "|" + item.id + "|true')";
                }
                if (item.type == "url") action = "window.open('" + item.url + "')";
                if (item.type == "header") action = "";
                if (item.type == "embed") action = "dtps.showIFrameCard('" + item.url + "')";

                //Get module HTML
                if (item.type == "header") {
                    moduleItemHTML += `<h5 style="font-size: 16px;padding: 0px 10px; text-decoration: underline;">${item.title}</h5>`;
                } else {
                    moduleItemHTML += `
                        <div class="moduleItem" onclick="${action}" style="margin-left: ${item.indent * 15}px;">
                            <i ${item.completed ? `style="color: #27ba3c"` : ""} class="fluid-icon">${item.completed ? "check" : icon}</i>
                            ${item.title}
                        </div>
                    `;
                }
            });

            //Add HTML for this module to the string
            modulesHTML += /*html*/`
                <div class="module card ${module.collapsed ? "collapsed" : ""}">
                    <h4>
                        <i onclick="dtps.moduleCollapse(this, '${dtps.classes[classNum].id}', '${module.id}');" 
                            class="fluid-icon collapseIcon">${module.collapsed ? "keyboard_arrow_right" : "keyboard_arrow_down"}</i>
                        ${module.title}
                    </h4>

                    <div class="moduleContents" style="padding-top: 10px;">
                        ${moduleItemHTML}
                    </div>
                </div>
            `;
        });

        if (data.length == 0) {
            modulesHTML += /*html*/`
                <div style="text-align: center;">
                    <div style="margin: 60px; padding: 20px 40px; display: inline-block; border: 2px solid var(--elements); border-radius: var(--elementRadius);">
                        <h5>No Modules</h5>
                        <p>There aren't any modules in this class yet.</p>
                    </div>
                </div>
            `;
        }

        //Render module HTML
        if ((dtps.selectedClass == classNum) && (dtps.selectedContent == "moduleStream")) {
            $(".classContent").html(modulesHTML);
            if (dtpsLMS.collapseAllModules) {
                $("#moduleExpandCollapse").html(allCollapsed ? `<i class="fluid-icon">unfold_more</i> Expand all` : `<i class="fluid-icon">unfold_less</i> Collapse all`);
                $("#moduleExpandCollapse").attr("onclick", allCollapsed ? `dtps.moduleCollapseAll(false)` : `dtps.moduleCollapseAll(true)`);
                $("#moduleExpandCollapse").show();
            }
        }
    }).catch(err => {
        dtps.error("Could not load modules", "Caught promise rejection @ dtps.moduleStream", err);
    });
}

/**
 * Collapses a module
 * 
 * @param {Element} ele Element of the module collapse arrow
 * @param {string} classID Class ID
 * @param {string} modID Module ID of the module to collapse
 */
dtps.moduleCollapse = function (ele, classID, modID) {
    //Add collapsed class to module card
    $(ele).parents('.card').toggleClass('collapsed');

    //Update arrow icon and, if the LMS supports it, collapse the module in the LMS as well
    if ($(ele).parents('.card').hasClass('collapsed')) {
        if (dtpsLMS.collapseModule) dtpsLMS.collapseModule(classID, modID, true);
        $(ele).html('keyboard_arrow_right');
        dtps.classes[dtps.selectedClass].modules.find(m => m.id == modID).collapsed = true;
    } else {
        if (dtpsLMS.collapseModule) dtpsLMS.collapseModule(classID, modID, false);
        $(ele).html('keyboard_arrow_down');
        dtps.classes[dtps.selectedClass].modules.find(m => m.id == modID).collapsed = false;
    }
}


/**
 * Collapses all module
 * 
 * @param {boolean} collapse If true, modules will be collapsed, otherwise, they will all be expanded
 */
dtps.moduleCollapseAll = function (collapse) {
    if (collapse && dtpsLMS.collapseAllModules) {
        dtpsLMS.collapseAllModules(dtps.classes[dtps.selectedClass].lmsID, true);
        $(".classContent .card.module h4 i").text("keyboard_arrow_right");
        $(".classContent .module.card").addClass("collapsed");
        dtps.classes[dtps.selectedClass].modules.forEach(m => m.collapsed = true);
        $("#moduleExpandCollapse").html(`<i class="fluid-icon">unfold_more</i> Expand all`);
        $("#moduleExpandCollapse").attr("onclick", `dtps.moduleCollapseAll(false)`);
    } else if (dtpsLMS.collapseAllModules) {
        dtpsLMS.collapseAllModules(dtps.classes[dtps.selectedClass].lmsID, false);
        $(".classContent .card.module h4 i").text("keyboard_arrow_down");
        $(".classContent .module.card").removeClass("collapsed");
        dtps.classes[dtps.selectedClass].modules.forEach(m => m.collapsed = false);
        $("#moduleExpandCollapse").html(`<i class="fluid-icon">unfold_less</i> Collapse all`);
        $("#moduleExpandCollapse").attr("onclick", `dtps.moduleCollapseAll(true)`);
    }
}

/**
 * Gets stream tools HTML (search box, class info, and modules/assignment switcher)
 * 
 * @param {number} num Class number
 * @param {string} type Class content these tools are for (e.g. "stream")
 * @return {string} Stream tools HTML
 */
dtps.renderStreamTools = function (num, type) {
    var modulesSelector = dtps.classes[num].modules;
    return /*html*/`
        ${(type == "modules") || (type == "stream") ? /*html*/`
            <div style="text-align: right;${modulesSelector ? "" : "margin-top: 20px;"}">
                ${modulesSelector ? /*html*/`
                    ${(type == "modules") && dtpsLMS.collapseAllModules ? `<button id="moduleExpandCollapse" onclick="dtps.moduleCollapseAll()" style="margin-right:20px;display:none;" class="btn"></button>` : ""}
                    <div class="btns row small assignmentPicker" style="margin: 5px 20px 5px 0px !important;">
                        <button class="btn ${type == "stream" ? "active" : ""}" onclick="fluid.screen('stream', dtps.classes[dtps.selectedClass].id);"><i class="fluid-icon">assignment</i>Assignments</button>
                        <button class="btn ${type == "modules" ? "active" : ""}" onclick="fluid.screen('moduleStream', dtps.classes[dtps.selectedClass].id);"><i class="fluid-icon">view_module</i>Modules</button>
                    </div>
                ` : ``}
                
            </div>` : ``
        }
    `;
}

/**
 * Shows the generic gradebook
 * 
 * @param {string} classID Class ID
 */
dtps.gradebook = function (classID) {
    //Get class index and set as selected class
    var classNum = dtps.classes.map(course => course.id).indexOf(classID);
    dtps.selectedClass = classNum;

    //Set stream as the selected content
    dtps.selectedContent = "grades";
    $("#dtpsTabBar .btn").removeClass("active");
    $("#dtpsTabBar .btn.grades").addClass("active");

    //Load class color, name, etc.
    dtps.presentClass(classNum);

    //Ensure classes are shown in the sidebar
    dtps.showClasses();

    if (classNum == -1) {
        //Class does not exist
        dtps.error("The selected class doesn't exist", "classNum check failed @ dtps.gradebook");
    }

    //Show loading indicator
    if ((dtps.selectedClass == classNum) && (dtps.selectedContent == "grades")) {
        jQuery(".classContent").html(`<div class="spinner"></div>`);
    }

    //Terminate function if the class doesn't have a letter grade or assignments
    if (!(dtps.classes[classNum].letter || dtps.classes[classNum].grade) || !dtps.classes[classNum].assignments) {
        return;
    }

    //Define variables for total points and zeros
    var zeros = 0;
    var totalPoints = 0;
    var earnedPoints = 0;
    var gradedAssignments = 0;

    //Define variable for assignment HTML string
    var assignmentHTML = "";

    //Calculate total points and zeros
    dtps.classes[classNum].assignments.forEach(assignment => {
        if (assignment.grade || (assignment.grade == 0)) {
            earnedPoints += assignment.grade;
            gradedAssignments++;

            assignmentHTML += /*html*/`
                <div onclick="dtps.assignment('${assignment.id}', ${classNum})" class="gradebookAssignment">
                    <h5>
                        ${assignment.title}

                        <div class="stats">
                            ${assignment.letter ? `<div class="gradebookLetter">${assignment.letter}</div>` : ""}
                            <div class="gradebookScore">${Math.round(assignment.grade * 100) / 100}</div>
                            <div class="gradebookValue">/${Math.round(assignment.value * 100) / 100}</div>
                            <div class="gradebookPercentage">${Math.round((assignment.grade / assignment.value) * 100)}%</div>
                        </div>
                    </h5>
                </div>
            `;
        }

        if (assignment.value) {
            totalPoints += assignment.value;
        }

        if ((assignment.grade == 0) && assignment.value) {
            zeros++;
        }
    })

    //Grade calculation summary
    var gradeCalcSummary = /*html*/`
        <div style="--classColor: ${dtps.classes[classNum].color}" class="card">
            <h3 class="gradeTitle">
                Grade Summary
                <div class="classGradeCircle">
                    ${dtps.classes[classNum].grade ? `<div class="percentage">${dtps.classes[classNum].grade}%</div>` : ``}
                    ${dtps.classes[classNum].letter ? `<div class="letter">${dtps.classes[classNum].letter}</div>` : ""}
                </div>
            </h3>

            ${zeros ? /*html*/`
                <h5 style="color: #d63d3d;" class="gradeStat">
                    Zeros
                    <div style="color: #d63d3d;" class="numFont">${zeros}</div>
                </h5>
            ` : ``}

            <div style="${dtps.gradebookExpanded ? "" : "display: none;"}" id="genericClassGradeMore">
                <br />

                ${dtps.classes[classNum].previousLetter ? /*html*/`
                    <h5 class="smallStat">
                        Previous Grade
                        <div class="numFont">${dtps.classes[classNum].previousLetter}</div>
                    </h5>
                ` : ``}

                <h5 class="smallStat">
                    Points
                    <div class="numFont fraction">
                        <span class="earned">${Math.round(earnedPoints)}</span>
                        <span class="total">/${Math.round(totalPoints)}</span>
                    </div>
                </h5>

                <h5 class="smallStat">
                    Graded Assignments
                    <div class="numFont">${gradedAssignments}</div>
                </h5>
            </div>

            <br />
            <a onclick="$('#genericClassGradeMore').toggle(); if ($('#genericClassGradeMore').is(':visible')) {$(this).html('Show less'); dtps.gradebookExpanded = true;} else {$(this).html('Show more'); dtps.gradebookExpanded = false;}"
                style="color: var(--secText, gray); cursor: pointer; margin-right: 10px;">${dtps.gradebookExpanded ? "Show less" : "Show more"}</a>
        </div>

        <br />
    `;


    //Render HTML
    if ((dtps.selectedClass == classNum) && (dtps.selectedContent == "grades")) {
        $(".classContent").html(gradeCalcSummary + assignmentHTML);
    }
}

//Fluid UI screen definitions
fluid.externalScreens.dashboard = () => {
    dtps.mainStream();
}

fluid.externalScreens.stream = (courseID) => {
    dtps.classStream(courseID);
}

fluid.externalScreens.moduleStream = (courseID) => {
    dtps.moduleStream(courseID);
}

fluid.externalScreens.gradebook = (courseID) => {
    const courseLMSGradebookAllowed = dtpsLMS.lmsGradebookAllowlist ? dtpsLMS.lmsGradebookAllowlist.includes(courseID) : true;
    if (dtpsLMS.gradebook && courseLMSGradebookAllowed && !((dtps.env == "dev") && (fluid.get("pref-debuggingGenericGradebook") == "true"))) {
        //Handle LMS gradebook
        dtps.showLMSGradebook(courseID);
    } else if (dtpsLMS.genericGradebook || ((dtps.env == "dev") && (fluid.get("pref-debuggingGenericGradebook") == "true"))) {
        //Generic gradebook script
        dtps.gradebook(courseID);
    }
}

//Type definitions

/**
* @typedef {Object} Assignment
* @description Defines assignments objects in DTPS
* @property {string} title Title of the assignment
* @property {string} [body] HTML of the assignment's body
* @property {string} id Assignment ID
* @property {number} class Automatically managed by DTPS. The class number that this assignment belongs to.
* @property {Date} [dueAt] Assignment due date
* @property {string} [url] Assignment URL
* @property {AssignmentFeedback[]} [feedback] Feedback / private comments for this assignment
* @property {User} [grader] Assignment grader
* @property {boolean} [turnedIn] True if the assignment is turned in
* @property {boolean} [late] True if the assignment is late
* @property {boolean} [missing] True if the assignment is missing
* @property {boolean} [locked] True if assignment submissions are locked
* @property {string} [category] Assignment category
* @property {Date} [publishedAt] Date for when the assignment was published
* @property {Date} [gradedAt] Date for when the assignment was graded
* @property {number} [grade] Points earned on this assignment
* @property {string} [letter] Letter grade on this assignment
* @property {number} [value] Total amount of points possible for this assignment
* @property {RubricItem[]} [rubric] Assignment rubric
* @property {string} [error] If an error occures when processing this assignment, store an error message here to notify the user that some features/data may be unavailable
*/

/**
* @typedef {Object} Module
* @description Defines module objects in DTPS
* @property {string} title Title of the module
* @property {string} id Module ID
* @property {boolean} [collapsed] True if the module is collapsed, false otherwise. undefined or null if this module does not support collapsing.
* @property {ModuleItem[]} items An array of items for this module.
*/

/**
 * @typedef {Object} ModuleItem
 * @description Defines module items in DTPS
 * @property {string} type Either "assignment", "page", "discussion", "url", "embed", or "header".
 * @property {string} [title] Required for URL and header items, and can be used to override the title of assignment, page, and discussion items.
 * @property {string} [id] Required for assignment, page, and discussion items.
 * @property {string} [url] Required for URL and embed items. Required for discussion and page items if the class does not support the pages or discussions feature.
 * @property {number} [indent] Indent level
 * @property {boolean} [completed] True if the module item has been completed
 */

/**
* @typedef {Object} Announcement
* @description Defines announcement objects in DTPS
* @property {string} title Title of the announcement
* @property {Date} postedAt Date when the announcement was posted
* @property {string} body Announcement content
* @property {string} url Announcement URL
*/

/**
* @typedef {Object} AssignmentFeedback
* @description Defines assignment feedback objects in DTPS
* @property {string} comment Feedback comment
* @property {User} [author] Feedback author
*/

/**
* @typedef {Object} RubricItem
* @description Defines rubric item objects in DTPS
* @property {string} title Title of the rubric item
* @property {string} id Rubric item ID (only needs to be unique to this assignment)
* @property {number} value Total amount of points possible
* @property {string} [outcome] The ID of the outcome this rubric item is assessing. This is only used for grade calculation.
* @property {string} [description] Rubric item description
* @property {number} [score] Rubric assessment score in points
* @property {string} [scoreName] Rubric assessment score name
* @property {string} [color] Score color in a CSS color format
*/

/**
 * @typedef {Object} DashboardItem
 * @description Defines dashboard items in DTPS
 * @property {string} name Dashbord item name
 * @property {string} id Unique dashboard item ID
 * @property {string} icon Dashboard item icon
 * @property {boolean} supportsCompactMode True if this dashboard item supports compact mode
 * @property {number} size The approximate size of this dashboard item relative to other dashboard items. Should be no less than 20.
 * @property {string} defaultSide The default side of this dashboard item. Either "right" or "left".
 * @property {boolean} compact True if the user has turned on compact mode for this item.
 */