Skip to content

Commit

Permalink
✨ it is now possible to filter on any column in the details view (imp…
Browse files Browse the repository at this point in the history
…lements #187)

🌐 renamed the filter bar to quick filters and made it collapse-able
⬆️ updated package dependencies
  • Loading branch information
faburem committed Nov 7, 2023
1 parent 9073aa0 commit bd7c269
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 70 deletions.
4 changes: 2 additions & 2 deletions .meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ es5-shim@4.8.0
faburem:accounts-anonymous@0.4.0
faburem:client-server-logger@0.0.5
fetch@0.1.3
fourseven:scss@4.15.0
fourseven:scss@4.16.0
geojson-utils@1.0.11
hot-code-push@1.0.4
html-tools@1.1.3
Expand Down Expand Up @@ -96,4 +96,4 @@ underscore@1.0.13
url@1.3.2
webapp@1.13.5
webapp-hashing@1.1.1
zodern:types@1.0.9
zodern:types@1.0.10
7 changes: 5 additions & 2 deletions imports/api/timecards/server/publications.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Meteor.publish('getDetailedTimeEntriesForPeriodCount', function getDetailedTimeE
period,
dates,
search,
filters,
}) {
check(projectId, Match.OneOf(String, Array))
check(userId, Match.OneOf(String, Array))
Expand All @@ -81,7 +82,7 @@ Meteor.publish('getDetailedTimeEntriesForPeriodCount', function getDetailedTimeE
let count = 0
let initializing = true
const selector = buildDetailedTimeEntriesForPeriodSelector({
projectId, search, customer, period, dates, userId,
projectId, search, customer, period, dates, userId, filters,
})
const countsId = projectId instanceof Array ? projectId.join('') : projectId
const handle = Timecards.find(selector[0], selector[1]).observeChanges({
Expand Down Expand Up @@ -115,6 +116,7 @@ Meteor.publish('getDetailedTimeEntriesForPeriod', async function getDetailedTime
sort,
limit,
page,
filters,
}) {
check(projectId, Match.OneOf(String, Array))
check(userId, Match.OneOf(String, Array))
Expand All @@ -133,9 +135,10 @@ Meteor.publish('getDetailedTimeEntriesForPeriod', async function getDetailedTime
}
check(limit, Number)
check(page, Match.Maybe(Number))
check(filters, Match.Maybe(Object))
await checkAuthentication(this)
const selector = buildDetailedTimeEntriesForPeriodSelector({
projectId, search, customer, period, dates, userId, limit, page, sort,
projectId, search, customer, period, dates, userId, limit, page, sort, filters,
})
return Timecards.find(selector[0], selector[1])
})
Expand Down
52 changes: 38 additions & 14 deletions imports/ui/pages/details/components/detailtimetable.html
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
<template name="detailtimetable">
<div class="row d-print-none">
<div class="col-xl-6 col-lg-7 col-6 col-sm-12 mt-2">
<div class="col-xl-7 col-lg-7 col-6 col-sm-12 mt-2">
<div class="btn-group">
<button type="button" class="btn btn-primary border js-track-time"> <i class="fa fa-plus"></i> <span class="d-none d-md-inline">{{t "navigation.track"}}</span></button>
<button type="button" class="btn btn-primary border js-track-time"> <i class="fa fa-plus"></i> <span class="d-none d-md-inline">{{t "navigation.track"}}</span></button>
{{#if detailTimeEntries}}
<button type="button" class="btn btn-secondary border js-export-csv"><i class="fa fa-download"></i> <span class="d-none d-md-inline">CSV</span></button>
<button type="button" class="btn btn-secondary border js-export-xlsx"><i class="fa fa-download"></i> <span class="d-none d-md-inline">Excel</span></button>
<button type="button" class="btn btn-secondary border js-share" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="{{t 'dashboard.shareMessage'}}"><i class="fa fa-link"></i> <span class="d-none d-md-inline">{{t "navigation.share"}}</span></button>
<button type="button" class="btn btn-secondary border js-export-csv"><i class="fa fa-download"></i> <span class="d-none d-md-inline">CSV</span></button>
<button type="button" class="btn btn-secondary border js-export-xlsx"><i class="fa fa-download"></i> <span class="d-none d-md-inline">Excel</span></button>
<button type="button" class="btn btn-secondary border js-share" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="{{t 'dashboard.shareMessage'}}"><i class="fa fa-link"></i> <span class="d-none d-md-inline">{{t "navigation.share"}}</span></button>
{{#if showInvoiceButton}}
<button type="button" class="btn btn-secondary border js-invoice"><i class="fa fa-upload"></i> <span class="d-none d-md-inline">{{t "navigation.invoice"}}</span></button>
<button type="button" class="btn btn-secondary border js-invoice"><i class="fa fa-upload"></i> <span class="d-none d-md-inline">{{t "navigation.invoice"}}</span></button>
{{/if}}
{{#if showMarkAsBilledButton}}
<button type="button" class="btn btn-secondary border js-mark-billed"><i class="fa-regular fa-square-check"></i> <span class="d-none d-md-inline">{{t "details.markAsBilled"}}</span></button>
<button type="button" class="btn btn-secondary border js-mark-billed"><i class="fa-regular fa-square-check"></i> <span class="d-none d-md-inline">{{t "details.markAsBilled"}}</span></button>
{{/if}}
{{else}}
<button type="button" class="btn btn-secondary border js-export-xlsx" disabled><i class="fa fa-download"></i> <span class="d-none d-md-inline">Excel</span></button>
<button type="button" class="btn btn-secondary border js-export-csv" disabled><i class="fa fa-download"></i> <span class="d-none d-md-inline">CSV</span></button>
<button type="button" class="btn btn-secondary border js-share" disabled><i class="fa fa-link"></i> <span class="d-none d-md-inline">{{t "navigation.share"}}</span></button>
<button type="button" class="btn btn-secondary border js-export-xlsx" disabled><i class="fa fa-download"></i> <span class="d-none d-md-inline">Excel</span></button>
<button type="button" class="btn btn-secondary border js-export-csv" disabled><i class="fa fa-download"></i> <span class="d-none d-md-inline">CSV</span></button>
<button type="button" class="btn btn-secondary border js-share" disabled><i class="fa fa-link"></i> <span class="d-none d-md-inline">{{t "navigation.share"}}</span></button>
{{#if showInvoiceButton}}
<button type="button" class="btn btn-secondary border js-invoice" disabled><i class="fa fa-upload"></i> <span class="d-none d-md-inline">{{t "navigation.invoice"}}</span></button>
<button type="button" class="btn btn-secondary border js-invoice" disabled><i class="fa fa-upload"></i> <span class="d-none d-md-inline">{{t "navigation.invoice"}}</span></button>
{{/if}}
{{#if showMarkAsBilledButton}}
<button type="button" class="btn btn-secondary border js-mark-billed" disabled><i class="fa-regular fa-square-check"></i> <span class="d-none d-md-inline">{{t "details.markAsBilled"}}</span></button>
<button type="button" class="btn btn-secondary border js-mark-billed" disabled><i class="fa-regular fa-square-check"></i> <span class="d-none d-md-inline">{{t "details.markAsBilled"}}</span></button>
{{/if}}
{{/if}}
{{#if filters}}
<button type="button" class="btn btn-secondary border js-remove-filters"><i class="fa-solid fa-filter-circle-xmark"></i> Remove</button>
{{/if}}
</div>
</div>
<!-- <div class="col-xl-3 col-lg-3 col-6 mt-2">
Expand All @@ -41,13 +44,34 @@
<div class="row">
<div class="col-md-3 ">
{{#if detailTimeEntries}}
{{>pagination totalEntries=totalDetailTimeEntries limit=limit}}
{{/if}}
{{>pagination totalEntries=totalDetailTimeEntries limit=limit}}
{{/if}}
</div>
<div class="col-md-3 ms-md-auto">
{{>limitpicker}}
</div>
</div>
{{>editTimeEntryModal tcid=tcid selectedProjectId=project}}
<div class="modal fade" id="filterModal" tabindex="-1" aria-labelledby="filterModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="filterModalLabel">{{t "details.filter"}}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="filterModalBody">
<div class="form-floating">
<select id="genericFilter" type="text" class="form-control" placeholder="{{t "details.filter"}}" aria-label="{{t 'details.filter'}}">
</select>
<label class="form-label" for="genericFilter">{{t "details.filter"}}</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{t "navigation.close"}}</button>
<button type="button" id="saveFilter" class="btn btn-primary" data-bs-dismiss="modal">{{t "navigation.save"}}</button>
</div>
</div>
</div>
</div>
</template>

102 changes: 90 additions & 12 deletions imports/ui/pages/details/components/detailtimetable.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import { saveAs } from 'file-saver'
import { FlowRouter } from 'meteor/ostrio:flow-router-extra'
import { NullXlsx } from '@neovici/nullxlsx'
Expand Down Expand Up @@ -27,14 +28,22 @@ import './limitpicker.js'
const Counts = new Mongo.Collection('counts')

dayjs.extend(utc)
dayjs.extend(customParseFormat)

const customFieldType = 'name'
function detailedDataTableMapper(entry) {

function detailedDataTableMapper(entry, forExport) {
const project = Projects.findOne({ _id: entry.projectId })
const mapping = [project ? project.name : '',
let mapping = [entry.projectId,
dayjs.utc(entry.date).format(getGlobalSetting('dateformat')),
entry.task.replace(/^=/, '\\='),
projectResources.findOne() ? projectResources.findOne({ _id: entry.userId })?.name : '']
entry.userId]
if (forExport) {
mapping = [project?.name ? project.name : '',
dayjs.utc(entry.date).format(getGlobalSetting('dateformat')),
entry.task.replace(/^=/, '\\='),
projectResources.findOne() ? projectResources.findOne({ _id: entry.userId })?.name : '']
}
if (getGlobalSetting('showCustomFieldsInDetails')) {
if (CustomFields.find({ classname: 'time_entry' }).count() > 0) {
for (const customfield of CustomFields.find({ classname: 'time_entry' }).fetch()) {
Expand Down Expand Up @@ -67,6 +76,7 @@ Template.detailtimetable.onCreated(function workingtimetableCreated() {
this.sort = new ReactiveVar()
this.tcid = new ReactiveVar()
this.selector = new ReactiveVar()
this.filters = new ReactiveVar({})
this.subscribe('customfieldsForClass', { classname: 'time_entry' })
this.subscribe('customfieldsForClass', { classname: 'project' })
this.autorun(() => {
Expand All @@ -90,6 +100,7 @@ Template.detailtimetable.onCreated(function workingtimetableCreated() {
limit: this.data.limit.get(),
page: Number(FlowRouter.getQueryParam('page')),
sort: this.sort.get(),
filters: this.filters.get(),
}))
delete this.selector.get()[1].skip
const subscriptionParameters = {
Expand All @@ -101,6 +112,7 @@ Template.detailtimetable.onCreated(function workingtimetableCreated() {
search: this.search.get(),
sort: this.sort.get(),
page: Number(FlowRouter.getQueryParam('page')),
filters: this.filters.get(),
}
if (this.data.period.get() === 'custom') {
subscriptionParameters.dates = {
Expand All @@ -118,21 +130,38 @@ Template.detailtimetable.onRendered(() => {
dayjs.extend(utc)
templateInstance.autorun(() => {
if (templateInstance.detailedTimeEntriesForPeriodHandle.ready()
&& templateInstance.detailedEntriesPeriodCountHandle.ready()
&& templateInstance.projectResourcesHandle.ready() && i18nReady.get()) {
const data = Timecards.find(templateInstance.selector.get()[0], templateInstance.selector.get()[1])
.fetch().map(detailedDataTableMapper)
const data = Timecards.find(
templateInstance.selector.get()[0],
templateInstance.selector.get()[1],
)
.fetch().map((entry) => detailedDataTableMapper(entry, false))
if (data.length === 0) {
$('.dt-row-totalRow').remove()
}
const columns = [
{ name: t('globals.project'), editable: false, format: addToolTipToTableCell },
{
name: t('globals.project'),
id: 'projectId',
editable: false,
format: (value) => addToolTipToTableCell(Projects.findOne({ _id: value })?.name),
},
{
name: t('globals.date'),
id: 'date',
editable: false,
format: addToolTipToTableCell,
},
{ name: t('globals.task'), editable: false, format: addToolTipToTableCell },
{ name: t('globals.resource'), editable: false, format: addToolTipToTableCell }]
{
name: t('globals.task'), id: 'task', editable: false, format: addToolTipToTableCell,
},
{
name: t('globals.resource'),
id: 'userId',
editable: false,
format: (value) => addToolTipToTableCell(projectResources.findOne() ? projectResources.findOne({ _id: value })?.name : ''),
}]
if (getGlobalSetting('showCustomFieldsInDetails')) {
let customFieldColumnType = 'desc'
if (getGlobalSetting('showNameOfCustomFieldInDetails')) {
Expand All @@ -142,6 +171,7 @@ Template.detailtimetable.onRendered(() => {
for (const customfield of CustomFields.find({ classname: 'time_entry' }).fetch()) {
columns.push({
name: customfield[customFieldColumnType],
id: customfield.name,
editable: false,
format: addToolTipToTableCell,
})
Expand All @@ -151,6 +181,7 @@ Template.detailtimetable.onRendered(() => {
for (const customfield of CustomFields.find({ classname: 'project' }).fetch()) {
columns.push({
name: customfield[customFieldColumnType],
id: customfield.name,
editable: false,
format: addToolTipToTableCell,
})
Expand All @@ -159,13 +190,16 @@ Template.detailtimetable.onRendered(() => {
}
if (getGlobalSetting('showCustomerInDetails')) {
columns.push(
{ name: t('globals.customer'), editable: false, format: addToolTipToTableCell },
{
name: t('globals.customer'), id: 'customer', editable: false, format: addToolTipToTableCell,
},
)
}
if (getGlobalSetting('useState')) {
columns.push(
{
name: t('details.state'),
id: 'state',
editable: true,
format: (value) => {
if (value === null) {
Expand All @@ -181,13 +215,15 @@ Template.detailtimetable.onRendered(() => {
columns.push(
{
name: t('details.startTime'),
id: 'startTime',
editable: false,
format: addToolTipToTableCell,
},
)
columns.push(
{
name: t('details.endTime'),
id: 'endTime',
editable: false,
format: addToolTipToTableCell,
},
Expand All @@ -196,11 +232,13 @@ Template.detailtimetable.onRendered(() => {
columns.push(
{
name: getUserTimeUnitVerbose(),
id: 'hours',
editable: false,
format: numberWithUserPrecision,
},
{
name: t('navigation.edit'),
id: 'actions',
editable: false,
dropdown: false,
focusable: false,
Expand Down Expand Up @@ -251,7 +289,32 @@ Template.detailtimetable.onRendered(() => {
{
label: 'Filter',
action(column) {
console.log(column)
const filterModal = new bootstrap.Modal('#filterModal')
filterModal.show()
templateInstance.$('#genericFilter').html('')
const uniqueRowValues = new Map()
for (const row of templateInstance.datatable.datamanager.rows) {
if (column.id === 'state') {
if (row[column.colIndex].content === undefined) {
uniqueRowValues.set('new', t('details.new'))
} else {
uniqueRowValues.set(
row[column.colIndex].content,
$(row[column.colIndex].html).text(),
)
}
} else {
uniqueRowValues.set(
row[column.colIndex].content,
$(row[column.colIndex].html).text()
? $(row[column.colIndex].html).text() : row[column.colIndex].content,
)
}
}
for (const [key, value] of uniqueRowValues) {
templateInstance.$('#genericFilter').append(new Option(value, key))
}
templateInstance.$('#genericFilter').data('filtertarget', column.id)
},
},
],
Expand Down Expand Up @@ -354,6 +417,10 @@ Template.detailtimetable.helpers({
tcid() { return Template.instance().tcid },
showInvoiceButton: () => (getGlobalSetting('enableSiwapp') && getUserSetting('siwappurl')),
showMarkAsBilledButton: () => (getGlobalSetting('useState') && (!getGlobalSetting('enableSiwapp') || !getUserSetting('siwappurl'))),
filters() {
return !!(Template.instance().filters.get()
&& Object.keys(Template.instance().filters.get()).length > 0)
},
})
Template.detailtimetable.events({
'click .js-export-csv': (event, templateInstance) => {
Expand Down Expand Up @@ -383,7 +450,7 @@ Template.detailtimetable.events({
selector.state = { $in: ['new', undefined] }
for (const timeEntry of Timecards
.find(templateInstance.selector.get()[0], templateInstance.selector.get()[1])
.fetch().map(detailedDataTableMapper)) {
.fetch().map((entry) => detailedDataTableMapper(entry, true))) {
const row = []
for (const attribute of timeEntry) {
row.push(attribute)
Expand Down Expand Up @@ -436,7 +503,7 @@ Template.detailtimetable.events({
selector.state = { $in: ['new', undefined] }
for (const timeEntry of Timecards
.find(templateInstance.selector.get()[0], templateInstance.selector.get()[1]).fetch()
.map(detailedDataTableMapper)) {
.map((entry) => detailedDataTableMapper(entry, true))) {
const row = []
let index = 0
timeEntry.splice(timeEntry.length - 1, 1)
Expand Down Expand Up @@ -572,6 +639,17 @@ Template.detailtimetable.events({
cancelable: true,
}))
},
'click #saveFilter': (event, templateInstance) => {
event.preventDefault()
const filter = templateInstance.filters.get()
const filterTarget = templateInstance.$('#genericFilter').data('filtertarget')
filter[filterTarget] = templateInstance.$('#genericFilter').val()
templateInstance.filters.set(filter)
},
'click .js-remove-filters': (event, templateInstance) => {
event.preventDefault()
templateInstance.filters.set({})
},
})
Template.detailtimetable.onDestroyed(() => {
FlowRouter.setQueryParams({ page: null })
Expand Down
4 changes: 2 additions & 2 deletions imports/ui/pages/details/components/filterbar.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template name="filterbar">
<div class="card mb-2 d-print-none">
<div class="card-header card-header-filter">Filter</div>
<div class="card-body">
<div class="card-header card-header-filter" data-bs-toggle="collapse" data-bs-target=".js-quickfilter-body" style="cursor:pointer;">{{t "details.quickFilter"}}</div>
<div class="card-body collapse show js-quickfilter-body">
<div class="row">
<div class="col-sm-3 mb-2 mb-md-0">
{{>multiselectfilter items=projects name="projectselect" all="overview.all_projects" label="globals.project"}}
Expand Down

0 comments on commit bd7c269

Please sign in to comment.