How to calculate my work hours

-–
name: Daily-20230220-Mon
-–

  • 9:30 am #starttime
    • log 1
    • log 2
  • 12:30 pm #endtime
  • break
  • 1:30 pm #starttime
    • log 3
    • log 4
  • 7:00 pm #endtime
  • break
  • 10:00 pm #starttime
    • log 5
    • log 6
  • 1:00 am #starttime

How can I use dataview to calculate the hours work for the day.

The hours that bleed into the next day should also count.

1 Like

Any recommendation on changing the tags, structure to make the query easier.

Here’s what I do, maybe there’s something useful here.

I track my time in a section in my daily note, in this format. I use Templater to insert the timestamps for me:

image

## 🕔 Time tracking
- 07:30-10:56 Some stuff
- 10:56-11:23 Another task, did some things
- 12:34-13:30 Third project

Then I have a master time tracking note which sums them all up for me using Dataview, in hours-per-week/day:

const tracked = {}
dv.pages('"Daily/Notes"').file.lists
  .where(x => x.section.subpath === "🕔 Time tracking").array()
  .forEach(x => {
    // Find the start/end times for each bullet point
    const times = x.text.match(/^(\d{2}:\d{2})-(\d{2}:\d{2})/)
    if (times) {
      const start = moment(times[1], 'HH:mm')
      const end = moment(times[2], 'HH:mm')
      const minutes = moment.duration(end.diff(start)).asMinutes()
      const date = x.path.match(/(\d{4}-\d{2}-\d{2})/)[1]
      const week = moment(date).format('YYYY, [Week] WW')
      if (!tracked[week]) tracked[week] = {}
      if (tracked[week][date]) {
        tracked[week][date].minutes += minutes
      } else {
        tracked[week][date] = {
          path: x.path,
          minutes: minutes
        }
      }
    }
  })

const hours = minutes => (minutes / 60).toFixed(1)

const table = []
Object.keys(tracked).sort((a, b) => b.localeCompare(a))
  .forEach(weekDate => {
    // Push weekly value
    const week = tracked[weekDate]
    const weekTime = Object.values(week).reduce((prev, curr) => prev + curr.minutes, 0)
    table.push([weekDate, '**' + hours(weekTime) + '**'])

    // Push daily values
    Object.keys(week).sort((a, b) => b.localeCompare(a))
      .forEach(date => {
        const link = `– [[${week[date].path}#🕔 Time tracking|${moment(date).format('dddd D MMMM')}]]`
        table.push([link, '– ' + hours(week[date].minutes)])
      })
  })

dv.table(['Date', 'Hours'], table)

The summary table looks like this, with links back to each day:

1 Like

In your code, how would you change the code to use the current date and only display the days for the week?

How does the template to insert the timestamps look like?

