├── .gitignore
├── scores.gif
├── filtering.gif
├── logbook.png
├── settings.ndtl
├── scripts
├── logbook.js
├── lib
│ ├── tablatal.js
│ ├── indental.js
│ └── helpers.js
├── interface
│ ├── summary.js
│ ├── balance.js
│ ├── scores.js
│ └── interface.js
├── day.js
└── database.js
├── LICENSE
├── README.md
├── index.html
├── CODE_OF_CONDUCT.md
├── links
└── main.css
└── logbook.tbtl
/.gitignore:
--------------------------------------------------------------------------------
1 | database/
2 | .DS*
3 | *.plist
--------------------------------------------------------------------------------
/scores.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rostiger/logbook/HEAD/scores.gif
--------------------------------------------------------------------------------
/filtering.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rostiger/logbook/HEAD/filtering.gif
--------------------------------------------------------------------------------
/logbook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rostiger/logbook/HEAD/logbook.png
--------------------------------------------------------------------------------
/settings.ndtl:
--------------------------------------------------------------------------------
1 | DATABASE.settings = `
2 | CATEGORIES
3 | WORK : #FF68A3
4 | LIFE : #EBD33F
5 | SLEEP : #42C2EE
6 | PROJECTS
7 | Company
8 | CAT : WORK
9 | NAME : Company
10 | TimeTracker
11 | CAT : WORK
12 | NAME : Time Tracker
13 | Website
14 | CAT : WORK
15 | NAME : Website
16 | Household
17 | CAT : LIFE
18 | NAME : Household
19 | Cooking
20 | CAT : LIFE
21 | NAME : Cooking
22 | Personal
23 | CAT : LIFE
24 | NAME : Personal
25 | Family
26 | CAT : LIFE
27 | NAME : Family
28 | SLEEP
29 | CAT : SLEEP
30 | NAME : Sleep
31 | SCORES
32 | BODY : #FF68A3
33 | MIND : #EBD33F
34 | MOOD : #42C2EE
35 | `
--------------------------------------------------------------------------------
/scripts/logbook.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const database = {}
3 | const dates = { from : 0, to : 0 }
4 | const page = { url : '' }
5 |
6 | function Logbook () {
7 | this.errors = []
8 | this.database = new Database()
9 | this.interface = new Interface()
10 |
11 | this.install = function (host) {
12 | this.database.install()
13 | this.interface.install(host)
14 | }
15 |
16 | this.start = function () {
17 | // stop at errors
18 | if (this.errors.length > 0) this.interface.update()
19 | else {
20 | this.database.start()
21 | this.update()
22 | }
23 | }
24 |
25 | this.update = function (updateDB = true, input = window.location.hash) {
26 | page.url = input.toUrl().toUpperCase()
27 | setTimeout(() => { window.scrollTo(0, 0) }, 250)
28 | if (updateDB) this.database.update(dates)
29 | this.interface.update()
30 | }
31 | }
--------------------------------------------------------------------------------
/scripts/lib/tablatal.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function tablatal (data, Type) {
4 | function makeKey (segs) {
5 | segs.pop()
6 | const keys = {}
7 | let counter = 0
8 | for (const id in segs) {
9 | const key = segs[id].trim().toLowerCase()
10 | const len = id < segs.length - 1 ? segs[id].length : -1
11 | keys[key] = [counter, len - 1]
12 | counter += len
13 | }
14 | return keys
15 | }
16 | const a = []
17 | const lines = data.trim().split('\n')
18 | const key = makeKey(lines.shift().match(/(\w*\W*)/g))
19 | for (const id in lines) {
20 | if (lines[id].trim() === '') { continue }
21 | if (lines[id].substr(0, 1) === ';') { continue }
22 | const entry = {}
23 | for (const i in key) {
24 | entry[i] = lines[id].substr(key[i][0], key[i][1] < 0 ? lines[id].length : key[i][1]).trim()
25 | }
26 | a.push(Type ? new Type(entry) : entry)
27 | }
28 | return a
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Clemens Scott
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Logbook
2 | [Logbook](https://rostiger.github.io/logbook/) is a personal tracking tool I use to document and analyse how I spend my time.
3 |
4 | **Logbook** is a subset of **Anchors**, an ecosystem that hosts an array of personal tools, such as [Chronicle](https://github.com/Rostiger/chronicle) and Lexicon.
5 |
6 | Anchors was born from the desire to better know and understand myself and gather data on where I come from, where I am and where I am going. It is an attempt to store and visualize this data in a single place where it is easily accessible and maintainable.
7 |
8 | Logbook is inspired by members of the [Merveilles](https://merveilles.town) community who create and use their own time tracking tools.
9 |
10 | # Screenshots
11 |
12 |
13 | # Usage
14 | Logbook creates an interface from text based data using [neauoire](https://github.com/neauoire)'s _Tablatal_ and _Indental_ formats and parsers.
15 |
16 | More description coming soon!
17 |
18 | # Extras
19 | - See the [License](https://github.com/Rostiger/logbook/blob/master/LICENSE) file for license rights and limitations (MIT).
20 | - Live Version: (https://rostiger.github.io/logbook/)
21 | - Pull Requests are welcome!
22 |
--------------------------------------------------------------------------------
/scripts/interface/summary.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function Summary (host) {
4 | const total = document.createElement('section')
5 | total.id = 'total'
6 | host.appendChild(total)
7 |
8 | this.update = function (categories) {
9 |
10 | const bars = document.createElement('figure')
11 | bars.id = 'bars'
12 | total.appendChild(bars)
13 |
14 | const legend = document.createElement('figure')
15 | legend.id = 'legend'
16 | total.appendChild(legend)
17 |
18 | for (const c in database.categories) {
19 | const cat = categories[c]
20 | if (!cat) continue
21 | const color = database.categories[c].COLOR
22 | const width = cat.percentage === 0 ? 1 : cat.percentage
23 | const display = cat.percentage === 0 ? "none" : "block"
24 | bars.innerHTML +=
25 | `
26 |
${cat.percentage}%
27 |
28 |
29 |
30 |
`
31 | legend.innerHTML +=
32 | `
33 |
34 |
35 |
36 |
37 |
38 |
${c}
39 |
`
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/scripts/lib/indental.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function indental (data, Type) {
4 | function formatLine (line) {
5 | const a = []
6 | const h = {}
7 | for (const child of line.children) {
8 | if (child.key) {
9 | if (h[child.key]) { console.warn(`Redefined key: ${child.key}.`) }
10 | h[child.key] = child.value
11 | } else if (child.children.length === 0 && child.content) {
12 | a[a.length] = child.content
13 | } else {
14 | h[child.content.toUpperCase()] = formatLine(child)
15 | }
16 | }
17 | return a.length > 0 ? a : h
18 | }
19 |
20 | function makeLine (line) {
21 | return line.indexOf(' : ') > -1 ? {
22 | indent: line.search(/\S|$/),
23 | content: line.trim(),
24 | key: line.split(' : ')[0].trim().toUpperCase(),
25 | value: line.split(' : ')[1].trim()
26 | } : {
27 | indent: line.search(/\S|$/),
28 | content: line.trim(),
29 | children: []
30 | }
31 | }
32 |
33 | function skipLine (line) {
34 | return line.trim() !== '' && line.trim().substr(0, 1) !== ';' && line.trim().slice(-1) !== '`'
35 | }
36 |
37 | const lines = data.split('\n').filter(skipLine).map(makeLine)
38 |
39 | // Assoc
40 | const stack = {}
41 | for (const line of lines) {
42 | const target = stack[line.indent - 2]
43 | if (target) { target.children.push(line) }
44 | stack[line.indent] = line
45 | }
46 |
47 | // Format
48 | const h = {}
49 | for (const id in lines) {
50 | const line = lines[id]
51 | if (line.indent > 0) { continue }
52 | const key = line.content.toUpperCase()
53 | if (h[key]) { console.warn(`Redefined key: ${key}, line ${id}.`) }
54 | h[key] = Type ? new Type(key, formatLine(line)) : formatLine(line)
55 | }
56 | return h
57 | }
58 |
59 | // module.exports = indental
60 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | Logbook
8 |
9 |
10 |
11 |
12 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at aliceffekt@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/scripts/interface/balance.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function Balance () {
4 | const balance = document.createElement('section')
5 | balance.id = 'balance'
6 | const header = document.createElement('header')
7 | const bars = document.createElement('section')
8 | bars.id = 'bars'
9 | const footer = document.createElement('footer')
10 |
11 | const dailyDetails = document.createElement('section')
12 | dailyDetails.id = 'dailyDetails'
13 |
14 | this.install = function (host) {
15 | balance.appendChild(header)
16 | balance.appendChild(bars)
17 | balance.appendChild(footer)
18 | host.appendChild(balance)
19 | }
20 |
21 | this.update = function (project = null) {
22 | this.createBars(project)
23 | }
24 |
25 | this.createBars = function (project) {
26 | const today = document.createElement('span')
27 | today.id = 'today'
28 |
29 | header.innerHTML =
30 | `Logs `
31 |
32 | // first & last days date
33 | const dur = database.stats.filter.days
34 | const to = toTimeStamp(database.stats.filter.lastEntry)
35 | let from = to
36 | const width = 100 / dur
37 |
38 | for (let i = dur; i > 0 ; i--) {
39 | const bar = document.createElement('a')
40 | const day = new Date(to)
41 | day.setDate(to.getDate() - i + 1)
42 | const date = convertDateToDB(day)
43 | if (i == dur) from = date
44 |
45 | bar.href = `#${date}`
46 | bar.id = date
47 | bar.style = `width: ${width}%;`
48 | bar.title = date
49 | bars.appendChild(bar)
50 |
51 | // assemble daily overview
52 | const title = {}
53 | const cats = database.categories
54 | if (database.days[date] && i <= dur) {
55 | if (project) {
56 | //get project
57 | if (!database.days[date].projects[project]) continue
58 | const day = database.days[date]
59 | const cat = database.projects[project].CAT
60 | const hours = day.projects[project].hours
61 | const percent = parseFloat((hours * 100 / 24).toFixed(1))
62 | const color = database.categories[cat].COLOR
63 | bar.innerHTML = this.addSegment(percent, color)
64 | bar.title += ` / ${dayNames[toTimeStamp(date).getDay()]} / ${database.days[date].projects[project].hours}h / ${database.days[date].projects[project].count}e`
65 | } else {
66 | // get categories
67 | for (const c in cats) {
68 | const cat = database.days[date].categories[c]
69 | if (cat && cat.percentage > 0) {
70 | bar.innerHTML += this.addSegment(Math.ceil(cat.percentage), database.categories[c].COLOR)
71 | title[c] = `\n${c}: ${cat.percentage}% / ${cat.hours}h`
72 | }
73 | }
74 | bar.title += ` / ${dayNames[toTimeStamp(date).getDay()]} / ${database.days[date].entries.length}e`
75 | }
76 | for (const t in title) bar.title += title[t]
77 | }
78 |
79 | // mouse events
80 | bar.addEventListener("mouseover", function( event ) {
81 | let el = event.target
82 | while (el.parentElement.id !== 'bars') el = el.parentElement
83 | today.innerHTML = `${el.id} `
84 | }, false);
85 | }
86 |
87 | const firstDateContainer = document.createElement('span')
88 | const lastDateContainer = document.createElement('span')
89 | firstDateContainer.id = 'firstDate'
90 | lastDateContainer.id = 'lastDate'
91 | firstDateContainer.innerHTML += from
92 | lastDateContainer.innerHTML += convertDateToDB(to)
93 |
94 | footer.appendChild(firstDateContainer)
95 | footer.appendChild(today)
96 | footer.appendChild(lastDateContainer)
97 | }
98 |
99 | this.addSegment = function (height, color) {
100 | const html =
101 | `
102 |
103 | `
104 | return html
105 | }
106 |
107 | this.diff = function (start, end) {
108 | return Math.round((start - end)/60/60/1000 * 2) * 0.5
109 | }
110 | }
--------------------------------------------------------------------------------
/scripts/lib/helpers.js:
--------------------------------------------------------------------------------
1 | // Transforms
2 |
3 | String.prototype.toTitleCase = function () { return this.toLowerCase().split(' ').map((s) => s.charAt(0).toUpperCase() + s.substring(1)).join(' ') }
4 | String.prototype.toUrl = function () { return this.toLowerCase().replace(/ /g, '+').replace(/[^0-9a-z\(\)\+\:\-\.\/\~]/gi, '').trim() }
5 | String.prototype.toEntities = function () { return this.replace(/[\u00A0-\u9999<>\&]/gim, function (i) { return `${i.charCodeAt(0)}` }) }
6 | String.prototype.toAlpha = function () { return this.replace(/[^a-z ]/gi, '').trim() }
7 | String.prototype.toAlphanum = function () { return this.replace(/[^0-9a-z ]/gi, '') }
8 | String.prototype.isAlphanum = function () { return !!this.match(/^[A-Za-z0-9 ]+$/) }
9 | String.prototype.toLink = function (name, cl) { return this.indexOf('(') === 0 ? this.toReplLink(name, cl) : this.indexOf('//') > -1 ? this.toExternalLink(name, cl) : this.toLocalLink(name, cl) }
10 | String.prototype.toLocalLink = function (name, cl = '') { return `${name || this} ` }
11 | String.prototype.toExternalLink = function (name, cl = '') { return `${name || this} ` }
12 | String.prototype.toReplLink = function (name, cl = '') { return `${name || this} ` }
13 | String.prototype.stripHTML = function () { return this.replace(/<(?:.|\n)*?>/gm, '') }
14 | String.prototype.replaceAll = function (search, replacement) { return `${this}`.split(search).join(replacement) }
15 | String.prototype.isUrl = function () { return this.substr(0, 4) === 'http' }
16 | String.prototype.insert = function (s, i) { return [this.slice(0, i), s, this.slice(i)].join('') }
17 | String.prototype.toPath = function () { return this.toLowerCase().replace(/ /g, '.').replace(/[^0-9a-z\.]/gi, '').trim() }
18 |
19 | const convertDateToDB = function(date) {
20 | const zero = "0"
21 | const yy = date.getFullYear().toString().slice(2,4)
22 | let mm = date.getMonth() + 1
23 | mm = mm < 10 ? zero.concat(mm.toString()) : mm.toString()
24 | let dd = date.getDate()
25 | dd = dd < 10 ? zero.concat(dd.toString()) : dd.toString()
26 | return yy.concat(mm).concat(dd)
27 | }
28 |
29 | const toTimeStamp = function(date, time = '0000') {
30 | const year = parseInt("20" + date.slice(0,2))
31 | const month = parseInt(date.slice(2,4),10) - 1
32 | const day = parseInt(date.slice(4,6),10)
33 | const hour = parseInt(time.slice(0,2),10)
34 | const minute = parseInt(time.slice(2,4),10)
35 | const d = new Date(year,month,day,hour,minute)
36 | return d
37 | }
38 |
39 | const prettyDate = function(timeStamp, long = false) {
40 | const d = new Date(timeStamp)
41 | const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
42 | const day = d.getDate()
43 | const month = long ? months[d.getMonth()] : months[d.getMonth()].slice(0,3).concat('.')
44 | const year = d.getFullYear()
45 | const date = `${day}. ${month} ${year}`
46 | return date
47 | }
48 |
49 | const getToday = function() {
50 | const today = new Date()
51 | return convertDateToDB(today)
52 | }
53 |
54 | const longestString = function(strings) {
55 | let longest = strings[0].toString().length
56 | for (const id in strings) {
57 | const length = strings[id].toString().length
58 | if (length > longest) longest = length
59 | }
60 | return longest
61 | }
62 |
63 | const fillWithChar = function(oldLength, newLength, char) {
64 | let chars = ""
65 | while (oldLength < newLength) {
66 | chars += char
67 | oldLength++
68 | }
69 | return chars
70 | }
71 |
72 | const leadingZero = function(number, zeros = 1) {
73 | const one = "1"
74 | let leading = ""
75 | let counter = 0
76 | while (counter < zeros) {
77 | leading = leading.concat("0")
78 | counter++
79 | }
80 | return number < parseInt(one.concat(leading)) ? leading.concat(number.toString()) : number.toString()
81 | }
82 |
83 | const getDuration = function(start, end) {
84 | const staHrs = leadingZero(start.getHours())
85 | const staMin = leadingZero(start.getMinutes())
86 | const endHrs = leadingZero(end.getHours())
87 | const endMin = leadingZero(end.getMinutes())
88 | // console.info(`Start: ${convertDateToDB(start)} ${staHrs}:${staMin} - End: ${convertDateToDB(end)} ${endHrs}:${endMin} - Duration: ${(end - start)/1000/60/60}`)
89 | return (end - start)/1000/60/60
90 | }
91 |
--------------------------------------------------------------------------------
/scripts/day.js:
--------------------------------------------------------------------------------
1 | 'const strict'
2 |
3 | function Day (date) {
4 | this.date = date
5 | this.entries = []
6 | this.categories = {}
7 | this.projects = {}
8 | this.firstEntry = {}
9 | this.timeline = []
10 | this.trackedHours = 0
11 |
12 | this.update = function () {
13 | // parse entries
14 | for (const e in this.entries) {
15 | const entry = this.entries[e]
16 | let hours = 0
17 |
18 | // store the entries timestamp
19 | entry.timeStamp = toTimeStamp(this.date, entry.time)
20 |
21 | // pick the previous entries timestamp (midnight if it's the first entry)
22 | const prevEntry = e == 0 ? { timeStamp : toTimeStamp(entry.date) } : this.entries[e-1]
23 | let project = e == 0 ? this.firstEntry.project : prevEntry.project
24 | let tags = e == 0 ? this.firstEntry.tags : prevEntry.tags
25 | let score = e == 0 ? this.firstEntry.scr : prevEntry.scr
26 | let time = e == 0 ? "0000" : prevEntry.time
27 |
28 | if (prevEntry.project === "!") continue
29 | hours = (entry.timeStamp - prevEntry.timeStamp) / (60 * 60 * 1000)
30 | if (hours > 24) console.warn(`Logging error detected in logbook at ${this.date}: ${entry.time}`)
31 | else if (project) {
32 | this.addHours(hours, project, tags)
33 | this.addTimeLineEntry(time, hours, project, tags)
34 | }
35 | if (score && database.scores) this.scores.add(score, time)
36 |
37 | // calculate hours and add the to the project
38 | if (e == this.entries.length - 1) {
39 | // add the days remaining hours to the last entry
40 | const nextDay = { timeStamp : toTimeStamp(this.date)}
41 | nextDay.timeStamp.setDate(nextDay.timeStamp.getDate() + 1)
42 |
43 | // check if the next day is in the future
44 | let diff = nextDay.timeStamp - entry.timeStamp
45 | const isFuture = new Date() - nextDay.timeStamp < 0 ? true : false
46 | if (isFuture) diff = new Date() - entry.timeStamp
47 |
48 | // calculate the hours
49 | hours = parseFloat((diff / (60 * 60 * 1000)).toFixed(1))
50 | this.addHours(hours, entry.project, entry.tags)
51 | this.addTimeLineEntry(entry.time, hours, entry.project, entry.tags)
52 | }
53 |
54 | // end at current time
55 | if (new Date() - entry.timeStamp < 0) break
56 | }
57 | // update stats
58 | database.stats.total.hours += this.trackedHours
59 |
60 | // calculate category percentages
61 | for (const c in this.categories) {
62 | const cat = this.categories[c]
63 | cat.percentage = parseFloat((cat.hours * 100 / 24).toFixed(1))
64 | }
65 |
66 | // calculate score averages
67 | this.scores.getAverages()
68 | }
69 |
70 | this.scores = {
71 | scores : {},
72 | average : {},
73 | add : function (score, time) {
74 | for (let i = 0; i < score.length; i++) {
75 | const s = parseInt(score[i])
76 | if (!database.scores[i]) { console.warn(`Can't map score to category. Add more categories to scores.ndtl`); continue }
77 | const cat = database.scores[i].category
78 | if (!this.scores[cat]) this.scores[cat] = []
79 | if (!isNaN(s)) {
80 | const entry = { score: 10 - s - 5, time : time }
81 | this.scores[cat].push(entry)
82 | }
83 | }
84 | },
85 | getAverages : function () {
86 | for (const s in this.scores) {
87 | const cat = this.scores[s]
88 | const avrg = parseFloat((this.getSum(cat) / cat.length).toFixed(2))
89 | this.average[s] = isNaN(avrg) ? 0 : avrg
90 | }
91 | },
92 | getSum : function (array) {
93 | let sum = 0
94 | let i = array.length
95 | while (i--) sum += array[i].score
96 | return sum
97 | }
98 | }
99 |
100 | this.addHours = function (hours, project, tags, time) {
101 | // check if project exists
102 | project = project.toUpperCase()
103 | const prj = database.projects[project]
104 | if (prj) {
105 | const cat = prj.CAT
106 | // add hours to project
107 | if (!this.projects[project]) this.projects[project] = { hours : 0, count: 0, tags : {}, NAME : project, CAT : cat, }
108 | this.projects[project].hours += hours
109 | this.projects[project].count += 1
110 | // add tag
111 | const tag = this.parseTag(tags)
112 | if (tag) this.addTag(tag, hours, this.projects[project].tags)
113 | // add hours to categories
114 | if (!this.categories[cat]) this.categories[cat] = { hours : 0, percentage : 0 }
115 | this.categories[cat].hours += hours
116 | // add hours to checkSum
117 | this.trackedHours += hours
118 | } else console.warn(`Project ${project} on day ${this.date} doesn't exist in projects database.`)
119 | }
120 |
121 | this.addTimeLineEntry = function (time, hours, project, tags) {
122 | // add data to timeline
123 | const data = { time : time, duration : hours, project : project, tags : tags }
124 | this.timeline.push(data)
125 | }
126 |
127 | this.addTag = function (tag, hours, object) {
128 | if (!object[tag]) object[tag] = { count : 0, hours : 0 }
129 | object[tag].count += 1
130 | object[tag].hours += hours
131 | }
132 |
133 | this.parseTag = function (string) {
134 | let tag = string.split('#')[1]
135 | if (tag) tag = tag.split(' ')[0]
136 | else return null
137 | return tag
138 | }
139 | }
--------------------------------------------------------------------------------
/links/main.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --background: #29272b;
3 | --f_inv: #43423E;
4 | --b_high: #E8E9E2;
5 | --b_med: #C2C1BB;
6 | --b_low: #4B4B49;
7 | --b_inv: #0C0B0;
8 | --rounded: 2px;
9 | }
10 | * { margin:0; padding:0; border:0; outline:0; border-spacing:0; text-decoration:none; font-weight:inherit; font-style:inherit; color:inherit; font-size:100%; font-family:inherit; vertical-align:baseline; list-style:none; border-collapse:collapse; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale }
11 | *:focus { outline: none}
12 | body { background:var(--background); color:white; padding:16px; overflow-x: hidden; transition: opacity 150ms; opacity: 1 !important; font-family: var(--mono); font-size: 12px }
13 | h1 { font-size: 32px; font-weight: bold; text-transform: uppercase; border-bottom: 2px solid var(--b_med); margin-bottom: 8px; }
14 | h2 { font-size: 24px; font-weight: bold; text-transform: uppercase; }
15 | h3 { font-size: 16px; font-weight: bold; text-transform: uppercase; }
16 | p { margin-bottom: 10px; line-height: 2; }
17 | b { font-weight:bold }
18 | i { font-style:italic }
19 | a { cursor: pointer }
20 | ul { display: block }
21 | ul li { display: block }
22 | hr { clear:both }
23 | code { display: block; white-space: pre }
24 | svg { stroke-width: 10; stroke: white; stroke-linecap: round }
25 | small { font-size: 80%; }
26 |
27 | #logbook section { padding: 8px; margin-bottom: 32px; }
28 | #logbook section section { padding: 0; margin: 0; }
29 | #logbook #stats { display: flex; justify-content: space-between; align-items: baseline; }
30 | #logbook #overview #total #bars { display: flex; width:100%; }
31 | #logbook #overview section { margin-top: 16px; }
32 | #logbook #overview .bar { margin-right: 4px; }
33 | #logbook #overview .bar:last-child { margin:0; }
34 | #logbook #overview .bar svg { height: 8px; border-radius: var(--rounded) }
35 |
36 | #logbook #legend { display: flex; justify-content: flex-start; }
37 | #logbook #legend .item { display: flex; align-items: stretch; margin-right: 8px; padding: 0 4px 0 4px; border-radius: 2px; }
38 | #logbook #legend a.item:hover { background-color: var(--b_low); }
39 | #logbook #legend .box { width: 8px; height: 8px; margin-right: 4px; }
40 | #logbook #legend .box svg { border-radius: var(--rounded); }
41 | #logbook #legend .cat { text-transform: uppercase; }
42 |
43 | #logbook #balance header { margin-bottom: 8px; }
44 | #logbook #balance #bars { display:flex; height: 128px; }
45 | #logbook #balance #bars a { display: flex; flex-direction: column; justify-content: flex-end; margin-right: 2px; background-color: var(--b_low); border-radius: var(--rounded); box-sizing: border-box; }
46 | #logbook #balance #bars a:hover { background-color: white; }
47 | #logbook #balance #bars a:hover { opacity: 0.5; }
48 | #logbook #balance #bars a svg:first-child { border-radius: var(--rounded) var(--rounded) 0px 0px; }
49 | #logbook #balance #bars a svg:last-child { border-radius: 0px 0px var(--rounded) var(--rounded); }
50 | #logbook #balance section { margin-top: 16px; }
51 | #logbook #balance section>section { margin: 0; }
52 | #logbook #balance #past figure:last-child { margin: 0; }
53 | #logbook #balance footer { margin-top: 4px; border-top: 2px solid var(--b_low); display: flex; justify-content: space-between; color: var(--b_med); }
54 |
55 | #logbook #projects #categories { display: flex; }
56 | #logbook #projects #categories section { width: 100%; margin-right: 32px; }
57 | #logbook #projects #categories section:last-child { margin-right: 0; }
58 | #logbook #projects #categories figure { margin: 8px 0px 4px 0px; padding-bottom: 8px; border-bottom: 1px solid var(--b_low); }
59 |
60 | #logbook #graph { height: 128px; position: relative; background-color: var(--b_low); border-radius: var(--rounded); margin-bottom: 4px; }
61 | #logbook #graph svg { position: absolute; top: 0; left: 0; }
62 | #logbook #graph #bars { display: flex; }
63 | #logbook #graph #bars a { height: 128px; margin-right: 2px; z-index: 1; }
64 | #logbook #graph #bars a:last-child { margin: 0; border: 0; }
65 | #logbook #graph #bars a:hover { border-right: 3px solid var(--b_high); background-color: rgba(255,255,255,0.1); }
66 |
67 | #logbook #datepicker { display: flex; }
68 | #logbook #datepicker form { font-size: 16px; font-weight: bold; font-family:inherit; }
69 | #logbook #datepicker form select { color: var(--b_high); background-color: var(--background); width: 100%; box-sizing: border-box; border-radius: 0; -moz-appearance: none; -webkit-appearance: none; appearance: none; }
70 | #logbook #datepicker form select:hover { background-color: var(--b_low); }
71 | #logbook #datepicker form select option { font-family: monospace; font-weight: normal; }
72 |
73 | #logbook #timeline #ruler { width: 100%; height:16px; margin-bottom:4px; display: flex; align-items: center; border-bottom: 2px solid var(--b_low); }
74 | #logbook #timeline .hour { height: 100%; position: relative; display: flex; padding-left:2px; color: var(--b_low); border-left: 2px solid var(--b_low); }
75 | #logbook #timeline .hour:last-child { border-right: 2px solid var(--b_low); }
76 | #logbook #timeline #label { position: absolute; top:0; left:2px; color: var(--b_med); }
77 | #logbook #timeline #items { width: 100%; height:16px; margin-bottom:2px; display: flex; justify-content: flex-start; background-color:var(--b_low); border-radius: var(--rounded); }
78 | #logbook #timeline .item { height: 100%; margin-right: 2px; border-radius: var(--rounded); }
79 | #logbook #timeline .item:last-child { margin: 0; }
80 |
81 | .empty { fill: var(--b_low); stroke: var(--b_low); color: var(--b_low); }
82 | .stroke { stroke-width: 2px; stroke-linecap: round; stroke-linejoin: round; fill: none; }
83 | .lc { text-transform: lowercase; }
84 | .rounded { border-radius: var(--rounded); }
85 | .small { font-size: 80%; }
--------------------------------------------------------------------------------
/scripts/interface/scores.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function Scores () {
4 | const height = 128
5 | const h = 9
6 | const center = h * 0.5
7 | const offset = 0.1
8 | this.isHome = true
9 |
10 | const scores = document.createElement('section')
11 | scores.id = 'scores'
12 |
13 | this.install = function (host) {
14 | host.appendChild(scores)
15 | }
16 |
17 | this.update = function () {
18 | this.isHome = logbook.interface.isHome
19 |
20 | scores.innerHTML = ''
21 | const header = document.createElement('header')
22 | header.innerHTML = `Rhythm `
23 | scores.appendChild(header)
24 |
25 | if (this.isHome) this.createGraph()
26 | else this.createDailyGraph()
27 | this.createLegend()
28 | }
29 |
30 | this.createGraph = function () {
31 | const graph = document.createElement('figure')
32 | graph.id = 'graph'
33 |
34 | const dur = database.stats.filter.days
35 | const width = 100 / dur
36 | let lines = ''
37 |
38 | const bars = document.createElement('div')
39 | bars.id = 'bars'
40 |
41 | this.points = []
42 |
43 | const d = toTimeStamp(database.stats.filter.lastEntry)
44 | for (let i = 0; i <= dur; i++) {
45 | const x = i
46 |
47 | let date = new Date(d)
48 | date.setDate(d.getDate() - dur + i)
49 | const day = dayNames[date.getDay()]
50 | date = convertDateToDB(date)
51 |
52 | const bar = document.createElement('a')
53 | bar.href = `#${date}`
54 | bar.id = date
55 | bar.title = `${date} / ${day}`
56 | bar.style = `width: ${width}%;`
57 | if (i <= dur && i != 0) bars.appendChild(bar)
58 |
59 | // add segmentation
60 | lines += ` `
61 | // get scores as position data
62 | for (const s in database.scores) {
63 | const cat = database.scores[s].category
64 | if (!this.points[s]) this.points[s] = ''
65 | // average scores over the filtered amount of days
66 | if (database.days[date]) {
67 | if (!database.days[date].scores.scores[cat]) continue
68 | const y = center - database.days[date].scores.average[cat]
69 | this.points[s] += `${x} ${y} `
70 | bar.title += `\n${cat}: ${database.days[date].scores.average[cat]} / ${database.days[date].scores.scores[cat].length}e`
71 | } else {
72 | this.points[s] += `${x} ${center + offset * s} `
73 | }
74 | }
75 | }
76 | const data = { height: height, dur : dur, h : h, center : center, points : this.points, lines : lines }
77 | const svg = this.createSVG(data)
78 | graph.appendChild(svg)
79 | graph.appendChild(bars)
80 | scores.appendChild(graph)
81 | }
82 |
83 | this.createDailyGraph = function () {
84 | const graph = document.createElement('figure')
85 | graph.id = 'graph'
86 |
87 | const dur = 24
88 | const width = 100 / dur
89 | let lines = ''
90 |
91 | this.points = []
92 | this.day = database.days[page.url]
93 |
94 | const d = toTimeStamp(page.url)
95 | for (let x = 0; x <= dur; x++) lines += ` `
96 |
97 | // get scores as position data
98 | for (const c in database.scores) {
99 | const cat = database.scores[c].category
100 | if (!this.points[c]) this.points[c] = ''
101 | if (!this.day.scores.scores[cat]) continue
102 | for (const s in this.day.scores.scores[cat]) {
103 | this.entry = this.day.scores.scores[cat][s]
104 | console.log(this.entry.time)
105 | this.x = 0 //this.entry.time
106 | this.y = center + this.entry.score
107 | this.points[s] += `${this.x} ${this.y} `
108 | }
109 | }
110 | const data = { height: height, dur : dur, h : h, center : center, points : this.points, lines : lines }
111 | const svg = this.createSVG(data)
112 | graph.appendChild(svg)
113 | scores.appendChild(graph)
114 | }
115 |
116 | this.createSVG = function ( data ) {
117 | let polylines = ''
118 | for (const c in data.points) {
119 | polylines += ` `
120 | }
121 | const svg = document.createElement('div')
122 | svg.id = 'svg'
123 | svg.innerHTML =
124 | `
125 | ${data.lines}
126 |
127 | ${polylines}
128 | `
129 | return svg
130 | }
131 |
132 | this.createLegend = function () {
133 | const legend = document.createElement('figure')
134 | legend.id = 'legend'
135 |
136 | for (let i = 0; i < database.scores.length; i++) {
137 | const c = database.scores[i]
138 |
139 | const item = document.createElement('a')
140 | item.className = 'item'
141 | item.id = c.category
142 | item.onclick = function() { logbook.interface.scores.toggle(c.category) }
143 |
144 | const box = document.createElement('div')
145 | box.className = 'box'
146 | box.innerHTML =
147 | `
148 |
149 | `
150 |
151 | const cat = document.createElement('div')
152 | cat.className = 'cat'
153 | cat.innerHTML = c.category.toUpperCase()
154 |
155 | item.appendChild(box)
156 | item.appendChild(cat)
157 | legend.appendChild(item)
158 | }
159 | scores.appendChild(legend)
160 | }
161 |
162 | this.toggle = function ( category ) {
163 | const rect = document.querySelector(`section#scores figure#legend a#${category} div.box svg rect`)
164 | const line = document.querySelector(`figure#graph div#svg svg polyline#${category}`)
165 | if (line.style.display === 'none') {
166 | line.style.display = 'unset'
167 | rect.style.fill = database.settings.SCORES[category]
168 | } else {
169 | line.style.display = 'none'
170 | rect.style.fill = 'var(--b_med)'
171 | }
172 | }
173 | }
--------------------------------------------------------------------------------
/scripts/database.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function Database () {
4 |
5 | this.install = function () {
6 | database.settings = indental(DATABASE.settings)
7 | if (DATABASE.logs) database.logs = tablatal(DATABASE.logs)
8 | else this.error(`Couldn't find 'Logbook' database!`,`Please make sure there is a file named logbook.tbtl in the database folder that starts with 'DATABASE.logs ='`)
9 |
10 | // initialise categories & projects
11 | database.categories = {}
12 | for (const c in database.settings.CATEGORIES) database.categories[c] = { COLOR : database.settings.CATEGORIES[c] }
13 | database.projects = {}
14 | for (const p in database.settings.PROJECTS) {
15 | const project = database.settings.PROJECTS[p]
16 | const name = project.NAME == undefined ? p : project.NAME
17 | if (!project.CAT) this.error(`Missing category:`,`Project '${project}' has no category. Please update settings.ndtl.`)
18 | else if (!database.categories[project.CAT]) this.error(`Couldn't match category '${project.CAT}'`,`Project '${name}' has the category '${project.CAT}' . This category doesn't exist in settings.ndtl. Please update the settings file.`)
19 | database.projects[p] = { CAT : project.CAT, NAME : name }
20 | }
21 |
22 | // initialise scores
23 | database.scores = []
24 | for (const s in database.settings.SCORES) {
25 | const cat = { category : s, color : database.settings.SCORES[s] }
26 | database.scores.push(cat)
27 | }
28 |
29 | database.days = {}
30 | database.tags = {}
31 | database.stats = {}
32 | database.stats.total = {
33 | firstEntry : 0,
34 | lastEntry : 0,
35 | days : 0,
36 | hours : 0,
37 | entries : 0
38 | }
39 | }
40 |
41 | this.start = function () {
42 |
43 | // loop through logbook entries starting with the oldest entry
44 | const time = performance.now()
45 | const entries = database.logs
46 | const first = entries.length - 1
47 | let day
48 |
49 | for (let id = first; id >= 0; id--) {
50 | const entry = entries[id]
51 | if (id === first) {
52 | // first entry
53 | // check validity
54 | if (entry.date.charAt(0) === "-") { console.info(`First entry in Logs Database doesn't have a date!`); return }
55 | if (entry.project.charAt(0) === "-") { console.info(`First entry in Log doesn't have a project!`); return }
56 | database.stats.total.firstEntry = entry.date
57 | this.addDay(entry)
58 | day = database.days[entry.date]
59 | } else {
60 | const prevEntry = day.entries[day.entries.length - 1]
61 | const diff = toTimeStamp(prevEntry.date) - toTimeStamp(entry.date)
62 | // TODO: add check for proper times
63 | const isPrevDay = diff > 0 ? true : false
64 | const isSameDay = diff == 0 || entry.date.charAt(0) === "-" ? true : false
65 | if (isSameDay) day.entries.push(entry)
66 | else {
67 | day.update()
68 | this.addDay(entry)
69 | day = database.days[entry.date]
70 | if (!isPrevDay) day.firstEntry = prevEntry
71 | }
72 | }
73 | if (id == 0) {
74 | day.update()
75 | database.stats.total.lastEntry = day.date
76 | }
77 | // update stats
78 | database.stats.total.entries += 1
79 | }
80 | for (const cat in database.categories) database.categories[cat].percentage = parseFloat((database.categories[cat].hours * 100 / database.stats.hours).toFixed(2))
81 | console.info(`Found ${database.stats.total.entries} entries in database, in ${(performance.now() - time).toFixed(2)}ms.`)
82 | }
83 |
84 | this.update = function (params) {
85 |
86 | const time = performance.now()
87 | database.stats.filter = {
88 | firstEntry : 0,
89 | lastEntry : 0,
90 | days : 0,
91 | hours : 0,
92 | entries : 0
93 | }
94 |
95 | // check date validity
96 | this.to = this.isValidDate(dates.to) ? convertDateToDB(toTimeStamp(dates.to.toString())) : convertDateToDB(new Date())
97 | this.from = this.isValidDate(dates.from) ? convertDateToDB(toTimeStamp(dates.from.toString())) : this.shiftDate(this.to, -31)
98 | // odd date combincation handling
99 | if (parseInt(this.to) < parseInt(this.from)) this.from = this.shiftDate(this.to, -30)
100 |
101 | dates.from = this.from
102 | dates.to = this.to
103 |
104 |
105 | const from = toTimeStamp(this.from)
106 | const to = toTimeStamp(this.to)
107 | const dayCount = Math.round((to - from) / (60 * 60 * 1000 * 24))
108 | const days = {}
109 |
110 | // update stats
111 | database.stats.filter.firstEntry = this.from
112 | database.stats.filter.lastEntry = this.to
113 | database.stats.filter.entries = 0
114 | database.stats.filter.hours = 0
115 | database.stats.filter.days = dayCount + 1
116 |
117 | // reset categories
118 | for (const c in database.categories) { database.categories[c].hours = 0; database.categories[c].percentage = 0 }
119 |
120 | //reset projects
121 | for (const p in database.projects) { database.projects[p].hours = 0; database.projects[p].count = 0; database.projects[p].tags = {} }
122 |
123 | // reset tags
124 | for (const t in database.tags) { database.tags[t].hours = 0; database.tags[t].count = 0; }
125 |
126 | // get data between from - to dates
127 | for (let i = 0; i <= dayCount; i++) {
128 | let day = new Date(to)
129 | day.setDate(to.getDate() - i)
130 | day = convertDateToDB(day)
131 | day = database.days[day]
132 | if (!day) continue
133 |
134 | // add hours to projects
135 | this.filterEntries(day)
136 |
137 | // add hours to categories
138 | for (const c in day.categories) {
139 | const cat = database.categories[c]
140 | cat.hours += day.categories[c].hours
141 | }
142 |
143 | // add hours to stats
144 | database.stats.filter.hours += day.trackedHours
145 | database.stats.filter.entries += day.entries.length
146 | }
147 |
148 | // calculate category percentages
149 | for (const c in database.categories) {
150 | const cat = database.categories[c]
151 | if (!cat.hours) cat.hours = 0
152 | cat.percentage = parseFloat((cat.hours * 100 / database.stats.filter.hours).toFixed(2))
153 | if (isNaN(cat.percentage)) cat.percentage = 0
154 | }
155 | console.info(`Filtered ${database.stats.filter.entries} entries from ${this.from} to ${this.to}, in ${(performance.now() - time).toFixed(2)}ms.`)
156 | console.info(database)
157 | }
158 |
159 | this.isValidDate = function (date, filtered = false) {
160 | if (date === undefined || isNaN(date) || date === 0) return false
161 | return true
162 | }
163 |
164 | this.addDay = function (entry) {
165 | database.days[entry.date] = new Day(entry.date)
166 | database.days[entry.date].entries.push(entry)
167 | database.stats.total.days += 1
168 | }
169 |
170 | this.filterEntries = function (day) {
171 | if (!day.projects) return
172 | for (const p in day.projects) {
173 | if (database.projects[p]) {
174 | const project = database.projects[p]
175 |
176 | if (!project.hours) project.hours = 0
177 | if (!project.count) project.count = 0
178 | if (!project.firstEntry) project.firstEntry = day.date
179 | if (!project.tags) project.tags = {}
180 |
181 | // update the project hours
182 | project.hours += day.projects[p].hours
183 | project.count += day.projects[p].count
184 |
185 | // update the project tags
186 | this.filterTags(day, p)
187 | } else console.warn(`Couldn't find project in Projects Database at entry ${day.date}: ${p}`)
188 | }
189 | }
190 |
191 | this.shiftDate = function (date, amount, format = true) {
192 | this.dateOld = toTimeStamp(date)
193 | this.dateNew = new Date(this.dateOld)
194 | this.dateNew.setDate(this.dateOld.getDate() + amount)
195 | return format ? convertDateToDB(this.dateNew) : this.date
196 | }
197 |
198 | this.filterTags = function (day, project) {
199 | const p = project
200 | for (const t in day.projects[p].tags) {
201 | const tag = day.projects[p].tags[t]
202 | // add tags to project
203 | if (!database.projects[p].tags[t]) database.projects[p].tags[t] = { count : 0, hours : 0 }
204 | const filterTag = database.projects[p].tags[t]
205 | filterTag.hours += tag.hours
206 | filterTag.count += 1
207 | // add totals to tags
208 | if (!database.tags[t]) database.tags[t] = { count : 0, hours : 0 }
209 | const totalTag = database.tags[t]
210 | totalTag.hours += tag.hours
211 | totalTag.count += 1
212 | // track categories
213 | if (!totalTag.categories) totalTag.categories = {}
214 | if (!totalTag.categories[database.projects[p].CAT]) totalTag.categories[database.projects[p].CAT] = { hours : 0, count : 0}
215 | const catTag = totalTag.categories[database.projects[p].CAT]
216 | catTag.hours += tag.hours
217 | catTag.count += 1
218 | }
219 | }
220 |
221 | this.error = function (title, message) {
222 | const error = {}
223 | error.title = title
224 | error.text = message
225 | logbook.errors.push(error)
226 | }
227 | }
--------------------------------------------------------------------------------
/logbook.tbtl:
--------------------------------------------------------------------------------
1 | DATABASE.logs = `
2 | DATE TIME SCR PROJECT TAGS
3 | ------ 2100 --6 Sleep ---
4 | ------ 1900 --- Personal #Idle
5 | ------ 1730 577 Personal #Read Book XYZ
6 | ------ 1700 486 Personal #Commute Home
7 | ------ 0900 323 Company #Code Working on XYZ
8 | ------ 0830 --- Company #Commute
9 | ------ 0730 129 Cooking #Breakfast Porrige
10 | 200130 0700 543 Household #Routines
11 | ------ 2100 --- Sleep ---
12 | ------ 1900 --- Personal #Idle
13 | ------ 1730 513 Personal #Read Book XYZ
14 | ------ 1700 412 Personal #Commute Home
15 | ------ 0900 964 Company #Code Working on XYZ
16 | ------ 0830 --- Company #Commute
17 | ------ 0730 423 Cooking #Breakfast Porrige
18 | 200129 0700 523 Household #Routines
19 | ------ 2100 121 Sleep ---
20 | ------ 1900 --- Personal #Idle
21 | ------ 1730 574 Personal #Read Book XYZ
22 | ------ 1700 455 Personal #Commute Home
23 | ------ 0900 324 Company #Code Working on XYZ
24 | ------ 0830 --- Company #Commute
25 | ------ 0730 423 Cooking #Breakfast Porrige
26 | 200128 0700 543 Household #Routines
27 | ------ 2100 --- Sleep ---
28 | ------ 1900 --- Personal #Idle
29 | ------ 1730 574 Personal #Read Book XYZ
30 | ------ 1700 455 Personal #Commute Home
31 | ------ 0900 324 Company #Code Working on XYZ
32 | ------ 0830 --- Company #Commute
33 | ------ 0730 423 Cooking #Breakfast Porrige
34 | 200127 0700 543 Household #Routines
35 | ------ 2100 --- Sleep ---
36 | ------ 1900 --- Personal #Idle
37 | ------ 1730 574 Personal #Read Book XYZ
38 | ------ 1700 455 Personal #Commute Home
39 | ------ 0900 324 Company #Code Working on XYZ
40 | ------ 0830 --- Company #Commute
41 | ------ 0730 423 Cooking #Breakfast Porrige
42 | 200126 0700 754 Household #Routines
43 | ------ 2100 422 Sleep ---
44 | ------ 1900 --- Personal #Idle
45 | ------ 1730 574 Personal #Read Book XYZ
46 | ------ 1700 465 Personal #Commute Home
47 | ------ 0900 324 Company #Code Working on XYZ
48 | ------ 0830 --- Company #Commute
49 | ------ 0730 423 Cooking #Breakfast Porrige
50 | 200125 0700 753 Household #Routines
51 | ------ 2100 --- Sleep ---
52 | ------ 1900 2-- Personal #Idle
53 | ------ 1730 274 Personal #Read Book XYZ
54 | ------ 1700 255 Personal #Commute Home
55 | ------ 0900 324 Company #Code Working on XYZ
56 | ------ 0830 423 Company #Commute
57 | ------ 0730 543 Cooking #Breakfast Porrige
58 | 200124 0700 --- Household #Routines
59 | ------ 2100 --- Sleep ---
60 | ------ 1900 574 Personal #Idle
61 | ------ 1730 455 Personal #Read Book XYZ
62 | ------ 1700 324 Personal #Commute Home
63 | ------ 0900 --- Company #Code Working on XYZ
64 | ------ 0830 423 Company #Commute
65 | ------ 0730 543 Cooking #Breakfast Porrige
66 | 200123 0700 --- Household #Routines
67 | ------ 2100 --- Sleep ---
68 | ------ 1900 574 Personal #Idle
69 | ------ 1730 455 Personal #Read Book XYZ
70 | ------ 1700 324 Personal #Commute Home
71 | ------ 0900 --- Company #Code Working on XYZ
72 | ------ 0830 423 Company #Commute
73 | ------ 0730 754 Cooking #Breakfast Porrige
74 | 200122 0700 --- Household #Routines
75 | ------ 2100 --- Sleep ---
76 | ------ 1900 574 Personal #Idle
77 | ------ 1730 465 Personal #Read Book XYZ
78 | ------ 1700 324 Personal #Commute Home
79 | ------ 0900 --- Company #Code Working on XYZ
80 | ------ 0830 423 Company #Commute
81 | ------ 0730 753 Cooking #Breakfast Porrige
82 | 200121 0700 --- Household #Routines
83 | ------ 2100 2-- Sleep ---
84 | ------ 1900 274 Personal #Idle
85 | ------ 1730 255 Personal #Read Book XYZ
86 | ------ 1700 455 Personal #Commute Home
87 | ------ 0900 324 Company #Code Working on XYZ
88 | ------ 0830 --- Company #Commute
89 | ------ 0730 423 Cooking #Breakfast Porrige
90 | 200120 0700 543 Household #Routines
91 | ------ 2100 --- Sleep ---
92 | ------ 1900 --- Personal #Idle
93 | ------ 1730 574 Personal #Read Book XYZ
94 | ------ 1700 455 Personal #Commute Home
95 | ------ 0900 324 Company #Code Working on XYZ
96 | ------ 0830 423 Company #Commute
97 | ------ 0730 543 Cooking #Breakfast Porrige
98 | 200119 0700 --- Household #Routines
99 | ------ 2100 --- Sleep ---
100 | ------ 1900 574 Personal #Idle
101 | ------ 1730 455 Personal #Read Book XYZ
102 | ------ 1700 324 Personal #Commute Home
103 | ------ 0900 --- Company #Code Working on XYZ
104 | ------ 0830 423 Company #Commute
105 | ------ 0730 543 Cooking #Breakfast Porrige
106 | 200118 0700 --- Household #Routines
107 | ------ 2100 --- Sleep ---
108 | ------ 1900 574 Personal #Idle
109 | ------ 1730 455 Personal #Read Book XYZ
110 | ------ 1700 324 Personal #Commute Home
111 | ------ 0900 --- Company #Code Working on XYZ
112 | ------ 0830 423 Company #Commute
113 | ------ 0730 754 Cooking #Breakfast Porrige
114 | 200117 0700 --- Household #Routines
115 | ------ 2100 --- Sleep ---
116 | ------ 1900 574 Personal #Idle
117 | ------ 1730 465 Personal #Read Book XYZ
118 | ------ 1700 324 Personal #Commute Home
119 | ------ 0900 423 Company #Code Working on XYZ
120 | ------ 0830 543 Company #Commute
121 | ------ 0730 --- Cooking #Breakfast Porrige
122 | 200116 0700 --- Household #Routines
123 | ------ 2100 574 Sleep ---
124 | ------ 1900 455 Personal #Idle
125 | ------ 1730 324 Personal #Read Book XYZ
126 | ------ 1700 --- Personal #Commute Home
127 | ------ 0900 423 Company #Code Working on XYZ
128 | ------ 0830 543 Company #Commute
129 | ------ 0730 --- Cooking #Breakfast Porrige
130 | 200115 0700 --- Household #Routines
131 | ------ 2100 574 Sleep ---
132 | ------ 1900 455 Personal #Idle
133 | ------ 1730 324 Personal #Read Book XYZ
134 | ------ 1700 423 Personal #Commute Home
135 | ------ 0900 543 Company #Code Working on XYZ
136 | ------ 0830 --- Company #Commute
137 | ------ 0730 --- Cooking #Breakfast Porrige
138 | 200114 0700 --- Household #Routines
139 | ------ 2100 455 Sleep ---
140 | ------ 1900 324 Personal #Idle
141 | ------ 1730 --- Personal #Read Book XYZ
142 | ------ 1700 423 Personal #Commute Home
143 | ------ 0900 543 Company #Code Working on XYZ
144 | ------ 0830 --- Company #Commute
145 | ------ 0730 --- Cooking #Breakfast Porrige
146 | 200113 0700 --- Household #Routines
147 | ------ 2100 455 Sleep ---
148 | ------ 1900 324 Personal #Idle
149 | ------ 1730 --- Personal #Read Book XYZ
150 | ------ 1700 423 Personal #Commute Home
151 | ------ 0900 543 Company #Code Working on XYZ
152 | ------ 0830 --- Company #Commute
153 | ------ 0730 --- Cooking #Breakfast Porrige
154 | 200112 0700 --- Household #Routines
155 | ------ 2100 455 Sleep ---
156 | ------ 1900 324 Personal #Idle
157 | ------ 1730 --- Personal #Read Book XYZ
158 | ------ 1700 423 Personal #Commute Home
159 | ------ 0900 423 Company #Code Working on XYZ
160 | ------ 0830 543 Company #Commute
161 | ------ 0730 --- Cooking #Breakfast Porrige
162 | 200111 0700 --- Household #Routines
163 | ------ 2300 574 Sleep ---
164 | ------ 1900 455 Personal #Idle
165 | ------ 1730 324 Personal #Read Book XYZ
166 | ------ 1700 --- Personal #Commute Home
167 | ------ 0930 423 Company #Code Working on XYZ
168 | ------ 0830 543 Company #Commute
169 | ------ 0730 --- Cooking #Breakfast Porrige
170 | 200110 0700 423 Household #Routines
171 | ------ 2330 423 Sleep ---
172 | ------ 1900 543 Personal #Idle
173 | ------ 1730 --- Personal #Read Book XYZ
174 | ------ 1400 --- Personal #Commute Home
175 | ------ 0900 --- Company #Code Working on XYZ
176 | ------ 0830 574 Company #Commute
177 | ------ 0730 455 Cooking #Breakfast Porrige
178 | 200109 0700 324 Household #Routines
179 | ------ 2200 423 Sleep ---
180 | ------ 1900 543 Personal #Idle
181 | ------ 1730 --- Personal #Read Book XYZ
182 | ------ 1200 --- Personal #Commute Home
183 | ------ 0930 --- Company #Code Working on XYZ
184 | ------ 0830 574 Company #Commute
185 | ------ 0730 423 Cooking #Breakfast Porrige
186 | 200108 0700 543 Household #Routines
187 | ------ 2130 --- Sleep ---
188 | ------ 1900 --- Personal #Idle
189 | ------ 1730 574 Personal #Read Book XYZ
190 | ------ 1730 455 Personal #Commute Home
191 | ------ 0900 324 Company #Code Working on XYZ
192 | ------ 0830 --- Company #Commute
193 | ------ 0730 423 Cooking #Breakfast Porrige
194 | 200107 0700 543 Household #Routines
195 | ------ 2100 543 Sleep ---
196 | ------ 1900 --- Personal #Idle
197 | ------ 1730 --- Personal #Read Book XYZ
198 | ------ 1700 --- Personal #Commute Home
199 | ------ 0900 574 Company #Code Working on XYZ
200 | ------ 0830 455 Company #Commute
201 | ------ 0730 423 Cooking #Breakfast Porrige
202 | 200106 0700 543 Household #Routines
203 | ------ 2130 --- Sleep ---
204 | ------ 1900 --- Personal #Idle
205 | ------ 1730 574 Personal #Read Book XYZ
206 | ------ 1800 455 Personal #Commute Home
207 | ------ 0900 324 Company #Code Working on XYZ
208 | ------ 0830 --- Company #Commute
209 | ------ 0730 423 Cooking #Breakfast Porrige
210 | 200105 0700 543 Household #Routines
211 | ------ 2100 --- Sleep ---
212 | ------ 1900 --- Personal #Idle
213 | ------ 1730 574 Personal #Read Book XYZ
214 | ------ 1700 455 Personal #Commute Home
215 | ------ 0900 324 Company #Code Working on XYZ
216 | ------ 0830 --- Company #Commute
217 | ------ 0730 423 Cooking #Breakfast Porrige
218 | 200104 0700 754 Household #Routines
219 | ------ 2100 422 Sleep ---
220 | ------ 1930 --- Personal #Idle
221 | ------ 1730 574 Personal #Read Book XYZ
222 | ------ 1700 465 Personal #Commute Home
223 | ------ 0900 324 Company #Code Working on XYZ
224 | ------ 0830 --- Company #Commute
225 | ------ 0730 423 Cooking #Breakfast Porrige
226 | 200103 0700 753 Household #Routines
227 | ------ 2100 --- Sleep ---
228 | ------ 1900 2-- Personal #Idle
229 | ------ 1730 274 Personal #Read Book XYZ
230 | ------ 1700 255 Personal #Commute Home
231 | ------ 0930 324 Company #Code Working on XYZ
232 | ------ 0830 --- Company #Commute
233 | ------ 0730 426 Cooking #Breakfast Porrige
234 | 200102 0700 547 Household #Routines
235 | ------ 2100 --- Sleep ---
236 | ------ 1900 --- Personal #Idle
237 | ------ 1730 574 Personal #Read Book XYZ
238 | ------ 1700 456 Personal #Commute Home
239 | ------ 0900 324 Company #Code Working on XYZ
240 | ------ 0830 --- Company #Commute
241 | ------ 0730 423 Cooking #Breakfast Porrige
242 | ------ 0700 543 Household #Routines
243 | 200101 0000 --- Sleep ---
244 | `
--------------------------------------------------------------------------------
/scripts/interface/interface.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
3 | const dayNamesLong = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']
4 |
5 | function Interface () {
6 | this.logbook = document.createElement('div')
7 | this.logbook.id = 'logbook'
8 | this.scores = new Scores()
9 | this.isHome = true
10 |
11 | this.install = function (host) {
12 | host.appendChild(this.logbook)
13 | }
14 |
15 | this.update = function () {
16 | if (logbook.errors.length > 0) { this.errors(); return }
17 | const time = performance.now()
18 | this.logbook.innerHTML = ''
19 | this.isHome = page.url.length === 0 ? true : false
20 |
21 | if (this.isHome) this.home()
22 | else this.subPage()
23 |
24 | // drop down data selectors
25 | const fromDate = document.querySelector('#fromDate')
26 | const toDate = document.querySelector('#toDate')
27 | if (fromDate) fromDate.onchange = function () { dates.from = fromDate.value; logbook.update() }
28 | if (toDate) toDate.onchange = function () { dates.to = toDate.value; logbook.update() }
29 | console.info(`Logbook built interface in ${(performance.now() - time).toFixed(2)}ms.`)
30 | }
31 |
32 | this.home = function () {
33 | this.overview()
34 |
35 | if (database.stats.filter.entries > 0) {
36 | // only display if there are entries
37 | if (database.categories) {
38 | // build balance UI
39 | this.logs = new Balance()
40 | this.logs.install(this.logbook)
41 | this.logs.update()
42 | }
43 | if (database.scores) {
44 | // build score graph
45 | this.scores.install(this.logbook)
46 | this.scores.update()
47 | }
48 | // build projects list and tags
49 | this.projects()
50 | this.tags()
51 | }
52 | }
53 |
54 | this.overview = function () {
55 | const overview = document.createElement('section')
56 | overview.id = 'overview'
57 | this.logbook.appendChild(overview)
58 |
59 | const header = document.createElement('header')
60 | header.innerHTML = `Overview `
61 | overview.appendChild(header)
62 |
63 | const stats = database.stats.filter
64 | const statsEl = document.createElement('figure')
65 | statsEl.id = 'stats'
66 | overview.appendChild(statsEl)
67 | statsEl.innerHTML += this.datepickers()
68 | statsEl.innerHTML += `${stats.days}d / ${stats.hours.toFixed(2)}h / ${stats.entries}e `
69 |
70 | if (database.stats.filter.entries > 0) {
71 | const summary = new Summary(overview)
72 | summary.update(database.categories)
73 | } else {
74 | const msg = document.createElement('section')
75 | msg.innerHTML = 'There are no logged entries within the specified dates. '
76 | this.logbook.appendChild(msg)
77 | }
78 | }
79 |
80 | this.subPage = function () {
81 | // OVERVIEW
82 | const overview = document.createElement('section')
83 | overview.id = 'overview'
84 | overview.innerHTML = ``
85 | this.logbook.appendChild(overview)
86 |
87 | this.p = database.projects[page.url]
88 | this.d = database.days[page.url]
89 | // Project or day doesn't exist?
90 | if (!this.p && !this.d) {
91 | overview.innerHTML += `404 `
92 | overview.innerHTML += `The project or date '${page.url}' couldn't be found in the database.
`
93 | return
94 | }
95 |
96 | if (this.d) this.dailyDetails(this.d)
97 | else this.projectDetails()
98 | }
99 |
100 | this.dailyDetails = function (day) {
101 | this.day = day
102 | overview.innerHTML += `${page.url} `
103 | const statsEl = document.createElement('figure')
104 | statsEl.id = 'stats'
105 | overview.appendChild(statsEl)
106 | statsEl.innerHTML += `${dayNamesLong[toTimeStamp(page.url).getDay()]} `
107 | statsEl.innerHTML += `${this.day.trackedHours}h / ${this.day.entries.length}e `
108 |
109 | const summary = new Summary(overview)
110 | summary.update(this.day.categories)
111 | this.timeLine(overview, page.url)
112 |
113 | const dailyScore = new Scores()
114 | dailyScore.install(this.logbook)
115 | dailyScore.update()
116 |
117 | this.projects()
118 | }
119 |
120 | this.projectDetails = function () {
121 | this.project = database.projects[page.url]
122 | const entries = this.project.count
123 | let stats = 'This project has no entries.'
124 | if (entries > 0) {
125 | stats = ``
126 | stats += this.datepickers()
127 | stats += `
128 | ${this.project.hours}h / ${this.project.count}e
129 |
130 | `
131 | }
132 | overview.innerHTML +=
133 | `${this.project.NAME}
134 | ${stats}
135 | `
136 | // ENTRIES
137 | this.logs = new Balance()
138 | this.logs.install(this.logbook)
139 | this.logs.update(page.url)
140 |
141 | // TAGS
142 | this.tags()
143 | }
144 |
145 | this.projects = function () {
146 | this.database = this.isHome ? database.projects : database.days[page.url].projects
147 | const projects = document.createElement('section')
148 | projects.id = 'projects'
149 | this.logbook.appendChild(projects)
150 |
151 | projects.innerHTML = ``
152 |
153 | const categories = document.createElement('figure')
154 | categories.id = 'categories'
155 | projects.appendChild(categories)
156 |
157 | for (const c in database.categories) {
158 | const el = document.createElement('section')
159 | el.id = c.toLowerCase()
160 | el.innerHTML = ``
161 | categories.appendChild(el)
162 | }
163 |
164 | const tmp = []
165 | // create a temp array for sorting projects by hours
166 | for (const id in this.database) {
167 | if (!this.database[id].hours) continue
168 | const project = this.database[id]
169 | project.name = id
170 | tmp.push(project)
171 | }
172 | tmp.sort(this.sortByHours)
173 | // create html from sorted projects
174 | for (const id in tmp) {
175 | const project = tmp[id]
176 | const data = this.database[project.name]
177 | const el = document.createElement('figure')
178 | el.id = project.name
179 | const hours = this.isHome ? database.stats.filter.hours : 24
180 | const percent = (project.hours * 100 / hours).toFixed(1)
181 | el.innerHTML +=
182 | `
183 | ${project.hours.toFixed(2)}h / ${project.count}e / ${percent}% `
184 | const parent = projects.querySelector(`#${(database.projects[project.name].CAT).toLowerCase()}`)
185 | if (parent !== null) parent.appendChild(el)
186 | }
187 | }
188 |
189 | this.tags = function () {
190 | const tagList = this.tagList(this.data)
191 | if (tagList.length < 1) return
192 | const section = document.createElement('section')
193 | section.id = 'tags'
194 | this.logbook.appendChild(section)
195 | section.innerHTML = ``
196 | tags.innerHTML =
197 | `
203 | `
204 | }
205 |
206 | this.tagList = function () {
207 | this.data = this.isHome ? database : database.projects[page.url]
208 |
209 | const tmp = []
210 | // create a temp array for sorting tags by hours
211 | for (const id in this.data.tags) {
212 | const tag = this.data.tags[id]
213 | if (tag.hours) {
214 | tmp.push(tag)
215 | tmp[tmp.length - 1].name = id
216 | }
217 | }
218 |
219 | // const untagged = {}
220 | // if ()
221 | // untagged.name = "untagged"
222 | // untagged.count = this.data.count - this.data.taggedEntries
223 | // untagged.hours = this.data.hours - this.data.taggedHours
224 |
225 | tmp.sort(this.sortByHours)
226 | // if (untagged.count > 0) tmp.push(untagged)
227 |
228 | let tagList = ''
229 | for (const tag in tmp) {
230 | const w = Math.round(tmp[tag].hours * 100 / tmp[0].hours)
231 | const c = this.isHome ? "#FF68A3" : database.categories[this.data.CAT].COLOR
232 | tagList +=
233 | `
234 | ${tmp[tag].name}
235 | ${tmp[tag].hours}h / ${tmp[tag].count}e
236 |
237 |
238 |
239 | `
240 | }
241 | return tagList
242 | }
243 |
244 | this.sortByHours = function (a, b) {
245 | return b.hours - a.hours;
246 | }
247 |
248 | this.datepickers = function () {
249 | let html =
250 | `
251 |
259 | -
260 |
268 | `
269 | return html
270 | }
271 |
272 | this.errors = function () {
273 | // error messages
274 | const overview = document.createElement('section')
275 | overview.id = 'overview'
276 | this.logbook.appendChild(overview)
277 |
278 | const header = document.createElement('header')
279 | header.innerHTML = `Error! `
280 | overview.appendChild(header)
281 |
282 | const errors = document.createElement('figure')
283 | errors.id = 'errors'
284 | overview.appendChild(errors)
285 | for (const e in logbook.errors) errors.innerHTML += `${logbook.errors[e].title} ${logbook.errors[e].text}
`
286 | return
287 | }
288 |
289 | this.timeLine = function (host) {
290 | const date = page.url
291 | const timeLine = document.createElement('section')
292 | timeLine.id = 'timeline'
293 | host.appendChild(timeLine)
294 |
295 | const header = document.createElement('header')
296 | header.innerHTML = `Timeline `
297 | timeLine.appendChild(header)
298 |
299 | const ruler = document.createElement('figure')
300 | ruler.id = 'ruler'
301 |
302 | let h = 0
303 | const width = 100/24
304 | while (h < 24) {
305 | const hour = document.createElement('div')
306 | hour.id = `hour_${h}`
307 | hour.className = 'hour'
308 | hour.style = `width: ${width}%;`
309 | const label = document.createElement('div')
310 | label.id = 'label'
311 | label.innerHTML =`${leadingZero(h)}`
312 | hour.appendChild(label)
313 | ruler.appendChild(hour)
314 | h++
315 | }
316 | timeLine.appendChild(ruler)
317 |
318 | const items = document.createElement('figure')
319 | items.id = 'items'
320 | timeLine.appendChild(items)
321 |
322 | const day = database.days[date]
323 | const entries = day.entries
324 | const now = new Date()
325 | const dayId = toTimeStamp(day.date).getDay()
326 | const hours = now.getMinutes() > 30 ? leadingZero(now.getHours()+1) : leadingZero(now.getHours())
327 | const minutes = now.getMinutes() > 30 ? '00' : '30'
328 |
329 | for (const t in day.timeline) {
330 | const entry = day.timeline[t]
331 | const time = parseInt(entry.time.slice(0,2)) + parseFloat(entry.time.slice(2)) / 60
332 | const x = Math.round(100 / 24 * time)
333 | const w = Math.round(entry.duration * 100 / 24)
334 | const project = entry.project.toUpperCase()
335 | const cat = database.projects[project].CAT
336 | const tag = entry.tags
337 | const duration = entry.duration
338 | const color = database.categories[cat].COLOR
339 | items.innerHTML += `
`
340 | }
341 |
342 | const desc = document.createElement('figure')
343 | desc.id = 'desc'
344 | desc.style = 'width: 100%; display: flex; justify-content: end;'
345 | desc.innerHTML = ` `
346 | timeLine.appendChild(desc)
347 |
348 | // mouse events
349 | items.addEventListener("mouseover", function( event ) {
350 | const el = event.target
351 | if (el.id.slice(0,5) === 'item_') {
352 | el.style.backgroundColor = 'white'
353 | const i = el.id.slice(5)
354 | const entry = day.timeline[i]
355 | desc.innerHTML = `${entry.project}: ${entry.tags} / ${entry.duration}h `
356 | }
357 | })
358 |
359 | items.addEventListener("mouseout", function( event ) {
360 | const el = event.target
361 | if (el.id.slice(0,5) === 'item_') {
362 | el.style.backgroundColor = el.attributes.color.nodeValue
363 | desc.innerHTML = ` `
364 | }
365 | })
366 | }
367 | }
--------------------------------------------------------------------------------