diff --git a/changelog.md b/changelog.md index cee18310f..e8caef7e2 100644 --- a/changelog.md +++ b/changelog.md @@ -8,9 +8,9 @@ - Enhancement: [#328] Swap position for overall and month balance on day view - Enhancement: [#333] Adding start date for overall balance on preferences - Enhancement: [#357] Adding flexible table format for month calendar with variable number of entries per day +- Enhancement: [#369] Adding flexible table format for day calendar as well - Enhancement: [#383] Add system default theme that auto-detect if dark or light mode is set - Who built 1.5.6: - thamara diff --git a/css/styles.css b/css/styles.css index f831fb047..b1e98cb7e 100644 --- a/css/styles.css +++ b/css/styles.css @@ -172,7 +172,7 @@ body { } .error-tr { - border-bottom: 2px solid var(--error); + border-bottom: 2px solid var(--error) !important; } .title-header-text { @@ -1025,3 +1025,247 @@ html[data-view='flexible'] #month-balance { html[data-view='flexible'] #overall-balance { width: 70px; } + +/** Flexible Day view related styles **/ + +html[data-view='flexible-day'] .title-header { + width: 100%; + margin-bottom: 80px; +} + +html[data-view='flexible-day'] .title-header-text { + font-size: 300%; +} + +html[data-view='flexible-day'] .table-header { + width: 100%; +} + +html[data-view='flexible-day'] .th-month-name { + font-size: 115%; +} + +html[data-view='flexible-day'] #calendar input[type="date" i]::-webkit-calendar-picker-indicator { + background-image: var(--day-calendar-select-icon); + height: 10px; + margin-left: 2px; + margin-right: -5px; + width: 10px; +} + +html[data-view='flexible-day'] input[type="time" i]::-webkit-calendar-picker-indicator { + padding: 0; + margin: 0; +} + +html[data-view='flexible-day'] #calendar-table-body { + width: 100%; + display: flex; + flex-direction: column; + margin-top: 10px; +} + +html[data-view='flexible-day'] #calendar-table-body .th-label { + margin-top: auto; + margin-bottom: auto; + width: 30%; +} + +html[data-view='flexible-day'] #calendar-table-body .first-group { + width: 30%; + text-align: center; +} + +html[data-view='flexible-day'] #calendar-table-body .second-group { + width: 63%; + text-align: center; +} + +@media (min-width: 768px) { + html[data-view='flexible-day'] #calendar-table-body .second-group { + width: 67%; + } +} + +html[data-view='flexible-day'] #calendar-table-body .third-group { + width: 5%; + text-align: center; +} + +html[data-view='flexible-day'] #calendar-table-body .rows-time { + display: flex; + flex-direction: column; +} + +html[data-view='flexible-day'] #calendar-table-body .row-interval, +html[data-view='flexible-day'] #calendar-table-body .row-entry-pair, +html[data-view='flexible-day'] #calendar-table-body .row-controls { + display: inline-flex; + border-bottom: 1px solid var(--table-border); + width: 90% !important; + margin: auto; + padding: 2px 0 2px 0; +} + +html[data-view='flexible-day'] #calendar-table-body .row-entry-pair input { + width: 100px; + text-align: center; +} + +html[data-view='flexible-day'] #calendar-table-body .interval, +html[data-view='flexible-day'] .day-total-cell { + width: 80px; + margin: auto; +} + +html[data-view='flexible-day'] .interval, +html[data-view='flexible-day'] .day-total-cell { + background-color: var(--table-cell-total-bground) !important; + color: var(--table-cell-total-color) !important; + text-align: center; + padding: 2px; + user-select: none; + height: 28px; +} + +html[data-view='flexible-day'] #calendar-table-body .sign-cell { + text-align: center; + display: flex; + justify-content: space-evenly; + width: 24px; + user-select: none; +} + +html[data-view='flexible-day'] #calendar-table-body .sign-cell .sign-container { + display: inline-block; + height: 24px; + overflow: hidden; + background-color: var(--table-cell-total-bground); + border-radius: 15px; + width: 25px; + cursor: pointer; +} + +html[data-view='flexible-day'] #calendar-table-body .sign-cell .sign-container:hover { + background-color: var(--page-color); + transition: background-color 0.1s ease; +} + +html[data-view='flexible-day'] #calendar-table-body .sign-cell span { + font-weight: bold; + font-size: 25px; + position: relative; + color: var(--page-bground); +} + +html[data-view='flexible-day'] #calendar-table-body .row-total .third-group { + margin-top: 1px; +} + +html[data-view='flexible-day'] #calendar-table-body .sign-cell .minus-sign { + top: -7px; +} + +html[data-view='flexible-day'] #calendar-table-body .sign-cell .plus-sign { + top: -6px; +} + +html[data-view='flexible-day'] .table-header tr { + border-bottom: 2px solid var(--punch-bground); +} + +html[data-view='flexible-day'] .but-switch-view { + width: 6.5%; +} + +html[data-view='flexible-day'] .table-body { + margin-top: 10px; +} + +html[data-view='flexible-day'] .row-total { + display: inline-flex; + width: 90% !important; + margin: auto; + border-bottom: none !important; + padding: 10px 0 10px 0; +} + +html[data-view='flexible-day'] .row-total .th { + margin: auto 0 auto 0; +} + +html[data-view='flexible-day'] .row-waiver { + margin: auto; + padding: 2px 0 12px 0; + display: flex; +} + +html[data-view='flexible-day'] .row-waiver .waived-day-text { + margin: auto 20px auto auto; +} + +html[data-view='flexible-day'] .today-non-working { + border-bottom: none !important; +} + +html[data-view='flexible-day'] .non-working-day { + background: none; + margin: auto auto 10px auto; + text-align: center; +} + +html[data-view='flexible-day'] .summary { + padding: 5px 0 5px 0; + display: flex; + border-top: 4px solid var(--table-total-border); + border-bottom: none !important; +} + +html[data-view='flexible-day'] .summary .leave-by-text { + margin: 2px 20px auto auto; +} + +html[data-view='flexible-day'] .summary .leave-by-time { + margin-right: auto; +} + +html[data-view='flexible-day'] .month-total-row { + width: 100%; + display: flex; + flex: auto; + flex-direction: row; + border-top: 4px solid var(--table-total-border); + border-bottom: none; + margin: auto; + padding: 5px 0 2px 0; +} + +html[data-view='flexible-day'] .month-total-row .month-total-text { + padding-right: 10px; + text-align: right; +} + +html[data-view='flexible-day'] .month-total-row .month-total-element { + margin-left: auto; + margin-right: auto; + display: flex; + width: fit-content; +} + +html[data-view='flexible-day'] .month-total-row .month-total-time { + text-align: left; +} + +html[data-view='flexible-day'] .footer { + height: 32px; + font-size: 100%; +} + +html[data-view='flexible-day'] .footer .punch-button { + font-weight: 700; + font-family: 'Montserrat', sans-serif; +} + +html[data-view='flexible-day'] .punch-button img { + display: none; +} diff --git a/js/classes/Calendar.js b/js/classes/Calendar.js index 97ee29004..51f037127 100644 --- a/js/classes/Calendar.js +++ b/js/classes/Calendar.js @@ -18,7 +18,7 @@ const { displayWaiverWindow } = require('../workday-waiver-aux.js'); const { computeAllTimeBalanceUntilAsync } = require('../time-balance.js'); -const { generateKey } = require('../date-db-formatter'); +const { generateKey } = require('../date-db-formatter.js'); // Global values for calendar const store = new Store(); diff --git a/js/classes/CalendarFactory.js b/js/classes/CalendarFactory.js index 7ebcb236d..eba3f352a 100644 --- a/js/classes/CalendarFactory.js +++ b/js/classes/CalendarFactory.js @@ -5,6 +5,7 @@ const { getDefaultWidthHeight} = require('../user-preferences.js'); const { Calendar } = require('./Calendar.js'); const { FixedDayCalendar } = require('./FixedDayCalendar.js'); const { FlexibleMonthCalendar } = require('./FlexibleMonthCalendar.js'); +const { FlexibleDayCalendar } = require('./FlexibleDayCalendar.js'); class CalendarFactory { static getInstance(preferences, calendar = undefined) { @@ -38,7 +39,18 @@ class CalendarFactory { throw new Error(`Could not instantiate ${view}`); } else if (numberOfEntries === 'flexible') { - if (view === 'month') { + if (view === 'day') { + if (calendar === undefined || calendar.constructor.name !== 'FlexibleDayCalendar') { + if (calendar !== undefined && calendar.constructor.name !== 'FlexibleDayCalendar') { + ipcRenderer.send('RESIZE_MAIN_WINDOW', widthHeight.width, widthHeight.height); + } + return new FlexibleDayCalendar(preferences); + } else { + calendar.updatePreferences(preferences); + calendar.redraw(); + return calendar; + } + } else if (view === 'month') { if (calendar === undefined || calendar.constructor.name !== 'FlexibleMonthCalendar') { if (calendar !== undefined && calendar.constructor.name !== 'FlexibleMonthCalendar') { ipcRenderer.send('RESIZE_MAIN_WINDOW', widthHeight.width, widthHeight.height); diff --git a/js/classes/FlexibleDayCalendar.js b/js/classes/FlexibleDayCalendar.js new file mode 100644 index 000000000..dc57dd196 --- /dev/null +++ b/js/classes/FlexibleDayCalendar.js @@ -0,0 +1,680 @@ +'use strict'; + +const { ipcRenderer } = require('electron'); +const { + isNegative, + multiplyTime, + subtractTime, + sumTime, + validateTime +} = require('../time-math.js'); +const { getDateStr, getMonthLength } = require('../date-aux.js'); +const { generateKey } = require('../date-db-formatter.js'); +const { showDialog } = require('../window-aux.js'); +const { FlexibleMonthCalendar } = require('./FlexibleMonthCalendar.js'); + +class FlexibleDayCalendar extends FlexibleMonthCalendar { + /** + * @param {Object.} preferences + */ + constructor(preferences) { + super(preferences); + } + + /** + * Initializes the calendar by generating the html code, binding JS events and then drawing according to DB. + */ + _initCalendar() { + this._generateTemplate(); + + $('#next-day').click(() => { this._nextDay(); }); + $('#prev-day').click(() => { this._prevDay(); }); + $('#switch-view').click(() => { this._switchView(); }); + $('#current-day').click(() => { this._goToCurrentDate(); }); + $('#input-calendar-date').change((event) => { + let [year, month, day] = $(event.target).val().split('-'); + this._goToDate(new Date(year, month-1, day)); + }); + + this._draw(); + } + + /** + * Generates the calendar HTML view. + */ + _generateTemplate() { + var body = this._getBody(); + $('#calendar').html(body); + $('html').attr('data-view', 'flexible-day'); + } + + /** + * Returns the header of the page, with the image, name and a message. + * @return {string} + */ + static _getPageHeader() { + let switchView = ''; + let todayBut = ''; + let leftBut = ''; + let rightBut = ''; + return '
'+ + '
' + + '
Time to Leave
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + switchView + '' + leftBut + '
' + rightBut + '' + todayBut + '
'; + } + + /** + * Returns the template code of the body of the page. + * @return {string} + */ + _getBody() { + var html = '
'; + html += this.constructor._getPageHeader(); + html += '
'; + html += '
'; + + return html; + } + + /** + * Returns the summary field HTML code. + * @return {string} + */ + static _getSummaryRowCode() { + let leaveByCode = ''; + let summaryStr = 'You should leave by:'; + let code = '
' + + '
' + summaryStr + '
' + + '
' + leaveByCode + '
' + + '
'; + let finishedSummaryStr = 'All done for today. Balance of the day:'; + let dayBalance = ''; + code += ''; + return code; + } + + /** + * Returns the HTML code for the row with working days, month total and balance. + * @return {string} + */ + static _getBalanceRowCode() { + return '
' + + '
' + + '
' + + '
Month Balance
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
Overall Balance
' + + '
' + + '
' + + '
' + + '
'; + } + + /** + * Returns the code of the table body of the calendar. + * @return {string} + */ + _generateTableBody() { + return this._getInputsRowCode(this._getCalendarYear(), this._getCalendarMonth(), this._getCalendarDate()) + this.constructor._getBalanceRowCode(); + } + + /** + * Returns the code of a calendar row. + * @param {number} year + * @param {number} month + * @param {number} day + * @return {string} + */ + _getInputsRowCode(year, month, day) { + const today = new Date(), + isToday = (today.getDate() === day && today.getMonth() === month && today.getFullYear() === year), + dateKey = generateKey(year, month, day); + + if (!this._showDay(year, month, day)) { + return '
' + + '
Not a working day
' + + '
\n'; + } + + let waivedInfo = this._getWaiverStore(day, month, year); + if (waivedInfo !== undefined) { + let summaryStr = 'Waived day: ' + waivedInfo['reason']; + let waivedLineHtmlCode = + '
' + + '
' + summaryStr + '
' + + '
' + + '
     
' + + '
' + + '
\n'; + return waivedLineHtmlCode; + } + + let htmlCode = + '
' + + '
' + + '
' + + '
Day Total
' + + '
' + + '
' + + '
     
' + + '
' + + '
' + + '
' + + '
' + + '
+
' + + '
' + + '
' + + '
\n'; + + if (isToday) { + htmlCode += this.constructor._getSummaryRowCode(); + } + + return htmlCode; + } + + /** + * Updates the code of the table header of the calendar, to be called on demand. + */ + _updateTableHeader() { + let options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; + let today = this._calendarDate; + $('#header-date').html(today.toLocaleDateString(undefined, options)); + $('#input-calendar-date').val(getDateStr(today)); + } + + /** + * Display next day. + */ + _nextDay() { + this._changeDay(1); + } + + /** + * Display previous day. + */ + _prevDay() { + this._changeDay(-1); + } + + /** + * Go to current day. + */ + _goToCurrentDate() { + this._goToDate(new Date()); + } + + /** + * Returns if Calendar date agrees with parameter date. + * @return {Boolean} + */ + _isCalendarOnDate(date) { + return date.getDate() === this._getCalendarDate() && date.getMonth() === this._getCalendarMonth() && date.getFullYear() === this._getCalendarYear(); + } + + /** + * Go to date. + * @param {Date} date + */ + _goToDate(date) { + this._calendarDate = date; + this.redraw(); + } + + /** + * Change the calendar view by a number of days. + * @param int numDays number of days to be changed (positive/negative) + */ + _changeDay(numDays) { + this._calendarDate.setDate(this._calendarDate.getDate() + numDays); + this.redraw(); + } + + /** + * Draws elements of the Calendar that depend on DB data. + */ + _draw() { + super._draw(); + + if (!this._isCalendarOnDate(new Date())) { + $('#punch-button').prop('disabled', true); + ipcRenderer.send('TOGGLE_TRAY_PUNCH_TIME', false); + } + } + + /** + * Draws +/- buttons for the flexible calendar. Arrows are not needed for day calendar. + */ + _drawArrowsAndButtons() { + const calendar = this; + + function removeEntries() { + const existingEntryPairs = $('.row-entry-pair').length; + if (existingEntryPairs > 2) { + const dateKey = $('.rows-time').attr('id'); + const removeEntriesDialogOptions = { + title: 'Remove entry', + message: 'Are you sure you want to remove the last entry row?', + type: 'info', + buttons: ['Yes', 'No'] + }; + showDialog(removeEntriesDialogOptions, (result) => { + const buttonId = result.response; + if (buttonId === 1) { + return; + } + $('.rows-time > div:last-of-type').remove(); + $('.rows-time > div:last-of-type').remove(); + + if (existingEntryPairs - 1 > 2) { + const minusSignCode = + '
' + + '
-
' + + '
'; + $(minusSignCode).appendTo('.rows-time > div:last-of-type > .third-group'); + $('.sign-cell:has(span.minus-sign)').off('click').on('click', removeEntries); + } + + calendar._updateTimeDay(dateKey); + setTimeout(() => { + calendar._checkTodayPunchButton(); + }, 0); + }); + } + } + + function addEntries() { + const dateKey = $('.rows-time').attr('id'); + $('.sign-cell:has(span.minus-sign)').remove(); + const existingEntryPairs = 2 * $('.row-entry-pair').length; + calendar._addNecessaryEntries(dateKey, existingEntryPairs + 2); + $('.sign-cell:has(span.minus-sign)').off('click').on('click', removeEntries); + $('input[type=\'time\']').off('input propertychange').on('input propertychange', function() { + calendar._updateTimeDayCallback($(this).attr('data-date')); + }); + setTimeout(() => { + calendar._checkTodayPunchButton(); + }, 0); + } + + $('.sign-cell:has(span.plus-sign)').off('click').on('click', addEntries); + $('.sign-cell:has(span.minus-sign)').off('click').on('click', removeEntries); + } + + /** + * Every day change, if the calendar is showing the same date as that of the previous date, + * this function is called to redraw the calendar. + * @param {int} oldDayDate + * @param {int} oldMonthDate + * @param {int} oldYearDate + */ + refreshOnDayChange(oldDayDate, oldMonthDate, oldYearDate) { + let date = new Date(oldYearDate, oldMonthDate, oldDayDate); + if (this._isCalendarOnDate(date)) { + this._goToCurrentDate(); + } + } + + /** + * Gets the total for a specific day by looking into both stores. + * @param {number} year + * @param {number} month + * @param {number} day + * @return {string|undefined} + */ + _getDayTotal(year, month, day) { + const dateKey = generateKey(year, month, day); + const values = this._getStore(dateKey); + if (values !== undefined) { + const validatedTimes = this._validateTimes(values); + const inputsHaveExpectedSize = values.length >= 4 && values.length % 2 === 0; + const validatedTimesOk = validatedTimes.length > 0 && validatedTimes.every(time => time !== '--:--'); + const hasDayEnded = inputsHaveExpectedSize && validatedTimesOk; + + let dayTotal = undefined; + if (hasDayEnded) { + dayTotal = '00:00'; + let timesAreProgressing = true; + if (validatedTimes.length >= 4 && validatedTimes.length % 2 === 0) { + for (let i = 0; i < validatedTimes.length; i += 2) { + const difference = subtractTime(validatedTimes[i], validatedTimes[i + 1]); + dayTotal = sumTime(dayTotal, difference); + if (validatedTimes[i] >= validatedTimes[i + 1]) { + timesAreProgressing = false; + } + } + } + if (!timesAreProgressing) { + return undefined; + } + } + return dayTotal; + } + + const waiverTotal = this._getWaiverStore(day, month, year); + if (waiverTotal !== undefined) { + return waiverTotal['hours']; + } + return undefined; + } + + /** + * Updates the monthly time balance and triggers the all time balance update at end. + */ + _updateBalance() { + let yesterday = new Date(this._calendarDate); + yesterday.setDate(this._calendarDate.getDate() - 1); + let workingDaysToCompute = 0, + monthTotalWorked = '00:00'; + let countDays = false; + + let limit = this._getCountToday() ? this._getCalendarDate() : (yesterday.getMonth() !== this._getCalendarMonth() ? 0 : yesterday.getDate()); + for (let day = 1; day <= limit; ++day) { + if (!this._showDay(this._getCalendarYear(), this._getCalendarMonth(), day)) { + continue; + } + + let dayTotal = this._getDayTotal(this._getCalendarYear(), this._getCalendarMonth(), day); + if (dayTotal !== undefined) { + countDays = true; + monthTotalWorked = sumTime(monthTotalWorked, dayTotal); + } + if (countDays) { + workingDaysToCompute += 1; + } + } + let monthTotalToWork = multiplyTime(this._getHoursPerDay(), workingDaysToCompute * -1); + let balance = sumTime(monthTotalToWork, monthTotalWorked); + let balanceElement = $('#month-balance'); + if (balanceElement) + { + balanceElement.html(balance); + balanceElement.removeClass('text-success text-danger'); + balanceElement.addClass(isNegative(balance) ? 'text-danger' : 'text-success'); + } + } + + /** + * Update contents of the "time to leave" bar. + */ + _updateLeaveBy() { + if (!this._showDay(this._getTodayYear(), this._getTodayMonth(), this._getTodayDate()) || + this._getTodayMonth() !== this._getCalendarMonth() || + this._getTodayYear() !== this._getCalendarYear() || + this._getWaiverStore(this._getTodayDate(), this._getCalendarMonth(), this._getCalendarYear())) { + return; + } + + const leaveBy = this._calculateLeaveBy(); + $('#leave-by').val(leaveBy <= '23:59' ? leaveBy : '--:--'); + + this._checkTodayPunchButton(); + + const dayTotal = $('.day-total span').html(); + if (dayTotal !== undefined && dayTotal.length > 0) { + const dayBalance = subtractTime(this._getHoursPerDay(), dayTotal); + $('#leave-day-balance').val(dayBalance); + $('#leave-day-balance').removeClass('text-success text-danger'); + $('#leave-day-balance').addClass(isNegative(dayBalance) ? 'text-danger' : 'text-success'); + $('#summary-unfinished-day').addClass('hidden'); + $('#summary-finished-day').removeClass('hidden'); + } else { + $('#summary-unfinished-day').removeClass('hidden'); + $('#summary-finished-day').addClass('hidden'); + } + } + + /** + * Updates data displayed on the calendar based on the internal DB, and updates balances at end. + */ + _updateBasedOnDB() { + let monthLength = getMonthLength(this._getCalendarYear(), this._getCalendarMonth()); + let workingDays = 0; + let stopCountingMonthStats = false; + for (let day = 1; day <= monthLength; ++day) { + + if (stopCountingMonthStats) { + break; + } + + stopCountingMonthStats |= (this._getCalendarDate() === day); + + if (!this._showDay(this._getCalendarYear(), this._getCalendarMonth(), day)) { + continue; + } + + const dateKey = generateKey(this._getCalendarYear(), this._getCalendarMonth(), day); + if (day === this._getCalendarDate()) { + let waivedInfo = this._getWaiverStore(day, this._getCalendarMonth(), this._getCalendarYear()); + if (waivedInfo !== undefined) { + let waivedDayTotal = waivedInfo['hours']; + $('#' + dateKey + ' .day-total').html(waivedDayTotal); + } else { + this._setTableData(dateKey); + this._checkInputErrors(); + } + } + + workingDays += 1; + } + let monthDayInput = $('#month-day-input'); + if (monthDayInput) + { + monthDayInput.val(this._getBalanceRowPosition()); + } + let monthWorkingDays = $('#month-working-days'); + if (monthWorkingDays) + { + monthWorkingDays.val(workingDays); + } + this._updateBalance(); + + this._updateLeaveBy(); + } + + /** + * Updates the DB with the information of computed total lunch time and day time. + * @param {string} dateKey + */ + _updateTimeDay(dateKey) { + // Cleaning intervals + $('#' + dateKey + ' div.interval').html('     '); + + const inputs = $('#' + dateKey + ' .row-entry-pair input[type=\'time\']'); + let newValues = []; + for (const element of inputs) { + newValues.push(element.value); + } + + this._updateDayIntervals(dateKey); + this._updateDbEntry(dateKey, newValues); + this._updateDayTotal(dateKey); + this._checkInputErrors(); + } + + /** + * Updates the intervals shown on the calendar + * @param {string} dateKey + */ + _updateDayIntervals(dateKey) { + const inputs = $('#' + dateKey + ' input[type="time"]'); + let i = 0; + let timeStart = ''; + let timeEnd = ''; + for (const element of inputs) { + if (i !== 0 && (i + 1) % 2 === 1) { + timeEnd = element.value; + + if (validateTime(timeEnd) && validateTime(timeStart)) { + if (timeEnd > timeStart) { + $(element).closest('.row-entry-pair').prev().find('div.interval').html(subtractTime(timeStart, timeEnd)); + } + timeStart = ''; + timeEnd = ''; + } + } + else if ((i + 1) % 2 === 0) { + timeStart = element.value; + } + i++; + } + } + + /** + * Analyze the inputs of a day, and color entry lines if they have errors. + * An error means that an input earlier in the day is higher than one that is after it. + */ + _checkInputErrors() { + function colorErrorLine(entryRow, validated) { + $(entryRow).toggleClass('error-tr', !validated); + } + + // Checking errors on each row + const entryRows = $('.row-entry-pair'); + entryRows.each((index, entryRow) => { + const inputs = $(entryRow).find('input[type=\'time\']'); + let newValues = []; + for (const element of inputs) { + newValues.push(element.value); + } + + const validatedTimes = this._validateTimes(newValues, true /*removeEndingInvalids*/); + + const noInputsYet = validatedTimes.length === 0; + if (noInputsYet) { + colorErrorLine(entryRow, true /*validated*/); + return; + } + + const invalidInputs = validatedTimes.some(time => time === '--:--'); + if (invalidInputs) { + colorErrorLine(entryRow, false /*validated*/); + return; + } + + colorErrorLine(entryRow, true /*validated*/); + }); + + // Checking errors across rows + const allInputs = $('input[type=\'time\']'); + let newValues = []; + for (const element of allInputs) { + newValues.push(element.value); + } + const validatedTimes = this._validateTimes(newValues, true /*removeEndingInvalids*/); + for (let index = 0; index < validatedTimes.length; index++) { + if (index > 0 && (validatedTimes[index - 1] >= validatedTimes[index])) { + const entryRowIndex = Math.floor(index/2); + colorErrorLine(entryRows[entryRowIndex], false /*validated*/); + return; + } + } + } + + /** + * Appends the html elements for the day. + * The table consists of interleaved rows of entry pairs and intervals. + * @param {string} dateKey + * @param {number} entrySize + */ + _addNecessaryEntries(dateKey, entrySize) { + // 2 pairs is the default minimum size of the table + const numberOfPairs = Math.ceil(entrySize/2) >= 2 ? Math.ceil(entrySize/2) : 2; + + function entryPairHTMLCode(entryIndex, isLastRow) { + + const minusSignCode = + '
' + + '
' + + '
-
' + + '
' + + '
'; + const shouldPrintMinusSign = numberOfPairs > 2 && isLastRow; + + return '
' + + '
Entry #' + entryIndex + '
' + + '
' + + '' + + '' + + '
' + + (shouldPrintMinusSign ? minusSignCode : '') + + '
'; + } + + function intervalHTMLCode(entryIndex) { + + if (entryIndex === 0) { + return ''; + } + return '
' + + '
' + + '
' + + '
     
' + + '
' + + '
'; + } + + const existingEntryPairs = $('.row-entry-pair').length; + for (let i = existingEntryPairs; i < numberOfPairs; i++) { + $(intervalHTMLCode(i)).appendTo('#' + dateKey); + const isLastRow = i === (numberOfPairs - 1); + $(entryPairHTMLCode(i + 1, isLastRow)).appendTo('#' + dateKey); + } + } + + /** + * Updates data displayed based on the database. + * @param {string} dateKey + */ + _setTableData(dateKey) { + const values = this._getStore(dateKey); + this._addNecessaryEntries(dateKey, values.length); + + const inputs = $('#' + dateKey + ' input[type="time"]'); + let i = 0; + + for (const element of values) { + let input = inputs[i]; + if (input !== undefined) { + $(input).val(element); + } + i++; + } + + this._updateDayIntervals(dateKey); + this._updateDayTotal(dateKey); + } + + /** + * Returns a date object for which the all time balance will be calculated. + * For DayCalendar, it's the day of CalendarDate => the day being displayed. + * If "count_today" is active, the following day. + * @return {Date} + */ + _getTargetDayForAllTimeBalance() { + let targetDate = new Date(this._getCalendarYear(), this._getCalendarMonth(), this._getCalendarDate()); + if (this._getCountToday()) { + targetDate.setDate(targetDate.getDate() + 1); + } + return targetDate; + } +} + +module.exports = { + FlexibleDayCalendar +}; diff --git a/js/classes/FlexibleMonthCalendar.js b/js/classes/FlexibleMonthCalendar.js index 56c36be25..98739138e 100644 --- a/js/classes/FlexibleMonthCalendar.js +++ b/js/classes/FlexibleMonthCalendar.js @@ -10,6 +10,7 @@ const { validateTime } = require('../time-math.js'); const { getMonthLength } = require('../date-aux.js'); +const { generateKey } = require('../date-db-formatter.js'); const { formatDayId, sendWaiverDay, @@ -17,7 +18,6 @@ const { } = require('../workday-waiver-aux.js'); const { showDialog } = require('../window-aux.js'); const { Calendar } = require('./Calendar.js'); -const { generateKey } = require('../date-db-formatter'); // Global values for calendar const flexibleStore = new Store({name: 'flexible-store'}); @@ -55,6 +55,7 @@ class FlexibleMonthCalendar extends Calendar { * Returns the header of the page, with the image, name and a message. */ static _getPageHeader() { + let switchView = ''; let todayBut = ''; let leftBut = ''; let rightBut = ''; @@ -64,6 +65,7 @@ class FlexibleMonthCalendar extends Calendar { '
' + '
' + '' + + '' + '' + '' + '' + @@ -202,7 +204,7 @@ class FlexibleMonthCalendar extends Calendar { this._updateLeaveBy(); - let calendar = this; + const calendar = this; $('input[type=\'time\']').off('input propertychange').on('input propertychange', function() { calendar._updateTimeDayCallback($(this).attr('data-date')); }); @@ -214,8 +216,16 @@ class FlexibleMonthCalendar extends Calendar { displayWaiverWindow(); }); + this._drawArrowsAndButtons(); + this._updateAllTimeBalance(); + } + /* + * Draws the arrows and +/- buttons for the flexible calendar. + */ + _drawArrowsAndButtons() { + const calendar = this; let slideTimer; function sideScroll(element, direction, speed, step) { slideTimer = setInterval(function() { @@ -375,8 +385,8 @@ class FlexibleMonthCalendar extends Calendar { continue; } - let key = this._getCalendarYear() + '-' + this._getCalendarMonth() + '-' + day; - let dayTotal = $('#' + key).parent().find('.day-total span').html(); + const dateKey = generateKey(this._getCalendarYear(), this._getCalendarMonth(), day); + let dayTotal = $('#' + dateKey).parent().find('.day-total span').html(); if (dayTotal !== undefined && dayTotal.length !== 0) { countDays = true; monthTotalWorked = sumTime(monthTotalWorked, dayTotal); @@ -411,16 +421,16 @@ class FlexibleMonthCalendar extends Calendar { } let dayTotal = null; - let key = this._getCalendarYear() + '-' + this._getCalendarMonth() + '-' + day; + const dateKey = generateKey(this._getCalendarYear(), this._getCalendarMonth(), day); let waivedInfo = this._getWaiverStore(day, this._getCalendarMonth(), this._getCalendarYear()); if (waivedInfo !== undefined) { let waivedDayTotal = waivedInfo['hours']; - $('#' + key + ' .day-total').html(waivedDayTotal); + $('#' + dateKey + ' .day-total').html(waivedDayTotal); dayTotal = waivedDayTotal; } else { - this._setTableData(key); - this._colorErrorLine(key); + this._setTableData(dateKey); + this._colorErrorLine(dateKey); } stopCountingMonthStats |= (this._getTodayDate() === day && this._getTodayMonth() === this._getCalendarMonth() && this._getTodayYear() === this._getCalendarYear()); @@ -458,11 +468,34 @@ class FlexibleMonthCalendar extends Calendar { return; } - let key = this._getTodayYear() + '-' + this._getTodayMonth() + '-' + this._getTodayDate(); - const values = this._getStore(key); - let validatedTimes = this._validateTimes(values, true /*removeEndingInvalids*/); + const leaveBy = this._calculateLeaveBy(); + $('#leave-by').html(leaveBy <= '23:59' ? leaveBy : '--:--'); + + this._checkTodayPunchButton(); + + const dateKey = generateKey(this._getTodayYear(), this._getTodayMonth(), this._getTodayDate()); + const dayTotal = $('#' + dateKey).parent().find(' .day-total span').html(); + if (dayTotal !== undefined && dayTotal.length > 0) { + const dayBalance = subtractTime(this._getHoursPerDay(), dayTotal); + $('#leave-day-balance').html(dayBalance); + $('#leave-day-balance').removeClass('text-success text-danger'); + $('#leave-day-balance').addClass(isNegative(dayBalance) ? 'text-danger' : 'text-success'); + $('#summary-unfinished-day').addClass('hidden'); + $('#summary-finished-day').removeClass('hidden'); + } else { + $('#summary-unfinished-day').removeClass('hidden'); + $('#summary-finished-day').addClass('hidden'); + } + } + /** + * Calculate the time to leave for today for use in _updateLeaveBy(). + */ + _calculateLeaveBy() { let leaveBy = '--:--'; + const dateKey = generateKey(this._getTodayYear(), this._getTodayMonth(), this._getTodayDate()); + const values = this._getStore(dateKey); + const validatedTimes = this._validateTimes(values, true /*removeEndingInvalids*/); if (validatedTimes.length > 0 && validatedTimes.every(time => time !== '--:--')) { const smallestMultipleOfTwo = Math.floor(validatedTimes.length/2)*2; let dayTotal = '00:00'; @@ -480,22 +513,7 @@ class FlexibleMonthCalendar extends Calendar { leaveBy = sumTime(lastTime, remainingTime); } } - $('#leave-by').html(leaveBy <= '23:59' ? leaveBy : '--:--'); - - this._checkTodayPunchButton(); - - const dayTotal = $('#' + key).parent().find(' .day-total span').html(); - if (dayTotal !== undefined && dayTotal.length > 0) { - const dayBalance = subtractTime(this._getHoursPerDay(), dayTotal); - $('#leave-day-balance').html(dayBalance); - $('#leave-day-balance').removeClass('text-success text-danger'); - $('#leave-day-balance').addClass(isNegative(dayBalance) ? 'text-danger' : 'text-success'); - $('#summary-unfinished-day').addClass('hidden'); - $('#summary-finished-day').removeClass('hidden'); - } else { - $('#summary-unfinished-day').removeClass('hidden'); - $('#summary-finished-day').addClass('hidden'); - } + return leaveBy; } /* @@ -506,7 +524,7 @@ class FlexibleMonthCalendar extends Calendar { const isCurrentMonth = (today.getMonth() === this._calendarDate.getMonth() && today.getFullYear() === this._calendarDate.getFullYear()); let enableButton = false; if (isCurrentMonth) { - const dateKey = today.getFullYear() + '-' + today.getMonth() + '-' + today.getDate(); + const dateKey = generateKey(today.getFullYear(), today.getMonth(), today.getDate()); const inputs = $('#' + dateKey + ' input[type="time"]'); let allInputsFilled = true; for (let input of inputs) { diff --git a/js/date-db-formatter.js b/js/date-db-formatter.js index 541814355..07afba15c 100644 --- a/js/date-db-formatter.js +++ b/js/date-db-formatter.js @@ -1,6 +1,15 @@ -function generateKey(year, month, day, key) { - const dbKey = year + '-' + month + '-' + day; - return key === undefined ? dbKey : dbKey + '-' + key; +'use strict'; + +/** + * Formats year, month and day (and maybe key) for the key format of the DBs + * @param {number} year value representing a year in 4 digits YYYY + * @param {number} month value representing a month starting with 0 (0-11) + * @param {number} day value representing a day (1-31) + * @param {String|undefined} key Fixed calendar requires key's for the db entries (day-total, lunch-begin, etc) + * @return {String} + */ +function generateKey(year, month, day, key = undefined) { + return year + '-' + month + '-' + day + (key === undefined ? '' : ('-' + key)); } module.exports = { diff --git a/js/user-preferences.js b/js/user-preferences.js index ec25601f1..7ed8ec015 100644 --- a/js/user-preferences.js +++ b/js/user-preferences.js @@ -136,12 +136,7 @@ function initPreferencesFileIfNotExistsOrInvalid() { shouldSaveDerivedPrefs |= !isValidTheme(value); break; case 'view': - if (derivedPrefs['number-of-entries'] === 'flexible') { // flexible only working with month calendar yet - shouldSaveDerivedPrefs |= !(value === 'month'); - } - else { - shouldSaveDerivedPrefs |= !(value === 'month' || value === 'day'); - } + shouldSaveDerivedPrefs |= !(value === 'month' || value === 'day'); break; case 'number-of-entries': shouldSaveDerivedPrefs |= !(value === 'fixed' || value === 'flexible'); diff --git a/src/preferences.js b/src/preferences.js index 868383210..4437bc697 100644 --- a/src/preferences.js +++ b/src/preferences.js @@ -10,15 +10,6 @@ const { bindDevToolsShortcut } = require('../js/window-aux.js'); let usersStyles = getUserPreferences(); let preferences = usersStyles; -function limitCalendarViews(preferences) { - const isFlexibleCalendar = preferences['number-of-entries'] === 'flexible'; - $('#view option[value="day"]').prop('disabled', isFlexibleCalendar); - if (isFlexibleCalendar) { - $('#view').val('month'); - preferences['view'] = 'month'; - } -} - $(() => { // Theme-handling should be towards the top. Applies theme early so it's more natural. let theme = 'theme'; @@ -67,7 +58,6 @@ $(() => { $('#number-of-entries').change(function() { preferences['number-of-entries'] = this.value; - limitCalendarViews(preferences); ipcRenderer.send('PREFERENCE_SAVE_DATA_NEEDED', preferences); }); @@ -87,8 +77,6 @@ $(() => { } }); - limitCalendarViews(preferences); - const notification = $('#notification'); const repetition = $('#repetition'); const notificationsInterval = $('#notifications-interval');
' + switchView + '' + leftBut + '
' + rightBut + '