mirror of
https://github.com/Ellpeck/ObsidianSimpleTimeTracker.git
synced 2024-11-16 07:23:12 +01:00
Add Reporting Summary API and examples
This commit is contained in:
parent
c478ce6123
commit
40170e44b2
9 changed files with 600 additions and 30 deletions
90
README.md
90
README.md
|
@ -47,6 +47,96 @@ 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.
|
||||
|
||||
# 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
|
||||
If you like this plugin and want to support its development, you can do so through my website by clicking this fancy image!
|
||||
|
||||
|
|
BIN
reporting-screenshot.png
Normal file
BIN
reporting-screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 203 KiB |
18
src/interfaces.ts
Normal file
18
src/interfaces.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
// 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;
|
||||
}
|
135
src/main.ts
135
src/main.ts
|
@ -1,18 +1,38 @@
|
|||
import { MarkdownRenderChild, Plugin, TFile } from "obsidian";
|
||||
import { Editor, MarkdownRenderChild, moment, Plugin, TFile } from "obsidian";
|
||||
import { defaultSettings, SimpleTimeTrackerSettings } from "./settings";
|
||||
import { SimpleTimeTrackerSettingsTab } from "./settings-tab";
|
||||
import { displayTracker, Entry, formatDuration, formatTimestamp, getDuration, getRunningEntry, getTotalDuration, isRunning, loadAllTrackers, loadTracker, orderedEntries } from "./tracker";
|
||||
import {
|
||||
displayTracker,
|
||||
Entry,
|
||||
formatDuration,
|
||||
formatTimestamp,
|
||||
getDuration,
|
||||
getRunningEntry,
|
||||
getTotalDuration,
|
||||
isRunning,
|
||||
loadAllTrackers,
|
||||
loadTracker,
|
||||
orderedEntries,
|
||||
} from "./tracker";
|
||||
import { TimeTrackingSummary } from "./timeTrackingSummary";
|
||||
|
||||
export default class SimpleTimeTrackerPlugin extends Plugin {
|
||||
|
||||
public api = {
|
||||
// verbatim versions of the functions found in tracker.ts with the same parameters
|
||||
loadTracker, loadAllTrackers, getDuration, getTotalDuration, getRunningEntry, isRunning,
|
||||
loadTracker,
|
||||
loadAllTrackers,
|
||||
getDuration,
|
||||
getTotalDuration,
|
||||
getRunningEntry,
|
||||
isRunning,
|
||||
|
||||
// modified versions of the functions found in tracker.ts, with the number of required arguments reduced
|
||||
formatTimestamp: (timestamp: string) => formatTimestamp(timestamp, this.settings),
|
||||
formatDuration: (totalTime: number) => formatDuration(totalTime, this.settings),
|
||||
orderedEntries: (entries: Entry[]) => orderedEntries(entries, this.settings)
|
||||
formatTimestamp: (timestamp: string) =>
|
||||
formatTimestamp(timestamp, this.settings),
|
||||
formatDuration: (totalTime: number) =>
|
||||
formatDuration(totalTime, this.settings),
|
||||
orderedEntries: (entries: Entry[]) =>
|
||||
orderedEntries(entries, this.settings),
|
||||
};
|
||||
public settings: SimpleTimeTrackerSettings;
|
||||
|
||||
|
@ -21,37 +41,102 @@ export default class SimpleTimeTrackerPlugin extends Plugin {
|
|||
|
||||
this.addSettingTab(new SimpleTimeTrackerSettingsTab(this.app, this));
|
||||
|
||||
this.registerMarkdownCodeBlockProcessor("simple-time-tracker", (s, e, i) => {
|
||||
e.empty();
|
||||
let component = new MarkdownRenderChild(e);
|
||||
let tracker = loadTracker(s);
|
||||
this.registerMarkdownCodeBlockProcessor(
|
||||
"simple-time-tracker",
|
||||
(s, e, i) => {
|
||||
e.empty();
|
||||
let component = new MarkdownRenderChild(e);
|
||||
let tracker = loadTracker(s);
|
||||
|
||||
// Wrap file name in a function since it can change
|
||||
let filePath = i.sourcePath;
|
||||
const getFile = () => filePath;
|
||||
// Wrap file name in a function since it can change
|
||||
let filePath = i.sourcePath;
|
||||
const getFile = () => filePath;
|
||||
|
||||
// Hook rename events to update the file path
|
||||
component.registerEvent(this.app.vault.on("rename", (file, oldPath) => {
|
||||
if (file instanceof TFile && oldPath === filePath) {
|
||||
filePath = file.path;
|
||||
}
|
||||
}));
|
||||
// Hook rename events to update the file path
|
||||
component.registerEvent(
|
||||
this.app.vault.on("rename", (file, oldPath) => {
|
||||
if (file instanceof TFile && oldPath === filePath) {
|
||||
filePath = file.path;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
displayTracker(tracker, e, getFile, () => i.getSectionInfo(e), this.settings, component);
|
||||
i.addChild(component);
|
||||
});
|
||||
displayTracker(
|
||||
tracker,
|
||||
e,
|
||||
getFile,
|
||||
() => i.getSectionInfo(e),
|
||||
this.settings,
|
||||
component
|
||||
);
|
||||
i.addChild(component);
|
||||
}
|
||||
);
|
||||
|
||||
this.addCommand({
|
||||
id: `insert`,
|
||||
name: `Insert Time Tracker`,
|
||||
editorCallback: (e, _) => {
|
||||
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> {
|
||||
this.settings = Object.assign({}, defaultSettings, await this.loadData());
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
defaultSettings,
|
||||
await this.loadData()
|
||||
);
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
|
|
|
@ -74,6 +74,22 @@ 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("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", {
|
||||
|
|
|
@ -4,16 +4,52 @@ export const defaultSettings: SimpleTimeTrackerSettings = {
|
|||
csvDelimiter: ",",
|
||||
fineGrainedDurations: true,
|
||||
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 {
|
||||
|
||||
timestampFormat: string;
|
||||
editableTimestampFormat: string;
|
||||
csvDelimiter: string;
|
||||
fineGrainedDurations: boolean;
|
||||
reverseSegmentOrder: boolean;
|
||||
timestampDurations: boolean;
|
||||
|
||||
tagConfigurationsYaml: string;
|
||||
}
|
||||
|
|
308
src/timeTrackingSummary.ts
Normal file
308
src/timeTrackingSummary.ts
Normal file
|
@ -0,0 +1,308 @@
|
|||
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 @@
|
|||
[
|
||||
"simple-time-tracker",
|
||||
"dataview"
|
||||
"dataview",
|
||||
"simple-time-tracker"
|
||||
]
|
17
test-vault/Time summary reporting test.md
Normal file
17
test-vault/Time summary reporting test.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
# 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