Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

[email protected] # Packages every Meteor app needs to have
[email protected] # Packages for a great mobile UX
[email protected].2 # The database Meteor supports right now
[email protected].3 # The database Meteor supports right now
[email protected] # Reactive variable for tracker
[email protected] # Meteor's client-side reactive programming library

[email protected] # ECMAScript 5 compatibility for older browsers.
[email protected].11 # Enable ECMAScript2015+ syntax in app code
[email protected].12 # Enable ECMAScript2015+ syntax in app code
[email protected] # Server-side component of the `meteor shell` command

[email protected]
Expand All @@ -32,7 +32,7 @@ [email protected]
blaze-html-templates
[email protected]
[email protected]
[email protected].0
[email protected].1
leonardoventurini:scss
browser-policy-content
browser-policy-content@2.0.0
faburem:accounts-anonymous
2 changes: 1 addition & 1 deletion .meteor/release
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[email protected]
[email protected].1
20 changes: 10 additions & 10 deletions .meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ [email protected]
[email protected]
[email protected]
[email protected]
[email protected].0
[email protected].1
[email protected]
[email protected]
[email protected]
Expand All @@ -15,7 +15,7 @@ [email protected]
[email protected]
[email protected]
[email protected]
[email protected].0
[email protected].1
[email protected]
[email protected]
[email protected]
Expand All @@ -25,7 +25,7 @@ [email protected]
[email protected]
[email protected]
[email protected]
[email protected].11
[email protected].12
[email protected]
[email protected]
[email protected]
Expand All @@ -51,18 +51,18 @@ mdg:[email protected]
[email protected]
[email protected]
[email protected]
[email protected].2
[email protected].2
[email protected].3
[email protected].3
[email protected]
[email protected]
[email protected].2
[email protected].3
[email protected]
[email protected]
[email protected].2
[email protected].3
[email protected]
[email protected]
[email protected]
npm-mongo@6.10.2
npm-mongo@6.16.0
[email protected]
[email protected]
[email protected]
Expand All @@ -86,13 +86,13 @@ [email protected]
[email protected]
[email protected]
[email protected]
[email protected].0
[email protected].1
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected].4
[email protected].5
[email protected]
[email protected]
[email protected]
3 changes: 3 additions & 0 deletions imports/api/globalsettings/globalsettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,7 @@ defaultSettings.push({
defaultSettings.push({
name: 'debugMode', description: 'settings.debug_mode', type: 'checkbox', value: 'false', category: 'settings.categories.global',
})
defaultSettings.push({
name: 'enableTaskPlanning', description: 'settings.enable_task_planning', type: 'checkbox', value: false, category: 'settings.categories.time_tracking',
})
export { defaultSettings, Globalsettings }
10 changes: 8 additions & 2 deletions imports/api/tasks/server/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Inserts a new project task into the Tasks collection.
@param {string} args.name - The name of the task.
@param {Date} args.start - The start date of the task.
@param {Date} args.end - The end date of the task.
@param {number} [args.estimatedHours] - The estimated/planned hours for the task.
@param {string[]} [args.dependencies] - An array of task IDs that this task depends on.
*/
const insertProjectTask = new ValidatedMethod({
Expand All @@ -20,19 +21,21 @@ const insertProjectTask = new ValidatedMethod({
name: String,
start: Date,
end: Date,
estimatedHours: Match.Optional(Number),
dependencies: Match.Optional([String]),
customfields: Match.Optional(Object),
})
},
mixins: [authenticationMixin, transactionLogMixin],
async run({
projectId, name, start, end, dependencies, customfields,
projectId, name, start, end, estimatedHours, dependencies, customfields,
}) {
await Tasks.insertAsync({
projectId,
name,
start,
end,
estimatedHours,
dependencies,
...customfields,
})
Expand All @@ -46,6 +49,7 @@ const insertProjectTask = new ValidatedMethod({
* @param {string} [args.name] - The name of the task.
* @param {Date} [args.start] - The start date of the task.
* @param {Date} [args.end] - The end date of the task.
* @param {number} [args.estimatedHours] - The estimated/planned hours for the task.
* @param {string[]} [args.dependencies] - An array of task IDs that this task depends on.
* @throws {Meteor.Error} If user is not authenticated.
* @returns {String} 'notifications.success' if successful
Expand All @@ -59,19 +63,21 @@ const updateTask = new ValidatedMethod({
name: Match.Optional(String),
start: Match.Optional(Date),
end: Match.Optional(Date),
estimatedHours: Match.Optional(Number),
dependencies: Match.Optional([String]),
customfields: Match.Optional(Object),
})
},
mixins: [authenticationMixin, transactionLogMixin],
async run({
taskId, name, start, end, dependencies, customfields,
taskId, name, start, end, estimatedHours, dependencies, customfields,
}) {
await Tasks.updateAsync(taskId, {
$set: {
name,
start,
end,
estimatedHours,
dependencies,
...customfields,
},
Expand Down
2 changes: 1 addition & 1 deletion imports/api/tasks/server/publications.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { check } from 'meteor/check'
import { check, Match } from 'meteor/check'
import { checkAuthentication, getGlobalSettingAsync } from '../../../utils/server_method_helpers.js'
import Tasks from '../tasks.js'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ <h5 class="modal-title">
<input type="date" id="end" name="end" class="form-control" value="{{end}}"/>
<label class="form-label" for="end">{{t "task.endDate"}}</label>
</div>
{{#if getGlobalSetting "enableTaskPlanning"}}
<div class="mb-3 form-floating">
<input type="number" id="estimatedHours" name="estimatedHours" class="form-control" value="{{estimatedHours}}" step="0.25" min="0"/>
<label class="form-label" for="estimatedHours">{{t "task.estimatedHours"}}</label>
</div>
{{/if}}
<div class="mb-3 form-floating">
<select id="dependencies" name="dependencies" class="form-select">
<option value=""> </option>
Expand Down
4 changes: 4 additions & 0 deletions imports/ui/pages/overview/editproject/components/taskModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import BsDialogs from '../../../../shared components/bootstrapDialogs'

Template.taskModal.onCreated(function taskModalCreated() {
this.editTask = new ReactiveVar(false)
this.subscribe('globalsettings')
this.autorun(() => {
if (this.data?.editTaskID.get()) {
this.editTask.set(Tasks.findOne({ _id: this.data?.editTaskID.get() }))
Expand All @@ -26,6 +27,8 @@ Template.taskModal.events({
if ($(this).val() && !$(this).hasClass('js-customfield')) {
if ($(this).get(0).type === 'date') {
newTask[$(this).get(0).name] = new Date($(this).val())
} else if ($(this).get(0).type === 'number') {
newTask[$(this).get(0).name] = Number($(this).val())
} else {
newTask[$(this).get(0).name] = $(this).val()
}
Expand Down Expand Up @@ -91,6 +94,7 @@ Template.taskModal.helpers({
? Template.instance().editTask.get().start?.toJSON().slice(0, 10)
: new Date().toJSON().slice(0, 10)),
end: () => (Template.instance().editTask.get() ? Template.instance().editTask.get().end?.toJSON().slice(0, 10) : ''),
estimatedHours: () => (Template.instance().editTask.get() ? Template.instance().editTask.get().estimatedHours || '' : ''),
possibleDependencies: () => Tasks.find({ projectId: FlowRouter.getParam('id'), _id: { $ne: Template.instance().editTask.get()?._id } }),
isSelectedDep: (dependency) => (Template.instance().editTask.get()?.dependencies?.includes(dependency) ? 'selected' : ''),
replaceSpecialChars: (string) => string.replace(/[^A-Z0-9]/ig, '_'),
Expand Down
104 changes: 94 additions & 10 deletions imports/ui/pages/track/components/projectTasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,87 @@ import Bootstrap from 'bootstrap'
import { i18nReady, t } from '../../../../utils/i18n.js'
import './projectTasks.html'
import Tasks from '../../../../api/tasks/tasks'
import Timecards from '../../../../api/timecards/timecards'
import CustomFields from '../../../../api/customfields/customfields'
import '../../overview/editproject/components/taskModal.js'
import {
addToolTipToTableCell, getGlobalSetting, showToast,
addToolTipToTableCell, getGlobalSetting, showToast, timeInUserUnit, numberWithUserPrecision,
} from '../../../../utils/frontend_helpers'

dayjs.extend(utc)

// Progress bar formatter function
function formatProgressBar(value) {
const percent = parseFloat(value)
let colorClass = 'bg-success'
if (percent > 110) colorClass = 'bg-danger'
else if (percent > 100) colorClass = 'bg-warning'
return `
<div class="progress" style="width: 100%; height: 16px;">
<div class="progress-bar ${colorClass}" role="progressbar"
style="width: ${Math.min(percent, 100)}%; font-size: 12px; line-height: 16px;"
aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100">
${value}
</div>
</div>`
}

// Variance formatter function
function formatVariance(value) {
const variance = parseFloat(value)
if (variance > 0) {
return `<span class="text-danger">+${value}</span>`
} if (variance < 0) {
return `<span class="text-success">${value}</span>`
}
return `<span class="text-muted">${value}</span>`
}

function taskMapper(task) {
const mapping =
[task._id,
const mapping = [task._id,
task.name,
dayjs.utc(task.start).format(getGlobalSetting('dateformat')),
dayjs.utc(task.end).format(getGlobalSetting('dateformat')),
task.dependencies?.map((dep) => Tasks.findOne({ _id: dep })?.name).join(',')]
if (CustomFields.find({ classname: 'task' }).count() > 0) {
for (const customfield of CustomFields.find({ classname: 'task' }).fetch()) {
mapping.push(task[customfield.name])
}

// Add estimated hours column if task planning is enabled
if (getGlobalSetting('enableTaskPlanning')) {
mapping.push(numberWithUserPrecision(task.estimatedHours) || '')
// Calculate actual hours for this task
const actualHours = Timecards.find({
projectId: task.projectId,
task: task.name,
}).fetch().reduce((total, entry) => total + (entry.hours || 0), 0)
mapping.push(timeInUserUnit(actualHours) || '0')
// Calculate variance (actual - estimated)
const variance = actualHours - (task.estimatedHours || 0)
mapping.push(timeInUserUnit(variance) || '0')
// Calculate progress percentage
const progressPercent = task.estimatedHours ? (actualHours / task.estimatedHours * 100) : 0
mapping.push(`${numberWithUserPrecision(progressPercent)}%`)
}

if (CustomFields.find({ classname: 'task' }).count() > 0) {
for (const customfield of CustomFields.find({ classname: 'task' }).fetch()) {
mapping.push(task[customfield.name])
}
return mapping
}
return mapping
}

Template.projectTasks.onCreated(function projectTasksCreated() {
this.subscribe('projectTasks', { projectId: FlowRouter.getParam('id') })
this.subscribe('customfieldsForClass', { classname: 'task' })
this.subscribe('globalsettings')
// Subscribe to timecards for the last 2 years to calculate actual hours
const endDate = new Date()
const startDate = new Date()
startDate.setFullYear(startDate.getFullYear() - 2)
this.subscribe('periodTimecards', {
startDate,
endDate,
userId: 'all',
})
this.editTaskID = new ReactiveVar(false)
})

Expand Down Expand Up @@ -71,6 +126,35 @@ Template.projectTasks.onRendered(() => {
width: 2,
},
]

// Add estimated hours column if task planning is enabled
if (getGlobalSetting('enableTaskPlanning')) {
columns.push({
name: t('task.estimatedHours'),
editable: true,
format: addToolTipToTableCell,
width: 1,
})
columns.push({
name: t('task.actualHours'),
editable: false,
format: addToolTipToTableCell,
width: 1,
})
columns.push({
name: t('task.variance'),
editable: false,
format: (value) => formatVariance(value),
width: 1,
})
columns.push({
name: t('task.progress'),
editable: false,
format: (value) => formatProgressBar(value),
width: 2,
})
}

if (CustomFields.find({ classname: 'task' }).count() > 0) {
for (const customfield of CustomFields.find({ classname: 'task' }).fetch()) {
columns.push({
Expand All @@ -93,7 +177,7 @@ Template.projectTasks.onRendered(() => {
clusterize: false,
layout: 'ratio',
noDataMessage: t('tabular.sZeroRecords'),
getEditor(colIndex, rowIndex, value, parent, column, row, data) {
getEditor(colIndex, rowIndex, value, parent, column, row) {
templateInstance.editTaskID.set(row[0].content)
templateInstance.$(parent.parentNode).removeClass('dt-cell--editing')
new Bootstrap.Modal(templateInstance.$('#task-modal')).show()
Expand Down Expand Up @@ -152,7 +236,7 @@ Template.projectTasks.helpers({

Template.projectTasks.events({
'change .form-check-input': (event, templateInstance) => {
Meteor.call('setDefaultTaskForProject', { projectId: FlowRouter.getParam('id'), taskId: templateInstance.$(event.target).data('id') }, (error, result) => {
Meteor.call('setDefaultTaskForProject', { projectId: FlowRouter.getParam('id'), taskId: templateInstance.$(event.target).data('id') }, (error) => {
if (error) {
console.error(error)
} else {
Expand Down
5 changes: 5 additions & 0 deletions imports/ui/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@
"show_rate_in_details": "Rate in Detailansicht anzeigen/exportieren?",
"enable_LDAP": "LDAP Anmeldung aktivieren?",
"enable_project_CSV_import": "Projekt-CSV-Import aktivieren",
"enable_task_planning": "Aufgabenplanung mit geschätzten Stunden aktivieren?",
"debug_mode": "Debug Modus aktivieren?"
},
"customer": {
Expand Down Expand Up @@ -301,6 +302,10 @@
"lastUsed": "Zuletzt verwendet",
"startDate": "Geplanter Start",
"endDate": "Geplantes Ende",
"estimatedHours": "Geschätzte Stunden",
"actualHours": "Tatsächliche Stunden",
"variance": "Abweichung",
"progress": "Fortschritt",
"duration": "Dauer",
"dependencies": "Abhängigkeiten",
"newTask": "Neue Tätigkeit",
Expand Down
5 changes: 5 additions & 0 deletions imports/ui/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@
"show_rate_in_details": "Show/export rate field in details view?",
"enable_LDAP": "Enable LDAP login?",
"enable_project_CSV_import": "Enable Project CSV Import",
"enable_task_planning": "Enable task planning with estimated hours?",
"debug_mode": "Enable debug mode?"
},
"customer": {
Expand Down Expand Up @@ -301,6 +302,10 @@
"lastUsed": "Last used",
"startDate": "Planned start",
"endDate": "Planned finish",
"estimatedHours": "Estimated hours",
"actualHours": "Actual hours",
"variance": "Variance",
"progress": "Progress",
"duration": "Duration",
"dependencies": "Dependencies",
"newTask": "New task",
Expand Down
Loading
Loading