mirror of
https://github.com/Ellpeck/ObsidianSimpleTimeTracker.git
synced 2024-11-16 07:23:12 +01:00
Revert "Add Reporting Summary API and examples"
This reverts commit 40170e44b2
.
This commit is contained in:
parent
40170e44b2
commit
f79e76cb5b
9 changed files with 30 additions and 600 deletions
90
README.md
90
README.md
|
@ -47,96 +47,6 @@ A time tracker is really just a special code block that stores information about
|
||||||
|
|
||||||
The tracker's information is stored in the code block as JSON data. The names, start times and end times of each segment are stored. They're displayed neatly in the code block in preview or reading mode.
|
The tracker's information is stored in the code block as JSON data. The names, start times and end times of each segment are stored. They're displayed neatly in the code block in preview or reading mode.
|
||||||
|
|
||||||
# Tim Tracking Summary / Reporting
|
|
||||||
|
|
||||||
1. **Time Tracking Entries with Tags**: Track work sessions with tags to categorize different activities.
|
|
||||||
|
|
||||||
- Example of an entry: `#tt_dev #tt_client_a #tt_frontend` represents time spent working on the development (frontend) for a specific client.
|
|
||||||
|
|
||||||
2. **Enhanced Reporting Functionality**: Generate time tracking reports for specific time periods, allowing detailed insight into how time was allocated.
|
|
||||||
|
|
||||||
- **Stream-based Reports**: View summaries of time based on specific streams such as Development, Accounting, etc.
|
|
||||||
|
|
||||||
- **Client-based Reports**: Track hours spent working for specific clients.
|
|
||||||
|
|
||||||
|
|
||||||
![alt text](reporting-screenshot.png)
|
|
||||||
|
|
||||||
The output within Obsidian will render detailed information for each time segment, as shown in the first image.
|
|
||||||
|
|
||||||
## Example Report
|
|
||||||
|
|
||||||
Call command `Ctrl+P` select `Insert Time Tracking Summary`
|
|
||||||
|
|
||||||
The reporting capability allows generating summaries for specific time ranges and topics:
|
|
||||||
|
|
||||||
- **Streams Report**: A summary of all topics (e.g., Development, Accounting) over a selected period.
|
|
||||||
|
|
||||||
```
|
|
||||||
time-tracking-summary
|
|
||||||
"2024-11-01", "2024-11-30"
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Clients Report**: A summary for individual topic over a given time range.
|
|
||||||
|
|
||||||
```
|
|
||||||
time-tracking-summary
|
|
||||||
"2024-11-01", "2024-11-30", clients
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
These examples help demonstrate how you can leverage the new tracking and reporting capabilities.
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
1. **Tag Configuration**
|
|
||||||
- Configure your tags, icons, and sub tags using YAML in the settings of the plugin.
|
|
||||||
- Example configuration can be found in the settings:
|
|
||||||
2. **Tag your records with one or more tags / sub tags**
|
|
||||||
3. **Inserting Time Tracking Summary**
|
|
||||||
- Use the newly added command to insert the time tracking summary snippet into a markdown file.
|
|
||||||
- This will generate a report for a given period, optionally filtered by a specific topic.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# You can have as many 'sections' as you want to track different domains separately or in parallel
|
|
||||||
|
|
||||||
# Example secction / topic 1
|
|
||||||
streams:
|
|
||||||
name: "🌊 Streams"
|
|
||||||
items:
|
|
||||||
- topic: "Accounting"
|
|
||||||
icon: "🧮"
|
|
||||||
tag: "#tt_accounting"
|
|
||||||
subTags: []
|
|
||||||
|
|
||||||
- topic: "Development"
|
|
||||||
icon: "💗"
|
|
||||||
tag: "#tt_dev"
|
|
||||||
subTags:
|
|
||||||
- topic: "Frontend"
|
|
||||||
tag: "#tt_frontend"
|
|
||||||
subTags: []
|
|
||||||
|
|
||||||
- topic: "Backend"
|
|
||||||
tag: "#tt_backend"
|
|
||||||
subTags: []
|
|
||||||
|
|
||||||
# Example section / topic 2
|
|
||||||
clients:
|
|
||||||
name: "👨🏼💼 Clients"
|
|
||||||
items:
|
|
||||||
- topic: "Client A"
|
|
||||||
tag: "#tt_client_a"
|
|
||||||
subTags: []
|
|
||||||
|
|
||||||
- topic: "Client B"
|
|
||||||
tag: "#tt_client_b"
|
|
||||||
subTags: []`
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 🙏 Acknowledgements
|
# 🙏 Acknowledgements
|
||||||
If you like this plugin and want to support its development, you can do so through my website by clicking this fancy image!
|
If you like this plugin and want to support its development, you can do so through my website by clicking this fancy image!
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 203 KiB |
|
@ -1,18 +0,0 @@
|
||||||
// interfaces.ts
|
|
||||||
export interface SubTag {
|
|
||||||
topic: string;
|
|
||||||
tag: string;
|
|
||||||
icon?: string;
|
|
||||||
subTags: SubTag[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Item extends SubTag {}
|
|
||||||
|
|
||||||
export interface Section {
|
|
||||||
name: string;
|
|
||||||
items: Item[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Configuration {
|
|
||||||
[sectionKey: string]: Section;
|
|
||||||
}
|
|
113
src/main.ts
113
src/main.ts
|
@ -1,38 +1,18 @@
|
||||||
import { Editor, MarkdownRenderChild, moment, Plugin, TFile } from "obsidian";
|
import { MarkdownRenderChild, Plugin, TFile } from "obsidian";
|
||||||
import { defaultSettings, SimpleTimeTrackerSettings } from "./settings";
|
import { defaultSettings, SimpleTimeTrackerSettings } from "./settings";
|
||||||
import { SimpleTimeTrackerSettingsTab } from "./settings-tab";
|
import { SimpleTimeTrackerSettingsTab } from "./settings-tab";
|
||||||
import {
|
import { displayTracker, Entry, formatDuration, formatTimestamp, getDuration, getRunningEntry, getTotalDuration, isRunning, loadAllTrackers, loadTracker, orderedEntries } from "./tracker";
|
||||||
displayTracker,
|
|
||||||
Entry,
|
|
||||||
formatDuration,
|
|
||||||
formatTimestamp,
|
|
||||||
getDuration,
|
|
||||||
getRunningEntry,
|
|
||||||
getTotalDuration,
|
|
||||||
isRunning,
|
|
||||||
loadAllTrackers,
|
|
||||||
loadTracker,
|
|
||||||
orderedEntries,
|
|
||||||
} from "./tracker";
|
|
||||||
import { TimeTrackingSummary } from "./timeTrackingSummary";
|
|
||||||
|
|
||||||
export default class SimpleTimeTrackerPlugin extends Plugin {
|
export default class SimpleTimeTrackerPlugin extends Plugin {
|
||||||
|
|
||||||
public api = {
|
public api = {
|
||||||
// verbatim versions of the functions found in tracker.ts with the same parameters
|
// verbatim versions of the functions found in tracker.ts with the same parameters
|
||||||
loadTracker,
|
loadTracker, loadAllTrackers, getDuration, getTotalDuration, getRunningEntry, isRunning,
|
||||||
loadAllTrackers,
|
|
||||||
getDuration,
|
|
||||||
getTotalDuration,
|
|
||||||
getRunningEntry,
|
|
||||||
isRunning,
|
|
||||||
|
|
||||||
// modified versions of the functions found in tracker.ts, with the number of required arguments reduced
|
// modified versions of the functions found in tracker.ts, with the number of required arguments reduced
|
||||||
formatTimestamp: (timestamp: string) =>
|
formatTimestamp: (timestamp: string) => formatTimestamp(timestamp, this.settings),
|
||||||
formatTimestamp(timestamp, this.settings),
|
formatDuration: (totalTime: number) => formatDuration(totalTime, this.settings),
|
||||||
formatDuration: (totalTime: number) =>
|
orderedEntries: (entries: Entry[]) => orderedEntries(entries, this.settings)
|
||||||
formatDuration(totalTime, this.settings),
|
|
||||||
orderedEntries: (entries: Entry[]) =>
|
|
||||||
orderedEntries(entries, this.settings),
|
|
||||||
};
|
};
|
||||||
public settings: SimpleTimeTrackerSettings;
|
public settings: SimpleTimeTrackerSettings;
|
||||||
|
|
||||||
|
@ -41,9 +21,7 @@ export default class SimpleTimeTrackerPlugin extends Plugin {
|
||||||
|
|
||||||
this.addSettingTab(new SimpleTimeTrackerSettingsTab(this.app, this));
|
this.addSettingTab(new SimpleTimeTrackerSettingsTab(this.app, this));
|
||||||
|
|
||||||
this.registerMarkdownCodeBlockProcessor(
|
this.registerMarkdownCodeBlockProcessor("simple-time-tracker", (s, e, i) => {
|
||||||
"simple-time-tracker",
|
|
||||||
(s, e, i) => {
|
|
||||||
e.empty();
|
e.empty();
|
||||||
let component = new MarkdownRenderChild(e);
|
let component = new MarkdownRenderChild(e);
|
||||||
let tracker = loadTracker(s);
|
let tracker = loadTracker(s);
|
||||||
|
@ -53,90 +31,27 @@ export default class SimpleTimeTrackerPlugin extends Plugin {
|
||||||
const getFile = () => filePath;
|
const getFile = () => filePath;
|
||||||
|
|
||||||
// Hook rename events to update the file path
|
// Hook rename events to update the file path
|
||||||
component.registerEvent(
|
component.registerEvent(this.app.vault.on("rename", (file, oldPath) => {
|
||||||
this.app.vault.on("rename", (file, oldPath) => {
|
|
||||||
if (file instanceof TFile && oldPath === filePath) {
|
if (file instanceof TFile && oldPath === filePath) {
|
||||||
filePath = file.path;
|
filePath = file.path;
|
||||||
}
|
}
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
displayTracker(
|
displayTracker(tracker, e, getFile, () => i.getSectionInfo(e), this.settings, component);
|
||||||
tracker,
|
|
||||||
e,
|
|
||||||
getFile,
|
|
||||||
() => i.getSectionInfo(e),
|
|
||||||
this.settings,
|
|
||||||
component
|
|
||||||
);
|
|
||||||
i.addChild(component);
|
i.addChild(component);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: `insert`,
|
id: `insert`,
|
||||||
name: `Insert Time Tracker`,
|
name: `Insert Time Tracker`,
|
||||||
editorCallback: (e, _) => {
|
editorCallback: (e, _) => {
|
||||||
e.replaceSelection("```simple-time-tracker\n```\n");
|
e.replaceSelection("```simple-time-tracker\n```\n");
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addCommand({
|
|
||||||
id: `insert-time-tracking-summary`,
|
|
||||||
name: `Insert Time Tracking Summary`,
|
|
||||||
editorCallback: (editor) => this.insertTimeTrackingSummarySummary(editor),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.registerMarkdownCodeBlockProcessor(
|
|
||||||
"time-tracking-summary",
|
|
||||||
async (source, el, ctx) => {
|
|
||||||
const sourceWithoutComments = source
|
|
||||||
.split("\n")[0]
|
|
||||||
.replace(/\/\/.*$/g, ''); // Remove everything after //
|
|
||||||
|
|
||||||
const params = sourceWithoutComments
|
|
||||||
.split(",")
|
|
||||||
.map((s) => s.trim());
|
|
||||||
|
|
||||||
const [startDateStr, endDateStr, streamKey] = params;
|
|
||||||
|
|
||||||
const timeTracking = new TimeTrackingSummary(
|
|
||||||
this.app,
|
|
||||||
this.settings,
|
|
||||||
this.api
|
|
||||||
);
|
|
||||||
await timeTracking.timeTrackingSummaryForPeriod(
|
|
||||||
el,
|
|
||||||
startDateStr,
|
|
||||||
endDateStr,
|
|
||||||
streamKey // Optional parameter
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
insertTimeTrackingSummarySummary(editor: Editor) {
|
|
||||||
const now = moment();
|
|
||||||
// First day of the current month
|
|
||||||
const firstDay = now.clone().startOf('month').format('YYYY-MM-DD');
|
|
||||||
|
|
||||||
// Last day of the current month
|
|
||||||
const lastDay = now.clone().endOf('month').format('YYYY-MM-DD');
|
|
||||||
|
|
||||||
const snippet = `
|
|
||||||
\`\`\`time-tracking-summary
|
|
||||||
"${firstDay}", "${lastDay}" // Optional: add ", stream_name" to filter by streams section. By default will print all.
|
|
||||||
\`\`\``;
|
|
||||||
editor.replaceSelection(snippet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings(): Promise<void> {
|
async loadSettings(): Promise<void> {
|
||||||
this.settings = Object.assign(
|
this.settings = Object.assign({}, defaultSettings, await this.loadData());
|
||||||
{},
|
|
||||||
defaultSettings,
|
|
||||||
await this.loadData()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings(): Promise<void> {
|
async saveSettings(): Promise<void> {
|
||||||
|
|
|
@ -74,22 +74,6 @@ export class SimpleTimeTrackerSettingsTab extends PluginSettingTab {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(this.containerEl)
|
|
||||||
.setName("Tag Configurations")
|
|
||||||
.setDesc("Configure your tags, icons, and sub-tags using YAML.")
|
|
||||||
.addTextArea((text) => {
|
|
||||||
text
|
|
||||||
.setPlaceholder("Enter tag configurations in YAML format")
|
|
||||||
.setValue(this.plugin.settings.tagConfigurationsYaml)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.tagConfigurationsYaml = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
});
|
|
||||||
text.inputEl.rows = 15;
|
|
||||||
text.inputEl.style.width = "100%";
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
this.containerEl.createEl("hr");
|
this.containerEl.createEl("hr");
|
||||||
this.containerEl.createEl("p", { text: "Need help using the plugin? Feel free to join the Discord server!" });
|
this.containerEl.createEl("p", { text: "Need help using the plugin? Feel free to join the Discord server!" });
|
||||||
this.containerEl.createEl("a", { href: "https://link.ellpeck.de/discordweb" }).createEl("img", {
|
this.containerEl.createEl("a", { href: "https://link.ellpeck.de/discordweb" }).createEl("img", {
|
||||||
|
|
|
@ -4,52 +4,16 @@ export const defaultSettings: SimpleTimeTrackerSettings = {
|
||||||
csvDelimiter: ",",
|
csvDelimiter: ",",
|
||||||
fineGrainedDurations: true,
|
fineGrainedDurations: true,
|
||||||
reverseSegmentOrder: false,
|
reverseSegmentOrder: false,
|
||||||
timestampDurations: false,
|
timestampDurations: false
|
||||||
tagConfigurationsYaml: `
|
|
||||||
# This is a sample configuration file for the tag configurations
|
|
||||||
|
|
||||||
# You can have as many 'sections' as you want to track different domains separately or in parallel
|
|
||||||
|
|
||||||
# Example section 1
|
|
||||||
streams:
|
|
||||||
name: "🌊 Streams"
|
|
||||||
items:
|
|
||||||
- topic: "Accounting"
|
|
||||||
icon: "🧮"
|
|
||||||
tag: "#tt_accounting"
|
|
||||||
subTags: []
|
|
||||||
|
|
||||||
- topic: "Development"
|
|
||||||
icon: "💗"
|
|
||||||
tag: "#tt_dev"
|
|
||||||
subTags:
|
|
||||||
- topic: "Frontend"
|
|
||||||
tag: "#tt_frontend"
|
|
||||||
subTags: []
|
|
||||||
|
|
||||||
- topic: "Backend"
|
|
||||||
tag: "#tt_backend"
|
|
||||||
subTags: []
|
|
||||||
|
|
||||||
# Example section 2
|
|
||||||
clients:
|
|
||||||
name: "👨🏼💼 Clients"
|
|
||||||
items:
|
|
||||||
- topic: "Client A"
|
|
||||||
tag: "#tt_client_a"
|
|
||||||
subTags: []
|
|
||||||
|
|
||||||
- topic: "Client B"
|
|
||||||
tag: "#tt_client_b"
|
|
||||||
subTags: []`
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SimpleTimeTrackerSettings {
|
export interface SimpleTimeTrackerSettings {
|
||||||
|
|
||||||
timestampFormat: string;
|
timestampFormat: string;
|
||||||
editableTimestampFormat: string;
|
editableTimestampFormat: string;
|
||||||
csvDelimiter: string;
|
csvDelimiter: string;
|
||||||
fineGrainedDurations: boolean;
|
fineGrainedDurations: boolean;
|
||||||
reverseSegmentOrder: boolean;
|
reverseSegmentOrder: boolean;
|
||||||
timestampDurations: boolean;
|
timestampDurations: boolean;
|
||||||
tagConfigurationsYaml: string;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,308 +0,0 @@
|
||||||
import { SimpleTimeTrackerSettings } from "./settings";
|
|
||||||
import { Configuration, Section, Item, SubTag } from "./interfaces";
|
|
||||||
import { parseYaml, moment } from "obsidian";
|
|
||||||
|
|
||||||
|
|
||||||
export class TimeTrackingSummary {
|
|
||||||
settings: SimpleTimeTrackerSettings;
|
|
||||||
api: any;
|
|
||||||
app: any; // Reference to the Obsidian app
|
|
||||||
|
|
||||||
constructor(app: any, settings: SimpleTimeTrackerSettings, api: any) {
|
|
||||||
this.app = app;
|
|
||||||
this.settings = settings;
|
|
||||||
this.api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
async timeTrackingSummaryForPeriod(
|
|
||||||
containerEl: HTMLElement,
|
|
||||||
startDateStr: string,
|
|
||||||
endDateStr: string,
|
|
||||||
streamKey?: string
|
|
||||||
) {
|
|
||||||
// Define the time interval (inclusive)
|
|
||||||
const startDate = moment(startDateStr).startOf("day");
|
|
||||||
const endDate = moment(endDateStr).endOf("day");
|
|
||||||
|
|
||||||
// Initialize an object to hold total durations per dimension
|
|
||||||
let dimensionDurations: {
|
|
||||||
[dimension: string]: { [tag: string]: number };
|
|
||||||
} = {};
|
|
||||||
const untrackedSectionName = "Other";
|
|
||||||
|
|
||||||
// Parse the configuration from settings
|
|
||||||
let config: Configuration;
|
|
||||||
try {
|
|
||||||
config = this.parseConfiguration();
|
|
||||||
} catch (error: any) {
|
|
||||||
containerEl.createEl("p", {
|
|
||||||
text: `Error parsing configuration: ${error.message}`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = this.api;
|
|
||||||
|
|
||||||
// Load all trackers from all markdown files
|
|
||||||
const files = this.app.vault.getMarkdownFiles();
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const trackers = await api.loadAllTrackers(file.path);
|
|
||||||
|
|
||||||
for (let { tracker } of trackers) {
|
|
||||||
for (let entry of tracker.entries) {
|
|
||||||
let entryDate = moment(entry.startTime);
|
|
||||||
if (
|
|
||||||
!entryDate.isValid() ||
|
|
||||||
!entryDate.isBetween(startDate, endDate, null, "[]")
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tags = entry.name.match(/#\w+/g) || [];
|
|
||||||
let duration = api.getDuration(entry); // Use API duration calculation
|
|
||||||
duration = duration / (1000 * 60 * 60); // Convert ms to hours
|
|
||||||
|
|
||||||
let mappedDimensions = this.mapTagsToDimensions(
|
|
||||||
tags,
|
|
||||||
config
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Object.keys(mappedDimensions).length === 0) {
|
|
||||||
// Handle untracked time
|
|
||||||
if (!dimensionDurations[untrackedSectionName]) {
|
|
||||||
dimensionDurations[untrackedSectionName] = {};
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!dimensionDurations[untrackedSectionName][
|
|
||||||
untrackedSectionName
|
|
||||||
]
|
|
||||||
) {
|
|
||||||
dimensionDurations[untrackedSectionName][
|
|
||||||
untrackedSectionName
|
|
||||||
] = 0;
|
|
||||||
}
|
|
||||||
dimensionDurations[untrackedSectionName][
|
|
||||||
untrackedSectionName
|
|
||||||
] += duration;
|
|
||||||
} else {
|
|
||||||
// Accumulate tracked time
|
|
||||||
for (let dimension in mappedDimensions) {
|
|
||||||
if (!dimensionDurations[dimension]) {
|
|
||||||
dimensionDurations[dimension] = {};
|
|
||||||
}
|
|
||||||
for (let topic of mappedDimensions[dimension]) {
|
|
||||||
if (!dimensionDurations[dimension][topic]) {
|
|
||||||
dimensionDurations[dimension][topic] = 0;
|
|
||||||
}
|
|
||||||
dimensionDurations[dimension][topic] +=
|
|
||||||
duration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
streamKey = streamKey?.replace(/^["']|["']$/g, '').trim().toLowerCase() // remove any quotes and trim
|
|
||||||
|
|
||||||
|
|
||||||
if (streamKey && streamKey.trim() !== "") {
|
|
||||||
console.log(`streamKey: ${streamKey} is not empty, proceeding to filter dimensions`);
|
|
||||||
|
|
||||||
// Create mapping of section keys and names to dimensions
|
|
||||||
const sectionMapping: { [key: string]: string } = {};
|
|
||||||
for (const [key, section] of Object.entries(config)) {
|
|
||||||
sectionMapping[key.toLowerCase()] = section.name;
|
|
||||||
sectionMapping[section.name.toLowerCase()] = section.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find matching section
|
|
||||||
const filteredDimensions: typeof dimensionDurations = {};
|
|
||||||
for (const dimension in dimensionDurations) {
|
|
||||||
console.log(`dimension: ${dimension}, checking match with ${streamKey}`);
|
|
||||||
// Check if streamKey matches either section key or display name
|
|
||||||
if (dimension.toLowerCase() === streamKey ||
|
|
||||||
Object.keys(sectionMapping).includes(streamKey)) {
|
|
||||||
const matchedDimension = sectionMapping[streamKey] || dimension;
|
|
||||||
if (dimensionDurations[matchedDimension]) {
|
|
||||||
filteredDimensions[matchedDimension] = dimensionDurations[matchedDimension];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show message if no matching stream found
|
|
||||||
if (Object.keys(filteredDimensions).length === 0) {
|
|
||||||
containerEl.createEl("p", {
|
|
||||||
text: `No results found for stream "${streamKey}" in the selected period [${startDate.format(
|
|
||||||
"YYYY-MM-DD"
|
|
||||||
)} → ${endDate.format("YYYY-MM-DD")}]`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace original dimensions with filtered
|
|
||||||
dimensionDurations = filteredDimensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Display the results
|
|
||||||
for (let dimension in dimensionDurations) {
|
|
||||||
console.log(`dimension: ${dimension}`);
|
|
||||||
let totalDuration = Object.values(
|
|
||||||
dimensionDurations[dimension]
|
|
||||||
).reduce((sum, hours) => sum + hours, 0);
|
|
||||||
let totalMs = totalDuration * 60 * 60 * 1000; // Convert hours to ms
|
|
||||||
let formattedTotalDuration = api.formatDuration(totalMs);
|
|
||||||
|
|
||||||
containerEl.createEl("h2", {
|
|
||||||
text: `${dimension} 📅 [${startDate.format(
|
|
||||||
"YYYY-MM-DD"
|
|
||||||
)} → ${endDate.format(
|
|
||||||
"YYYY-MM-DD"
|
|
||||||
)}] ⏳Total: ${formattedTotalDuration}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
let tableEl = containerEl.createEl("table");
|
|
||||||
let headerRow = tableEl.createEl("tr");
|
|
||||||
["Icon", "Topic", "Total Hours", "Formatted Duration"].forEach(
|
|
||||||
(text) => {
|
|
||||||
headerRow.createEl("th", { text });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let tableData = Object.entries(dimensionDurations[dimension]).map(
|
|
||||||
([topic, hours]) => {
|
|
||||||
let durationMs = hours * 60 * 60 * 1000;
|
|
||||||
let formattedHours = api.formatDuration(durationMs);
|
|
||||||
let icon = this.getIconForTag(topic, config);
|
|
||||||
return {
|
|
||||||
icon,
|
|
||||||
topic,
|
|
||||||
hours: hours.toFixed(2),
|
|
||||||
formattedHours,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort table data by time spent (hours) in descending order
|
|
||||||
tableData.sort((a, b) => parseFloat(b.hours) - parseFloat(a.hours));
|
|
||||||
|
|
||||||
tableData.forEach((row) => {
|
|
||||||
let rowEl = tableEl.createEl("tr");
|
|
||||||
["icon", "topic", "hours", "formattedHours"].forEach((key) => {
|
|
||||||
rowEl.createEl("td", { text: (row as any)[key] });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseConfiguration(): Configuration {
|
|
||||||
const yamlContent = this.settings.tagConfigurationsYaml;
|
|
||||||
console.log(`received config:\n ${yamlContent}`);
|
|
||||||
const config = parseYaml(yamlContent);
|
|
||||||
|
|
||||||
console.log("Parsed configuration:", config);
|
|
||||||
|
|
||||||
if (!config || typeof config !== "object") {
|
|
||||||
throw new Error("Invalid configuration format.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate and transform the configuration
|
|
||||||
for (const [sectionKey, sectionValue] of Object.entries(config)) {
|
|
||||||
console.log(`sectionKey: ${sectionKey}`);
|
|
||||||
if (
|
|
||||||
!sectionValue.hasOwnProperty("name") ||
|
|
||||||
!sectionValue.hasOwnProperty("items")
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Section ${sectionKey} is missing 'name' or 'items' properties.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively validate items and subTags
|
|
||||||
const validateItems = (items: any[]) => {
|
|
||||||
console.log(`items: ${items}`);
|
|
||||||
for (const item of items) {
|
|
||||||
if (
|
|
||||||
!item.hasOwnProperty("topic") ||
|
|
||||||
!item.hasOwnProperty("tag")
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Item ${JSON.stringify(
|
|
||||||
item
|
|
||||||
)} is missing 'topic' or 'tag' properties.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
item.subTags = item.subTags || [];
|
|
||||||
validateItems(item.subTags);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
validateItems((sectionValue as Section).items);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config as Configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
mapTagsToDimensions(
|
|
||||||
tags: string[],
|
|
||||||
config: Configuration
|
|
||||||
): { [dimension: string]: string[] } {
|
|
||||||
const mappedDimensions: { [dimension: string]: string[] } = {};
|
|
||||||
|
|
||||||
const processItems = (
|
|
||||||
items: (Item | SubTag)[],
|
|
||||||
dimensionName: string,
|
|
||||||
tagsInDimension: Set<string>
|
|
||||||
) => {
|
|
||||||
for (const item of items) {
|
|
||||||
if (tags.includes(item.tag)) {
|
|
||||||
tagsInDimension.add(item.topic);
|
|
||||||
}
|
|
||||||
if (item.subTags && item.subTags.length > 0) {
|
|
||||||
processItems(item.subTags, dimensionName, tagsInDimension);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const section of Object.values(config)) {
|
|
||||||
const dimensionName = section.name;
|
|
||||||
const tagsInDimension: Set<string> = new Set();
|
|
||||||
processItems(section.items, dimensionName, tagsInDimension);
|
|
||||||
if (tagsInDimension.size > 0) {
|
|
||||||
mappedDimensions[dimensionName] = Array.from(tagsInDimension);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappedDimensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
getIconForTag(topic: string, config: Configuration): string {
|
|
||||||
let foundIcon = "";
|
|
||||||
|
|
||||||
const searchItems = (items: (Item | SubTag)[]): boolean => {
|
|
||||||
for (const item of items) {
|
|
||||||
if (item.topic === topic) {
|
|
||||||
foundIcon = item.icon || "";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (item.subTags && item.subTags.length > 0) {
|
|
||||||
if (searchItems(item.subTags)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const section of Object.values(config)) {
|
|
||||||
if (searchItems(section.items)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundIcon;
|
|
||||||
}
|
|
||||||
}
|
|
4
test-vault/.obsidian/community-plugins.json
vendored
4
test-vault/.obsidian/community-plugins.json
vendored
|
@ -1,4 +1,4 @@
|
||||||
[
|
[
|
||||||
"dataview",
|
"simple-time-tracker",
|
||||||
"simple-time-tracker"
|
"dataview"
|
||||||
]
|
]
|
|
@ -1,17 +0,0 @@
|
||||||
|
|
||||||
# Example data
|
|
||||||
```simple-time-tracker
|
|
||||||
{"entries":[{"name":"#tt_accounting #tt_client_a bills processing","startTime":"2024-11-01T09:25:42.000Z","endTime":"2024-11-01T17:25:56.000Z"},{"name":"#tt_dev #tt_client2 ","startTime":"2024-11-02T13:26:21.000Z","endTime":"2024-11-02T17:27:18.000Z"},{"name":"#tt_dev #tt_client_a #tt_frontend did this","startTime":"2024-11-03T08:27:20.000Z","endTime":"2024-11-03T17:27:39.000Z"},{"name":"#tt_dev #tt_client_b #tt_backend did that","startTime":"2024-11-04T14:27:40.000Z","endTime":"2024-11-04T17:27:53.000Z"}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Example report
|
|
||||||
## Report only one stream
|
|
||||||
|
|
||||||
```time-tracking-summary
|
|
||||||
"2024-11-01", "2024-11-30" // Optional: add ", stream_name" to filter by streams section. By default will print all.
|
|
||||||
```
|
|
||||||
## Report only one stream
|
|
||||||
|
|
||||||
```time-tracking-summary
|
|
||||||
"2024-11-01", "2024-11-30", clients // Optional: add ", stream_name" to filter by streams section. By default will print all.
|
|
||||||
```
|
|
Loading…
Reference in a new issue