In my case the time entries are in my daily Periodic Notes journals. The script looks like this:

    // Open the latest journal file
    const latest = this.journal[0]
    await this.goToFile(latest.file.path)
    // Find the end of the time tracking section
    const contents = await this.getContents(latest.file.path)
    const match = contents.match(/^.*?\n## 🕔 Time tracking.*?\n---/s)
    if (match) {
      const lines = match[0].split('\n').slice(0, -2)
      // Move cursor to end of time tracking section
      await this.setEditMode(true)
      await this.sleep(20)
      this.view.editor.focus()
      this.view.editor.setCursor({ line: lines.length - 1, ch: lines[lines.length - 1].length })
      await this.sleep(50)
      return moment().format('HH:mm') + '-'
    }
    return ''

The template part is really just:

moment().format('HH:mm') + '-'
1 Like

It’s fascinating to see the different methods to access the files, and I’ve not seen a script actually switching modes and editing at this level before. It’s very educational for me. .

Is this part of a bigger setup since you also seem to have a part ending the previous section, and starting a new with the same time? Or is that different templates/commands?

1 Like

Yes, this is referencing a couple of other files:

My journal class:

class main {
  constructor() {
    const tp = app.plugins.plugins['templater-obsidian'].templater.current_functions_object
    tp.user._boilerplate(this)
    // Journal is sorted by date descending, so most recent is index 0
    this.journal = this.dv.pages('"Journal/Daily"').sort(x => x.file.path, 'desc').array()
  }

  async changeIndex(amount) {
    let index = this.journal.findIndex(x => x.file.path === this.file.path)
    index = Math.max(0, Math.min(index + amount, this.journal.length - 1))
    await this.goToFile(this.journal[index].file.path)
  }

  async randomEntry() {
    const random = this.journal[Math.floor(Math.random() * this.journal.length)]
    await this.goToFile(random.file.path)
    await this.setEditMode(false) // change to Preview mode
    return ''
  }

  async goToLatestEntry() {
    await this.goToFile(this.journal[0].file.path)
    return ''
  }

  async previousEntry() {
    await this.changeIndex(1)
    return ''
  }

  async nextEntry() {
    await this.changeIndex(-1)
    return ''
  }
}
module.exports = main

And that file is referencing this general functions class:

function main(target) {
  const dv = app.plugins.plugins.dataview.api
  const tp = app.plugins.plugins['templater-obsidian'].templater.current_functions_object

  target.app = app
  target.dv = dv
  target.tp = tp
  target.page = dv.page(tp.file.path(true))
  target.file = target.page.file
  target.view = app.workspace.activeLeaf.view

  target.sleep = async function (ms) {
    await new Promise(resolve => setTimeout(resolve, ms))
  }

  /**
   * Create a new file from a template, and return the link or open the file
   * @param {string} templatePath 
   * @param {string} newNoteName 
   * @param {string} destinationFolder - Path to the destination folder. Defaults to current folder
   * @param {boolean} openNewNote - Open the note in a new window, or return a link
   * @returns 
   */
  target.createFromTemplate = async function (templatePath, newNoteName, destinationFolder, openNewNote) {
    destinationFolder = destinationFolder || tp.file.folder(true)
    await tp.file.create_new(tp.file.find_tfile(templatePath), newNoteName, openNewNote, app.vault.getAbstractFileByPath(destinationFolder))
    return openNewNote ? '' : `[[${newNoteName}]] `
  }

  target.setEditMode = function (canEdit) {
    const curr = app.workspace.activeLeaf.getViewState()
    curr.state.mode = canEdit ? 'source' : 'preview'
    app.workspace.activeLeaf.setViewState(curr)
    if (canEdit) {
      target.view.editor?.focus()
    }
  }

  /**
   * Get the text contents of a file, specified by string path
   * @param {string} path
   */
  target.getContents = async function (path) {
    const file = app.vault.getAbstractFileByPath(path)
    return app.vault.read(file)
  }

  target.goToFile = async function (path) {
    const file = app.vault.getAbstractFileByPath(path)
    if (path !== target.file.path) {
      await app.workspace.getLeaf(false).openFile(file)
    }
  }
}
module.exports = main

First, Thank you AlanG.

I made the following changes to only display the current week and handle roll-over time into the next day. (For those that work late hours)

const tracked = {}
dv.pages('"daily/2023"').file.lists
  .where(x => x.section.subpath === "Worklog").array()
  .forEach(x => {
    // /(\d{4}\d{2}\d{2})/ - Works too
    const date = x.path.match(/(\d{8})/)[1]
    const week = moment(date).format('YYYY, [Week] WW')
	if (week == moment().format('YYYY, [Week] WW')) {

        // Find the start/end times for each bullet point
        const times = x.text.match(/^(\d{2}:\d{2}) - (\d{2}:\d{2})/)
        if (times) {
            const start = moment(date + times[1], 'YYYYMMDDHH:mm')
            var end = moment(date + times[2], 'YYYYMMDDHH:mm')
            if ( start > end) {
               end = moment(date + times[2], 'YYYYMMDDHH:mm').add(1, 'days')
            } 
            
	        const minutes = moment.duration(end.diff(start)).asMinutes()

	        // Setup the week
            if (!tracked[week]) tracked[week] = {}

	        // Add up the date
            if (tracked[week][date]) {
	            tracked[week][date].minutes += minutes
	        } else {
		        tracked[week][date] = {
		            path: x.path,
		            minutes: minutes
		        }
		    }
	    }
	}
})

const hours = minutes => (minutes / 60).toFixed(1)

const table = []
Object.keys(tracked).sort((a, b) => b.localeCompare(a))
  .forEach(weekDate => {
    // Push weekly value
    const week = tracked[weekDate]
    const weekTime = Object.values(week).reduce((prev, curr) => prev + curr.minutes, 0)
    table.push([weekDate, '**' + hours(weekTime) + '**'])

    // Push daily values
    Object.keys(week).sort((a, b) => b.localeCompare(a))
      .forEach(date => {
        const link = `– [[${week[date].path}#Worklog|${moment(date).format('dddd, D MMMM')}]]`
        table.push([link, '– ' + hours(week[date].minutes)])
      })
  })

dv.table(['Date', 'Hours'], table)
1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.