-
-
Notifications
You must be signed in to change notification settings - Fork 138
feat: added workreport view #742
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,343 @@ | ||||||||||||||||||||||||||||||||||||||
| <template lang="pug"> | ||||||||||||||||||||||||||||||||||||||
| div | ||||||||||||||||||||||||||||||||||||||
| h3.mb-3 Work Time Report | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| div.row.mb-4 | ||||||||||||||||||||||||||||||||||||||
| div.col-md-3 | ||||||||||||||||||||||||||||||||||||||
| b-form-group(label="Hosts" label-class="font-weight-bold") | ||||||||||||||||||||||||||||||||||||||
| b-form-select(v-model="selectedHosts" :options="hostOptions" multiple :select-size="4") | ||||||||||||||||||||||||||||||||||||||
| small.text-muted Select devices to include | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| div.col-md-3 | ||||||||||||||||||||||||||||||||||||||
| b-form-group(label="Categories" label-class="font-weight-bold") | ||||||||||||||||||||||||||||||||||||||
| b-form-select(v-model="selectedCategories" :options="categoryOptions" multiple :select-size="3") | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| div.col-md-3 | ||||||||||||||||||||||||||||||||||||||
| b-form-group(label="Break Time" label-class="font-weight-bold") | ||||||||||||||||||||||||||||||||||||||
| div.d-flex.align-items-center | ||||||||||||||||||||||||||||||||||||||
| b-form-input( | ||||||||||||||||||||||||||||||||||||||
| v-model="breakTime" | ||||||||||||||||||||||||||||||||||||||
| type="range" | ||||||||||||||||||||||||||||||||||||||
| min="0" | ||||||||||||||||||||||||||||||||||||||
| max="30" | ||||||||||||||||||||||||||||||||||||||
| step="1" | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| span.ml-2.text-nowrap {{ breakTime }} min | ||||||||||||||||||||||||||||||||||||||
| small.text-muted Gaps shorter than this will be counted as work time | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| div.col-md-3 | ||||||||||||||||||||||||||||||||||||||
| b-form-group(label="Date Range" label-class="font-weight-bold") | ||||||||||||||||||||||||||||||||||||||
| b-form-select(v-model="dateRange" :options="dateRangeOptions") | ||||||||||||||||||||||||||||||||||||||
| small.text-muted(v-if="dateRange === 'custom'") | ||||||||||||||||||||||||||||||||||||||
| | Custom range: {{ customStart }} to {{ customEnd }} | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| div.mb-3 | ||||||||||||||||||||||||||||||||||||||
| b-button(@click="loadData" variant="primary") | ||||||||||||||||||||||||||||||||||||||
| icon(name="sync") | ||||||||||||||||||||||||||||||||||||||
| | Calculate Work Time | ||||||||||||||||||||||||||||||||||||||
| b-button.ml-2(@click="exportCSV" variant="outline-secondary" :disabled="!hasData") | ||||||||||||||||||||||||||||||||||||||
| icon(name="download") | ||||||||||||||||||||||||||||||||||||||
| | Export CSV | ||||||||||||||||||||||||||||||||||||||
| b-button.ml-2(@click="exportJSON" variant="outline-secondary" :disabled="!hasData") | ||||||||||||||||||||||||||||||||||||||
| icon(name="download") | ||||||||||||||||||||||||||||||||||||||
| | Export JSON | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| div(v-if="loading") | ||||||||||||||||||||||||||||||||||||||
| b-spinner.mr-2 | ||||||||||||||||||||||||||||||||||||||
| | Loading... | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| div(v-if="hasData && !loading") | ||||||||||||||||||||||||||||||||||||||
| h5.mt-4 Daily Breakdown | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| table.table.table-sm.table-hover | ||||||||||||||||||||||||||||||||||||||
| thead | ||||||||||||||||||||||||||||||||||||||
| tr | ||||||||||||||||||||||||||||||||||||||
| th Date | ||||||||||||||||||||||||||||||||||||||
| th.text-right Work Time | ||||||||||||||||||||||||||||||||||||||
| th.text-right Sessions | ||||||||||||||||||||||||||||||||||||||
| th.text-right Avg Session | ||||||||||||||||||||||||||||||||||||||
| tbody | ||||||||||||||||||||||||||||||||||||||
| tr(v-for="day in dailyData" :key="day.date") | ||||||||||||||||||||||||||||||||||||||
| td {{ day.date }} | ||||||||||||||||||||||||||||||||||||||
| td.text-right {{ formatDuration(day.duration) }} | ||||||||||||||||||||||||||||||||||||||
| td.text-right {{ day.sessions }} | ||||||||||||||||||||||||||||||||||||||
| td.text-right {{ formatDuration(day.avgSession) }} | ||||||||||||||||||||||||||||||||||||||
| tfoot | ||||||||||||||||||||||||||||||||||||||
| tr.font-weight-bold | ||||||||||||||||||||||||||||||||||||||
| td Total | ||||||||||||||||||||||||||||||||||||||
| td.text-right {{ formatDuration(totalDuration) }} | ||||||||||||||||||||||||||||||||||||||
| td.text-right {{ totalSessions }} | ||||||||||||||||||||||||||||||||||||||
| td.text-right {{ formatDuration(avgSessionLength) }} | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| </template> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| <script lang="ts"> | ||||||||||||||||||||||||||||||||||||||
| import moment from 'moment'; | ||||||||||||||||||||||||||||||||||||||
| import { getClient } from '~/util/awclient'; | ||||||||||||||||||||||||||||||||||||||
| import { useCategoryStore } from '~/stores/categories'; | ||||||||||||||||||||||||||||||||||||||
| import { useSettingsStore } from '~/stores/settings'; | ||||||||||||||||||||||||||||||||||||||
| import { useBucketsStore } from '~/stores/buckets'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import 'vue-awesome/icons/sync'; | ||||||||||||||||||||||||||||||||||||||
| import 'vue-awesome/icons/download'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| interface DailyData { | ||||||||||||||||||||||||||||||||||||||
| date: string; | ||||||||||||||||||||||||||||||||||||||
| duration: number; | ||||||||||||||||||||||||||||||||||||||
| sessions: number; | ||||||||||||||||||||||||||||||||||||||
| avgSession: number; | ||||||||||||||||||||||||||||||||||||||
| events: any[]; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export default { | ||||||||||||||||||||||||||||||||||||||
| name: 'WorkReport', | ||||||||||||||||||||||||||||||||||||||
| data() { | ||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||
| categoryStore: useCategoryStore(), | ||||||||||||||||||||||||||||||||||||||
| settingsStore: useSettingsStore(), | ||||||||||||||||||||||||||||||||||||||
| bucketsStore: useBucketsStore(), | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| selectedHosts: [], // Will be populated on mount | ||||||||||||||||||||||||||||||||||||||
| selectedCategories: [JSON.stringify(['Work'])], // Default to 'Work' category | ||||||||||||||||||||||||||||||||||||||
| breakTime: 5, | ||||||||||||||||||||||||||||||||||||||
| dateRange: 'last7d', | ||||||||||||||||||||||||||||||||||||||
| customStart: null, | ||||||||||||||||||||||||||||||||||||||
| customEnd: null, | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| loading: false, | ||||||||||||||||||||||||||||||||||||||
| dailyData: [] as DailyData[], | ||||||||||||||||||||||||||||||||||||||
| rawData: {}, | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| computed: { | ||||||||||||||||||||||||||||||||||||||
| hostOptions() { | ||||||||||||||||||||||||||||||||||||||
| // Get available hosts from window watcher buckets | ||||||||||||||||||||||||||||||||||||||
| const allBuckets = this.bucketsStore.buckets || []; | ||||||||||||||||||||||||||||||||||||||
| const windowBuckets = allBuckets.filter(b => b.type === 'currentwindow'); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const hosts = windowBuckets.map(b => { | ||||||||||||||||||||||||||||||||||||||
| // Extract hostname from bucket ID (format: aw-watcher-window_hostname) | ||||||||||||||||||||||||||||||||||||||
| return b.id.replace('aw-watcher-window_', ''); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return hosts.map(host => ({ | ||||||||||||||||||||||||||||||||||||||
| value: host, | ||||||||||||||||||||||||||||||||||||||
| text: host, | ||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| categoryOptions() { | ||||||||||||||||||||||||||||||||||||||
| // Get all categories (not just those with rules) for the filter | ||||||||||||||||||||||||||||||||||||||
| const cats = this.categoryStore.all_categories || []; | ||||||||||||||||||||||||||||||||||||||
| return cats.map(cat => ({ | ||||||||||||||||||||||||||||||||||||||
| value: JSON.stringify(cat), // Store as JSON string to preserve array structure | ||||||||||||||||||||||||||||||||||||||
| text: cat.join(' > '), | ||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| dateRangeOptions() { | ||||||||||||||||||||||||||||||||||||||
| return [ | ||||||||||||||||||||||||||||||||||||||
| { value: 'last7d', text: 'Last 7 days' }, | ||||||||||||||||||||||||||||||||||||||
| { value: 'last30d', text: 'Last 30 days' }, | ||||||||||||||||||||||||||||||||||||||
| { value: 'thisWeek', text: 'This week' }, | ||||||||||||||||||||||||||||||||||||||
| { value: 'thisMonth', text: 'This month' }, | ||||||||||||||||||||||||||||||||||||||
| { value: 'custom', text: 'Custom range' }, | ||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| hasData() { | ||||||||||||||||||||||||||||||||||||||
| return this.dailyData.length > 0; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| totalDuration() { | ||||||||||||||||||||||||||||||||||||||
| return this.dailyData.reduce((sum, day) => sum + day.duration, 0); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| totalSessions() { | ||||||||||||||||||||||||||||||||||||||
| return this.dailyData.reduce((sum, day) => sum + day.sessions, 0); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| avgSessionLength() { | ||||||||||||||||||||||||||||||||||||||
| return this.totalSessions > 0 ? this.totalDuration / this.totalSessions : 0; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| async mounted() { | ||||||||||||||||||||||||||||||||||||||
| this.categoryStore.load(); | ||||||||||||||||||||||||||||||||||||||
| await this.bucketsStore.ensureLoaded(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Auto-select all available hosts | ||||||||||||||||||||||||||||||||||||||
| if (this.hostOptions.length > 0) { | ||||||||||||||||||||||||||||||||||||||
| this.selectedHosts = this.hostOptions.map(opt => opt.value); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| methods: { | ||||||||||||||||||||||||||||||||||||||
| async loadData() { | ||||||||||||||||||||||||||||||||||||||
| this.loading = true; | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| const client = getClient(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (this.selectedHosts.length === 0) { | ||||||||||||||||||||||||||||||||||||||
| alert('Please select at least one host'); | ||||||||||||||||||||||||||||||||||||||
| this.loading = false; | ||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Get date range | ||||||||||||||||||||||||||||||||||||||
| const timeperiods = this.getTimeperiods(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Build query with flooding | ||||||||||||||||||||||||||||||||||||||
| const breakTimeSeconds = this.breakTime * 60; | ||||||||||||||||||||||||||||||||||||||
| // Parse categories back from JSON strings | ||||||||||||||||||||||||||||||||||||||
| const categoriesFilter = this.selectedCategories.map(c => JSON.parse(c)); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Get categories for query | ||||||||||||||||||||||||||||||||||||||
| const categories = this.categoryStore.classes_for_query; | ||||||||||||||||||||||||||||||||||||||
| const categoriesStr = JSON.stringify(categories).replace(/\\\\/g, '\\'); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Build multi-device query with custom flooding | ||||||||||||||||||||||||||||||||||||||
| let query = ''; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Query each host with custom flooding | ||||||||||||||||||||||||||||||||||||||
| for (const hostname of this.selectedHosts) { | ||||||||||||||||||||||||||||||||||||||
| const safeHost = hostname.replace(/[^a-zA-Z0-9_]/g, ''); | ||||||||||||||||||||||||||||||||||||||
| query += ` | ||||||||||||||||||||||||||||||||||||||
| events_${safeHost} = flood(query_bucket(find_bucket("aw-watcher-window_${hostname}")), ${breakTimeSeconds}); | ||||||||||||||||||||||||||||||||||||||
| events_${safeHost} = categorize(events_${safeHost}, ${categoriesStr}); | ||||||||||||||||||||||||||||||||||||||
| events_${safeHost} = filter_keyvals(events_${safeHost}, "$category", ${JSON.stringify( | ||||||||||||||||||||||||||||||||||||||
| categoriesFilter | ||||||||||||||||||||||||||||||||||||||
| )}); | ||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Combine events from all hosts using union_no_overlap | ||||||||||||||||||||||||||||||||||||||
| query += '\nevents = [];'; | ||||||||||||||||||||||||||||||||||||||
| for (const hostname of this.selectedHosts) { | ||||||||||||||||||||||||||||||||||||||
| const safeHost = hostname.replace(/[^a-zA-Z0-9_]/g, ''); | ||||||||||||||||||||||||||||||||||||||
| query += `\nevents = union_no_overlap(events, events_${safeHost});`; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| query += ` | ||||||||||||||||||||||||||||||||||||||
| duration = sum_durations(events); | ||||||||||||||||||||||||||||||||||||||
| RETURN = {"events": events, "duration": duration}; | ||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Debug: log the query | ||||||||||||||||||||||||||||||||||||||
| console.log('Query being sent:', query); | ||||||||||||||||||||||||||||||||||||||
| console.log('Query length:', query.length); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Query for each day | ||||||||||||||||||||||||||||||||||||||
| const results = await client.query(timeperiods, [query]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Process results into daily data | ||||||||||||||||||||||||||||||||||||||
| this.dailyData = timeperiods.map((tp, i) => { | ||||||||||||||||||||||||||||||||||||||
| const result = results[i]; | ||||||||||||||||||||||||||||||||||||||
| const events = result.events || []; | ||||||||||||||||||||||||||||||||||||||
| const duration = result.duration || 0; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // tp is a string like "2025-01-01T00:00:00Z/2025-01-02T00:00:00Z" | ||||||||||||||||||||||||||||||||||||||
| const startDate = tp.split('/')[0]; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||
| date: moment(startDate).format('YYYY-MM-DD'), | ||||||||||||||||||||||||||||||||||||||
| duration, | ||||||||||||||||||||||||||||||||||||||
| sessions: events.length, | ||||||||||||||||||||||||||||||||||||||
| avgSession: events.length > 0 ? duration / events.length : 0, | ||||||||||||||||||||||||||||||||||||||
| events, | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| this.rawData = results; | ||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||
| console.error('Error loading work time data:', error); | ||||||||||||||||||||||||||||||||||||||
| alert('Error loading data. See console for details.'); | ||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||
| this.loading = false; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| getTimeperiods() { | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The UI offers several date range options (e.g. 'thisWeek', 'thisMonth', and 'custom'), but |
||||||||||||||||||||||||||||||||||||||
| const offset = this.settingsStore.startOfDay; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const timeperiods = []; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (this.dateRange === 'last7d') { | ||||||||||||||||||||||||||||||||||||||
| for (let i = 6; i >= 0; i--) { | ||||||||||||||||||||||||||||||||||||||
| const start = moment().subtract(i, 'days').startOf('day').add(offset); | ||||||||||||||||||||||||||||||||||||||
| const end = start.clone().add(1, 'day'); | ||||||||||||||||||||||||||||||||||||||
| timeperiods.push(start.format() + '/' + end.format()); | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
254
to
262
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Parse the offset properly:
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/views/WorkReport.vue
Line: 252:260
Comment:
`offset` is a string (format "04:00") but is being passed directly to `moment().add()` which expects a number.
Parse the offset properly:
```suggestion
const offset = this.settingsStore.startOfDay;
const [hours, minutes] = offset.split(':');
const offsetDuration = { hours: parseInt(hours), minutes: parseInt(minutes) };
const timeperiods = [];
if (this.dateRange === 'last7d') {
for (let i = 6; i >= 0; i--) {
const start = moment().subtract(i, 'days').startOf('day').add(offsetDuration);
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } else if (this.dateRange === 'last30d') { | ||||||||||||||||||||||||||||||||||||||
| for (let i = 29; i >= 0; i--) { | ||||||||||||||||||||||||||||||||||||||
| const start = moment().subtract(i, 'days').startOf('day').add(offset); | ||||||||||||||||||||||||||||||||||||||
| const end = start.clone().add(1, 'day'); | ||||||||||||||||||||||||||||||||||||||
| timeperiods.push(start.format() + '/' + end.format()); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| // TODO: Add other date ranges | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. only 'last7d' and 'last30d' are implemented, but 'thisWeek', 'thisMonth', and 'custom' date range options are presented to users without corresponding logic Prompt To Fix With AIThis is a comment left during a code review.
Path: src/views/WorkReport.vue
Line: 269:269
Comment:
only 'last7d' and 'last30d' are implemented, but 'thisWeek', 'thisMonth', and 'custom' date range options are presented to users without corresponding logic
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return timeperiods; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| formatDuration(seconds: number): string { | ||||||||||||||||||||||||||||||||||||||
| if (!seconds) return '0:00'; | ||||||||||||||||||||||||||||||||||||||
| const hours = Math.floor(seconds / 3600); | ||||||||||||||||||||||||||||||||||||||
| const minutes = Math.floor((seconds % 3600) / 60); | ||||||||||||||||||||||||||||||||||||||
| return `${hours}:${minutes.toString().padStart(2, '0')}`; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| exportCSV() { | ||||||||||||||||||||||||||||||||||||||
| const headers = ['Date', 'Duration (hours)', 'Sessions', 'Avg Session (minutes)']; | ||||||||||||||||||||||||||||||||||||||
| const rows = this.dailyData.map(day => [ | ||||||||||||||||||||||||||||||||||||||
| day.date, | ||||||||||||||||||||||||||||||||||||||
| (day.duration / 3600).toFixed(2), | ||||||||||||||||||||||||||||||||||||||
| day.sessions, | ||||||||||||||||||||||||||||||||||||||
| (day.avgSession / 60).toFixed(1), | ||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const csv = [ | ||||||||||||||||||||||||||||||||||||||
| headers.join(','), | ||||||||||||||||||||||||||||||||||||||
| ...rows.map(row => row.join(',')), | ||||||||||||||||||||||||||||||||||||||
| '', | ||||||||||||||||||||||||||||||||||||||
| `Total,${(this.totalDuration / 3600).toFixed(2)},${this.totalSessions},${( | ||||||||||||||||||||||||||||||||||||||
| this.avgSessionLength / 60 | ||||||||||||||||||||||||||||||||||||||
| ).toFixed(1)}`, | ||||||||||||||||||||||||||||||||||||||
| ].join('\n'); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| this.downloadFile(csv, 'work_time_report.csv', 'text/csv'); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| exportJSON() { | ||||||||||||||||||||||||||||||||||||||
| const data = { | ||||||||||||||||||||||||||||||||||||||
| parameters: { | ||||||||||||||||||||||||||||||||||||||
| categories: this.selectedCategories, | ||||||||||||||||||||||||||||||||||||||
| breakTime: this.breakTime, | ||||||||||||||||||||||||||||||||||||||
| dateRange: this.dateRange, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| summary: { | ||||||||||||||||||||||||||||||||||||||
| totalDuration: this.totalDuration, | ||||||||||||||||||||||||||||||||||||||
| totalSessions: this.totalSessions, | ||||||||||||||||||||||||||||||||||||||
| avgSessionLength: this.avgSessionLength, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| daily: this.dailyData, | ||||||||||||||||||||||||||||||||||||||
| rawEvents: this.rawData, | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const json = JSON.stringify(data, null, 2); | ||||||||||||||||||||||||||||||||||||||
| this.downloadFile(json, 'work_time_report.json', 'application/json'); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| downloadFile(content: string, filename: string, mimeType: string) { | ||||||||||||||||||||||||||||||||||||||
| const blob = new Blob([content], { type: mimeType }); | ||||||||||||||||||||||||||||||||||||||
| const url = URL.createObjectURL(blob); | ||||||||||||||||||||||||||||||||||||||
| const link = document.createElement('a'); | ||||||||||||||||||||||||||||||||||||||
| link.href = url; | ||||||||||||||||||||||||||||||||||||||
| link.download = filename; | ||||||||||||||||||||||||||||||||||||||
| document.body.appendChild(link); | ||||||||||||||||||||||||||||||||||||||
| link.click(); | ||||||||||||||||||||||||||||||||||||||
| document.body.removeChild(link); | ||||||||||||||||||||||||||||||||||||||
| URL.revokeObjectURL(url); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| <style scoped> | ||||||||||||||||||||||||||||||||||||||
| .table { | ||||||||||||||||||||||||||||||||||||||
| font-size: 0.9rem; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| </style> | ||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just rename to
/report?Edit: oh, that already exists...