diff --git a/imports/api/dashboards/server/publications.js b/imports/api/dashboards/server/publications.js
index 8a2991a5..dd17dd39 100644
--- a/imports/api/dashboards/server/publications.js
+++ b/imports/api/dashboards/server/publications.js
@@ -13,7 +13,7 @@ Meteor.publish('dashboardById', async function dashboardById(_id) {
{
customer: dashboard.customer,
},
- { $fields: { _id: 1 } },
+ { fields: { _id: 1 } },
).fetchAsync()
projectList = projectList.map((value) => value._id)
if (dashboard.resourceId.includes('all')) {
diff --git a/imports/api/globalsettings/globalsettings.js b/imports/api/globalsettings/globalsettings.js
index 832f2b5e..dbbb0af9 100644
--- a/imports/api/globalsettings/globalsettings.js
+++ b/imports/api/globalsettings/globalsettings.js
@@ -143,10 +143,10 @@ defaultSettings.push({
name: 'enableTransactions', description: 'transactions.enable_transactions', type: 'checkbox', value: false, category: 'settings.categories.global',
})
defaultSettings.push({
- name: 'enableLogForOtherUsers', description: 'settings.enable_log_for_other_users', type: 'checkbox', value: false, category: 'settings.categories.time_tracking',
+ name: 'enableLogForOtherUsers', description: 'settings.enable_log_for_other_users', type: 'checkbox', value: false, category: 'settings.categories.time_tracking',
})
defaultSettings.push({
- name: 'userSearchNumResults', description: 'settings.user_search_num_results', type: 'number', value: 5, category: 'settings.categories.time_tracking',
+ name: 'userSearchNumResults', description: 'settings.user_search_num_results', type: 'number', value: 5, category: 'settings.categories.time_tracking',
})
defaultSettings.push({
name: 'customLogo', description: 'settings.custom_logo', type: 'textarea', value: '', category: 'settings.categories.customization',
@@ -163,4 +163,10 @@ defaultSettings.push({
defaultSettings.push({
name: 'showResourceInDetails', description: 'settings.show_resource_in_details', type: 'checkbox', value: true, category: 'settings.categories.customization',
})
+defaultSettings.push({
+ name: 'allowIndividualTaskRates', description: 'settings.allow_individual_task_rates', type: 'checkbox', value: false, category: 'settings.categories.time_tracking',
+})
+defaultSettings.push({
+ name: 'showRateInDetails', description: 'settings.show_rate_in_details', type: 'checkbox', value: false, category: 'settings.categories.customization',
+})
export { defaultSettings, Globalsettings }
diff --git a/imports/api/projects/server/publications.js b/imports/api/projects/server/publications.js
index 8194ee71..cd94dcd5 100644
--- a/imports/api/projects/server/publications.js
+++ b/imports/api/projects/server/publications.js
@@ -4,7 +4,7 @@ import isBetween from 'dayjs/plugin/isBetween'
import { check } from 'meteor/check'
import Projects from '../projects'
import Timecards from '../../timecards/timecards.js'
-import { checkAuthentication } from '../../../utils/server_method_helpers.js'
+import { checkAuthentication, getGlobalSettingAsync } from '../../../utils/server_method_helpers.js'
Meteor.publish('myprojects', async function myProjects({ projectLimit }) {
await checkAuthentication(this)
@@ -64,11 +64,25 @@ Meteor.publish('projectStats', async function projectStats(projectId) {
}, {
$group: { _id: '$userId', totalHours: { $sum: '$hours' } },
}]).toArray()
- for (const revenue of totalTimecardsRawForRevenue) {
- totalRevenue = project.rates && project.rates[revenue._id]
- ? totalRevenue += Number.parseFloat(revenue.totalHours)
+ if (await getGlobalSettingAsync('allowIndividualTaskRates')) {
+ const individualRateRevenue = await Timecards.find({ projectId }).fetchAsync()
+ for (const timecard of individualRateRevenue) {
+ if (timecard.taskRate) {
+ totalRevenue += Number.parseFloat(timecard.hours) * Number.parseFloat(timecard.taskRate)
+ } else {
+ totalRevenue = project.rates && project.rates[timecard.userId]
+ ? totalRevenue += Number.parseFloat(timecard.hours)
+ * Number.parseFloat(project.rates[timecard.userId])
+ : totalRevenue += Number.parseFloat(timecard.hours) * Number.parseFloat(project.rate)
+ }
+ }
+ } else {
+ for (const revenue of totalTimecardsRawForRevenue) {
+ totalRevenue = project.rates && project.rates[revenue._id]
+ ? totalRevenue += Number.parseFloat(revenue.totalHours)
* Number.parseFloat(project.rates[revenue._id])
- : totalRevenue += Number.parseFloat(revenue.totalHours) * Number.parseFloat(project.rate)
+ : totalRevenue += Number.parseFloat(revenue.totalHours) * Number.parseFloat(project.rate)
+ }
}
totalHours = Number.parseFloat(totalTimecardsRaw[0]?.totalHours)
const currentMonthTimeCardsRaw = await Timecards.rawCollection().aggregate([{ $match: { projectId, date: { $gte: currentMonthStart, $lte: currentMonthEnd } } }, { $group: { _id: null, currentMonthHours: { $sum: '$hours' } } }]).toArray()
@@ -171,7 +185,7 @@ Meteor.publish('projectStats', async function projectStats(projectId) {
.isBetween(beforePreviousMonthStart, beforePreviousMonthEnd)) {
beforePreviousMonthHours += Number.parseFloat(timecard.hours)
}
- if (project.rates[timecard.userId]) {
+ if (project?.rates[timecard.userId]) {
totalRevenue += Number.parseFloat(timecard.hours)
* Number.parseFloat(project.rates[timecard.userId])
} else {
diff --git a/imports/api/timecards/server/methods.js b/imports/api/timecards/server/methods.js
index df5cb8cd..782a5bc5 100644
--- a/imports/api/timecards/server/methods.js
+++ b/imports/api/timecards/server/methods.js
@@ -75,13 +75,14 @@ async function checkTimeEntryRule({
* @param {Date} args.date - The date of the timecard.
* @param {number} args.hours - The number of hours for the timecard.
* @param {string} [args.userId] - The ID of the user for the timecard.
+ * @param {number} taskRate - The rate of the task for the time card.
* @param {Object} [args.customfields] - The custom fields for the timecard.
* @throws {Meteor.Error} If user is not authenticated.
* @returns {String} 'notifications.success' if successful
* @throws {Meteor.Error} If time entry rule fails.
* @throws {Meteor.Error} If time entry rule throws an error.
*/
-async function insertTimeCard(projectId, task, date, hours, userId, customfields) {
+async function insertTimeCard(projectId, task, date, hours, userId, taskRate, customfields) {
const newTimeCard = {
userId,
projectId,
@@ -90,6 +91,9 @@ async function insertTimeCard(projectId, task, date, hours, userId, customfields
task: await emojify(task),
...customfields,
}
+ if (taskRate) {
+ newTimeCard.taskRate = taskRate
+ }
if (!await Tasks.findOneAsync({ $or: [{ userId }, { projectId }], name: await emojify(task) })) {
await Tasks.insertAsync({
userId, lastUsed: new Date(), name: await emojify(task), ...customfields,
@@ -203,12 +207,13 @@ const insertTimeCardMethod = new ValidatedMethod({
check(args.task, String)
check(args.date, Date)
check(args.hours, Number)
+ check(args.taskRate, Match.Maybe(Number))
check(args.customfields, Match.Maybe(Object))
check(args.user, String)
},
mixins: [authenticationMixin, transactionLogMixin],
async run({
- projectId, task, date, hours, customfields, user,
+ projectId, task, date, hours, taskRate, customfields, user,
}) {
let { userId } = this
if (user !== userId) {
@@ -217,7 +222,7 @@ const insertTimeCardMethod = new ValidatedMethod({
const check = await checkTimeEntryRule({
userId, projectId, task, state: 'new', date, hours,
})
- await insertTimeCard(projectId, task, date, hours, userId, customfields)
+ await insertTimeCard(projectId, task, date, hours, userId, taskRate, customfields)
},
})
/**
@@ -287,12 +292,13 @@ const updateTimeCard = new ValidatedMethod({
check(args.task, String)
check(args.date, Date)
check(args.hours, Number)
+ check(args.taskRate, Match.Maybe(Number))
check(args.customfields, Match.Maybe(Object))
check(args.user, String)
},
mixins: [authenticationMixin, transactionLogMixin],
async run({
- projectId, _id, task, date, hours, customfields, user,
+ projectId, _id, task, date, hours, taskRate, customfields, user,
}) {
let { userId } = this
if (user !== userId) {
@@ -305,15 +311,24 @@ const updateTimeCard = new ValidatedMethod({
if (!await Tasks.findOneAsync({ userId, name: await emojify(task) })) {
await Tasks.insertAsync({ userId, name: await emojify(task), ...customfields })
}
- await Timecards.updateAsync({ _id }, {
- $set: {
- projectId,
- date,
- hours,
- task: await emojify(task),
- ...customfields,
- },
- })
+ const fieldsToSet = {
+ projectId,
+ date,
+ hours,
+ task: await emojify(task),
+ ...customfields,
+ }
+ if (taskRate) {
+ fieldsToSet.taskRate = taskRate
+ await Timecards.updateAsync({ _id }, {
+ $set: fieldsToSet,
+ })
+ } else {
+ await Timecards.updateAsync({ _id }, {
+ $set: fieldsToSet,
+ $unset: { taskRate: '' },
+ })
+ }
},
})
/**
diff --git a/imports/api/timecards/server/publications.js b/imports/api/timecards/server/publications.js
index 91d8bfd8..6bbc14d5 100644
--- a/imports/api/timecards/server/publications.js
+++ b/imports/api/timecards/server/publications.js
@@ -10,8 +10,13 @@ Meteor.publish('periodTimecards', async function periodTimecards({ startDate, en
check(userId, String)
await checkAuthentication(this)
let projectList = await Projects.find(
- { $or: [{ userId: this.userId }, { public: true }, { team: this.userId }] },
- { $fields: { _id: 1 } },
+ {
+ $and: [
+ { $or: [{ userId: this.userId }, { public: true }, { team: this.userId }] },
+ { $or: [{ archived: false }, { archived: { $exists: false } }] },
+ ],
+ },
+ { fields: { _id: 1 } },
).fetchAsync()
projectList = projectList.map((value) => value._id)
diff --git a/imports/ui/pages/details/components/detailtimetable.js b/imports/ui/pages/details/components/detailtimetable.js
index 9775065a..5b379365 100644
--- a/imports/ui/pages/details/components/detailtimetable.js
+++ b/imports/ui/pages/details/components/detailtimetable.js
@@ -71,6 +71,14 @@ function detailedDataTableMapper(entry, forExport) {
mapping.push(dayjs.utc(entry.date).local().add(entry.hours, 'hour').format('HH:mm'))
}
mapping.push(Number(timeInUserUnit(entry.hours)))
+ if (getGlobalSetting('showRateInDetails')) {
+ let resourceRate
+ if (project.rates) {
+ resourceRate = project.rates[entry.userId]
+ }
+ const rate = entry.taskRate || resourceRate || project.rate || 0
+ mapping.push(rate)
+ }
mapping.push(entry._id)
return mapping
}
@@ -235,13 +243,21 @@ Template.detailtimetable.onRendered(() => {
},
)
}
- columns.push(
- {
- name: getUserTimeUnitVerbose(),
- id: 'hours',
+ columns.push({
+ name: getUserTimeUnitVerbose(),
+ id: 'hours',
+ editable: false,
+ format: numberWithUserPrecision,
+ })
+ if (getGlobalSetting('showRateInDetails')) {
+ columns.push({
+ name: t('project.rate'),
+ id: 'rate',
editable: false,
format: numberWithUserPrecision,
- },
+ })
+ }
+ columns.push(
{
name: t('navigation.edit'),
id: 'actions',
@@ -453,7 +469,11 @@ Template.detailtimetable.events({
csvArray[0] = `${csvArray[0]},${t('details.startTime')}`
csvArray[0] = `${csvArray[0]},${t('details.endTime')}`
}
- csvArray[0] = `${csvArray[0]},${getUserTimeUnitVerbose()}\r\n`
+ csvArray[0] = `${csvArray[0]},${getUserTimeUnitVerbose()}`
+ if (getGlobalSetting('showRateInDetails')) {
+ csvArray[0] = `${csvArray[0]},${t('project.rate')}`
+ }
+ csvArray[0] = `${csvArray[0]}\r\n`
const selector = structuredClone(templateInstance.selector.get()[0])
selector.state = { $in: ['new', undefined] }
for (const timeEntry of Timecards
@@ -510,6 +530,9 @@ Template.detailtimetable.events({
data[0].push(t('details.endTime'))
}
data[0].push(getUserTimeUnitVerbose())
+ if (getGlobalSetting('showRateInDetails')) {
+ data[0].push(t('project.rate'))
+ }
const selector = structuredClone(templateInstance.selector.get()[0])
selector.state = { $in: ['new', undefined] }
for (const timeEntry of Timecards
diff --git a/imports/ui/pages/track/components/tasksearch.html b/imports/ui/pages/track/components/tasksearch.html
index ee952c44..82b696ae 100644
--- a/imports/ui/pages/track/components/tasksearch.html
+++ b/imports/ui/pages/track/components/tasksearch.html
@@ -13,6 +13,13 @@
{{/if}}
+ {{#if getGlobalSetting "allowIndividualTaskRates"}}
+
+
+
+
+
+ {{/if}}
{{/if}}
diff --git a/imports/ui/pages/track/components/tasksearch.js b/imports/ui/pages/track/components/tasksearch.js
index a4265c8f..2e88e54a 100644
--- a/imports/ui/pages/track/components/tasksearch.js
+++ b/imports/ui/pages/track/components/tasksearch.js
@@ -26,6 +26,10 @@ Template.tasksearch.events({
templateInstance.filter.set('')
templateInstance.targetTask.renderIfNeeded()
},
+ 'click .js-show-task-rate': (event, templateInstance) => {
+ event.preventDefault()
+ templateInstance.$('.js-task-rate-container').toggleClass('d-none')
+ },
})
Template.tasksearch.onCreated(function tasksearchcreated() {
@@ -207,6 +211,7 @@ Template.tasksearch.onCreated(function tasksearchcreated() {
Template.tasksearch.helpers({
displayTaskSelectionIcon: () => (Template.instance()?.data?.projectId
? Template.instance()?.data?.projectId?.get() : false),
+ taskRate: () => Timecards.findOne({ _id: Template.instance().data.tcid?.get() })?.taskRate,
})
Template.tasksearch.onRendered(() => {
Template.instance().$('#edit-tc-entry-modal').on('hidden.bs.modal', () => {
diff --git a/imports/ui/pages/track/components/timetracker.html b/imports/ui/pages/track/components/timetracker.html
index cf9407c1..f6e13609 100644
--- a/imports/ui/pages/track/components/timetracker.html
+++ b/imports/ui/pages/track/components/timetracker.html
@@ -14,7 +14,7 @@
{{/if}}
@@ -25,7 +25,7 @@
{{/if}}
diff --git a/imports/ui/pages/track/components/usersearch.js b/imports/ui/pages/track/components/usersearch.js
index db434d4f..4299a372 100644
--- a/imports/ui/pages/track/components/usersearch.js
+++ b/imports/ui/pages/track/components/usersearch.js
@@ -26,7 +26,7 @@ Template.usersearch.onCreated(function usersearchcreated() {
const handle = this.subscribe('singleTimecard', tcid)
if (handle.ready()) {
const card = Timecards.findOne({ _id: tcid })
- const user = this.users.get().find((u) => u._id === card.userId)
+ const user = this.users?.get()?.find((u) => u._id === card.userId)
if (user?.profile) {
this.$('.js-usersearch-input').val(user.profile.name)
}
diff --git a/imports/ui/pages/track/tracktime.js b/imports/ui/pages/track/tracktime.js
index 793ecdbc..f3533c0d 100644
--- a/imports/ui/pages/track/tracktime.js
+++ b/imports/ui/pages/track/tracktime.js
@@ -158,6 +158,12 @@ Template.tracktime.events({
const selectedProjectElement = templateInstance.$('.js-tracktime-projectselect > div > div > .js-target-project')
templateInstance.projectId.set(selectedProjectElement.get(0).getAttribute('data-value'))
let hours = templateInstance.$('#hours').val()
+ let taskRate
+ if (getGlobalSetting('allowIndividualTaskRates')) {
+ if (templateInstance.$('#taskRate').val() && templateInstance.$('#taskRate').val() !== '0' && templateInstance.$('#taskRate').val() !== '') {
+ taskRate = Number(templateInstance.$('#taskRate').val())
+ }
+ }
if (!templateInstance.projectId.get()) {
selectedProjectElement.addClass('is-invalid')
showToast(t('notifications.select_project'))
@@ -224,7 +230,14 @@ Template.tracktime.events({
}
}
Meteor.call('updateTimeCard', {
- _id: templateInstance.tcid.get(), projectId, date, hours, task, customfields, user,
+ _id: templateInstance.tcid.get(),
+ projectId,
+ date,
+ hours,
+ task,
+ customfields,
+ user,
+ taskRate,
}, (error) => {
if (error) {
console.error(error)
@@ -244,7 +257,7 @@ Template.tracktime.events({
})
} else {
Meteor.call('insertTimeCard', {
- projectId, date, hours, task, customfields, user,
+ projectId, date, hours, task, customfields, user, taskRate,
}, (error) => {
if (error) {
console.error(error)
diff --git a/imports/ui/translations/de.json b/imports/ui/translations/de.json
index 52d4935e..73edca18 100644
--- a/imports/ui/translations/de.json
+++ b/imports/ui/translations/de.json
@@ -140,7 +140,7 @@
"zh": "Chinesisch",
"ru": "Russisch",
"ukr": "Ukrainisch",
- "es": "Spanisch",
+ "es": "Spanisch",
"user_interface": "Benutzeroberfläche",
"theme": "Thema",
"auto_detect": "Automatisch",
@@ -226,7 +226,9 @@
"google_clientid": "Google Workspace Client ID",
"google_secret": "Google Workspace Client Secret",
"openai": "Open AI API Key",
- "show_resource_in_details": "Ressource in Detailansicht anzeigen/exportieren?"
+ "show_resource_in_details": "Ressource in Detailansicht anzeigen/exportieren?",
+ "allow_individual_task_rates": "Individuelle Stundensätze für Tätigkeiten erlauben?",
+ "show_rate_in_details": "Rate in Detailansicht anzeigen/exportieren?"
},
"customer": {
"select_customer": "Kunde auswählen",
diff --git a/imports/ui/translations/en.json b/imports/ui/translations/en.json
index 3e2050d1..7bd92d41 100644
--- a/imports/ui/translations/en.json
+++ b/imports/ui/translations/en.json
@@ -140,7 +140,7 @@
"zh": "Chinese",
"ru": "Russian",
"ukr": "Ukrainian",
- "es": "Spanish",
+ "es": "Spanish",
"user_interface": "User interface",
"theme": "Theme",
"auto_detect": "Auto detect",
@@ -226,7 +226,9 @@
"google_clientid": "Google Workspace Client ID",
"google_secret": "Google Workspace Client Secret",
"openai": "Open AI API Key",
- "show_resource_in_details": "Show/export resource field in details view?"
+ "show_resource_in_details": "Show/export resource field in details view?",
+ "allow_individual_task_rates": "Allow individual task rates?",
+ "show_rate_in_details": "Show/export rate field in details view?"
},
"customer": {
"select_customer": "Select a customer",
diff --git a/imports/ui/translations/es.json b/imports/ui/translations/es.json
index e36838fc..08649a9a 100644
--- a/imports/ui/translations/es.json
+++ b/imports/ui/translations/es.json
@@ -140,12 +140,11 @@
"zh": "Chino",
"ru": "Ruso",
"ukr": "Ucraniano",
- "es": "Español",
+ "es": "Español",
"user_interface": "Interfaz de Usuario",
"theme": "Tema",
"auto_detect": "Detectar Automáticamente",
- "light_theme":
- "Claro",
+ "light_theme": "Claro",
"dark_theme": "Oscuro",
"time_tracking": "Seguimiento de Tiempo",
"track_time_view": "Vista Predeterminada del Seguimiento de Tiempo",
@@ -227,7 +226,9 @@
"google_clientid": "ID de Cliente de Google Workspace",
"google_secret": "Secreto de Cliente de Google Workspace",
"openai": "Clave API de Open AI",
- "show_resource_in_details": "¿Mostrar/exportar campo de recurso en la vista de detalles?"
+ "show_resource_in_details": "¿Mostrar/exportar campo de recurso en la vista de detalles?",
+ "allow_individual_task_rates": "¿Permitir tarifas individuales por tarea?",
+ "show_rate_in_details": "¿Mostrar/exportar campo de tarifa en la vista de detalles?"
},
"customer": {
"select_customer": "Seleccionar un Cliente",
diff --git a/imports/ui/translations/fr.json b/imports/ui/translations/fr.json
index c0739a97..caa45136 100644
--- a/imports/ui/translations/fr.json
+++ b/imports/ui/translations/fr.json
@@ -132,7 +132,7 @@
"zh": "Chinois",
"ru": "Russe",
"ukr": "Ukrainien",
- "es": "Espagnol",
+ "es": "Espagnol",
"user_interface": "Interface utilisateur",
"theme": "Thème",
"auto_detect": "Détection automatique",
@@ -216,7 +216,9 @@
"google_clientid": "Google Workspace Client ID",
"google_secret": "Google Workspace Client Secret",
"openai": "Open AI API Key",
- "show_resource_in_details": "Afficher/exporter le champ ressource dans la vue détaillée ?"
+ "show_resource_in_details": "Afficher/exporter le champ ressource dans la vue détaillée ?",
+ "allow_individual_task_rates": "Autoriser des taux de tâche individuels?",
+ "show_rate_in_details": "Afficher/exporter le champ taux dans la vue détaillée ?"
},
"customer": {
"select_customer": "Selectionner un client",
diff --git a/imports/ui/translations/ru.json b/imports/ui/translations/ru.json
index cabf6cf5..c343e574 100644
--- a/imports/ui/translations/ru.json
+++ b/imports/ui/translations/ru.json
@@ -140,7 +140,7 @@
"zh": "Китайский",
"ru": "Русский",
"ukr": "Украинский",
- "es": "Испанский",
+ "es": "Испанский",
"user_interface": "Пользовательский интерфейс",
"theme": "Тема",
"auto_detect": "Автоопределение",
@@ -224,7 +224,9 @@
"google_clientid": "Google Workspace Client ID",
"google_secret": "Google Workspace Client Secret",
"openai": "Open AI API Key",
- "show_resource_in_details": "Отображать/экспортировать поле ресурса на странице 'Детали'?"
+ "show_resource_in_details": "Отображать/экспортировать поле ресурса на странице 'Детали'?",
+ "allow_individual_task_rates": "Разрешить индивидуальные ставки задач?",
+ "show_rate_in_details": "Отображать/экспортировать ставку на странице 'Детали'?"
},
"customer": {
"select_customer": "Выберите клиента",
diff --git a/imports/ui/translations/ukr.json b/imports/ui/translations/ukr.json
index 3dff4596..701920d3 100644
--- a/imports/ui/translations/ukr.json
+++ b/imports/ui/translations/ukr.json
@@ -140,7 +140,7 @@
"zh": "Китайська",
"ru": "Російська",
"ukr": "Українська",
- "es": "Іспанська",
+ "es": "Іспанська",
"user_interface": "Інтерфейс користувача",
"theme": "Тема",
"auto_detect": "Автоматичне визначення",
@@ -224,7 +224,9 @@
"google_clientid": "Google Workspace Client ID",
"google_secret": "Google Workspace Client Secret",
"openai": "Open AI API Key",
- "show_resource_in_details": "Відображати/експортувати поле ресурсу на сторінці 'Деталі'?"
+ "show_resource_in_details": "Відображати/експортувати поле ресурсу на сторінці 'Деталі'?",
+ "allow_individual_task_rates": "Дозволити індивідуальні ставки за завдання?",
+ "show_rate_in_details": "Відображати/експортувати ставку на сторінці 'Деталі'?"
},
"customer": {
"select_customer": "Виберіть клієнта",
diff --git a/imports/ui/translations/zh.json b/imports/ui/translations/zh.json
index 4cec8106..b1f6e4d9 100644
--- a/imports/ui/translations/zh.json
+++ b/imports/ui/translations/zh.json
@@ -124,7 +124,7 @@
"zh": "中文",
"ru": "俄语",
"ukr": "乌克兰语",
- "es": "西班牙语",
+ "es": "西班牙语",
"user_interface": "用户界面",
"theme": "主题",
"auto_detect": "自动探测",
@@ -184,7 +184,9 @@
"google_clientid": "Google Workspace Client ID",
"google_secret": "Google Workspace Client Secret",
"openai": "Open AI API Key",
- "show_resource_in_details": "在详细视图中显示/导出资源字段?"
+ "show_resource_in_details": "在详细视图中显示/导出资源字段",
+ "allow_individual_task_rates": "允许单独任务费率",
+ "show_rate_in_details": "在详细视图中显示/导出费率字段"
},
"customer": {
"select_customer": "请选一个客户",
diff --git a/imports/utils/server_method_helpers.js b/imports/utils/server_method_helpers.js
index 5d47f5f2..ba80d39e 100644
--- a/imports/utils/server_method_helpers.js
+++ b/imports/utils/server_method_helpers.js
@@ -29,21 +29,27 @@ function getProjectListById(projectId) {
if (projectId.includes('all')) {
projectList = Projects.find(
{
- $or: [{ userId }, { public: true }, { team: userId }],
+ $and: [
+ { $or: [{ userId }, { public: true }, { team: userId }] },
+ { $or: [{ archived: false }, { archived: { $exists: false } }] },
+ ],
},
- { $fields: { _id: 1 } },
+ { fields: { _id: 1 } },
).fetch().map((value) => value._id)
} else {
const projectSelector = {
_id: projectId,
- $or: [{ userId }, { public: true }, { team: userId }],
+ $and: [
+ { $or: [{ userId }, { public: true }, { team: userId }] },
+ { $or: [{ archived: false }, { archived: { $exists: false } }] },
+ ],
}
if (projectId instanceof Array) {
projectSelector._id = { $in: projectId }
}
projectList = Projects.find(
projectSelector,
- { $fields: { _id: 1 } },
+ { fields: { _id: 1 } },
).fetch().map((value) => value._id)
}
return projectList
@@ -85,13 +91,20 @@ function getProjectListByCustomer(customer) {
if (customer.includes('all')) {
projects = Projects.find(
{
- $or: [{ userId }, { public: true }, { team: userId }],
+ $and: [
+ { $or: [{ userId }, { public: true }, { team: userId }] },
+ { $or: [{ archived: false }, { archived: { $exists: false } }] },
+ ],
},
{ _id: 1, name: 1 },
)
} else {
const selector = {
- customer, $or: [{ userId }, { public: true }, { team: userId }],
+ customer,
+ $and: [
+ { $or: [{ userId }, { public: true }, { team: userId }] },
+ { $or: [{ archived: false }, { archived: { $exists: false } }] },
+ ],
}
if (customer instanceof Array) {
selector.customer = { $in: customer }
@@ -513,7 +526,7 @@ function buildDetailedTimeEntriesForPeriodSelector({
filters.projectId = { $in: projectIds }
delete filters[filterKey]
} else if (filterKey === 'state' && filterValue === 'new') {
- filters['$or'] = [{ state: { $exists: false } }, { state: 'new' }]
+ filters.$or = [{ state: { $exists: false } }, { state: 'new' }]
delete filters[filterKey]
} else if (filterKey === 'date' && typeof filters[filterKey] === 'string') {
dayjs.extend(customParseFormat)
diff --git a/package-lock.json b/package-lock.json
index 4ee87fde..6994a820 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "titra",
- "version": "0.96.9",
+ "version": "0.96.11",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -11,46 +11,46 @@
"dev": true
},
"@ampproject/remapping": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
- "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"requires": {
- "@jridgewell/gen-mapping": "^0.3.0",
- "@jridgewell/trace-mapping": "^0.3.9"
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
}
},
"@babel/code-frame": {
- "version": "7.23.5",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
- "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
+ "version": "7.24.2",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
+ "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
"dev": true,
"requires": {
- "@babel/highlight": "^7.23.4",
- "chalk": "^2.4.2"
+ "@babel/highlight": "^7.24.2",
+ "picocolors": "^1.0.0"
}
},
"@babel/compat-data": {
- "version": "7.23.5",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
- "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
+ "version": "7.24.1",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz",
+ "integrity": "sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==",
"dev": true
},
"@babel/core": {
- "version": "7.24.0",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
- "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
+ "version": "7.24.3",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz",
+ "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==",
"dev": true,
"requires": {
"@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.23.5",
- "@babel/generator": "^7.23.6",
+ "@babel/code-frame": "^7.24.2",
+ "@babel/generator": "^7.24.1",
"@babel/helper-compilation-targets": "^7.23.6",
"@babel/helper-module-transforms": "^7.23.3",
- "@babel/helpers": "^7.24.0",
- "@babel/parser": "^7.24.0",
+ "@babel/helpers": "^7.24.1",
+ "@babel/parser": "^7.24.1",
"@babel/template": "^7.24.0",
- "@babel/traverse": "^7.24.0",
+ "@babel/traverse": "^7.24.1",
"@babel/types": "^7.24.0",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -68,9 +68,9 @@
}
},
"@babel/eslint-parser": {
- "version": "7.23.10",
- "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.10.tgz",
- "integrity": "sha512-3wSYDPZVnhseRnxRJH6ZVTNknBz76AEnyC+AYYhasjP3Yy23qz0ERR7Fcd2SHmYuSFJ2kY9gaaDd3vyqU09eSw==",
+ "version": "7.24.1",
+ "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.1.tgz",
+ "integrity": "sha512-d5guuzMlPeDfZIbpQ8+g1NaCNuAGBBGNECh0HVqz1sjOeVLh2CEaifuOysCH18URW6R7pqXINvf5PaR/dC6jLQ==",
"dev": true,
"requires": {
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
@@ -87,14 +87,14 @@
}
},
"@babel/generator": {
- "version": "7.23.6",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
- "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
+ "version": "7.24.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz",
+ "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==",
"dev": true,
"requires": {
- "@babel/types": "^7.23.6",
- "@jridgewell/gen-mapping": "^0.3.2",
- "@jridgewell/trace-mapping": "^0.3.17",
+ "@babel/types": "^7.24.0",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1"
}
},
@@ -160,12 +160,12 @@
}
},
"@babel/helper-module-imports": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
- "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
+ "version": "7.24.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
+ "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
"dev": true,
"requires": {
- "@babel/types": "^7.22.15"
+ "@babel/types": "^7.24.0"
}
},
"@babel/helper-module-transforms": {
@@ -200,9 +200,9 @@
}
},
"@babel/helper-string-parser": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
- "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
+ "version": "7.24.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
+ "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
"dev": true
},
"@babel/helper-validator-identifier": {
@@ -218,37 +218,38 @@
"dev": true
},
"@babel/helpers": {
- "version": "7.24.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz",
- "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==",
+ "version": "7.24.1",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz",
+ "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==",
"dev": true,
"requires": {
"@babel/template": "^7.24.0",
- "@babel/traverse": "^7.24.0",
+ "@babel/traverse": "^7.24.1",
"@babel/types": "^7.24.0"
}
},
"@babel/highlight": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
- "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
+ "version": "7.24.2",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
+ "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
- "js-tokens": "^4.0.0"
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
}
},
"@babel/parser": {
- "version": "7.24.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
- "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
+ "version": "7.24.1",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz",
+ "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==",
"dev": true
},
"@babel/runtime": {
- "version": "7.24.0",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
- "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
+ "version": "7.24.1",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
+ "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==",
"requires": {
"regenerator-runtime": "^0.14.0"
}
@@ -265,18 +266,18 @@
}
},
"@babel/traverse": {
- "version": "7.24.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz",
- "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==",
+ "version": "7.24.1",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz",
+ "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==",
"dev": true,
"requires": {
- "@babel/code-frame": "^7.23.5",
- "@babel/generator": "^7.23.6",
+ "@babel/code-frame": "^7.24.1",
+ "@babel/generator": "^7.24.1",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.24.0",
+ "@babel/parser": "^7.24.1",
"@babel/types": "^7.24.0",
"debug": "^4.3.1",
"globals": "^11.1.0"
@@ -405,14 +406,14 @@
"dev": true
},
"@jridgewell/gen-mapping": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz",
- "integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==",
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
+ "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dev": true,
"requires": {
- "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
- "@jridgewell/trace-mapping": "^0.3.9"
+ "@jridgewell/trace-mapping": "^0.3.24"
}
},
"@jridgewell/resolve-uri": {
@@ -434,9 +435,9 @@
"dev": true
},
"@jridgewell/trace-mapping": {
- "version": "0.3.23",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz",
- "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==",
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -1802,9 +1803,9 @@
"dev": true
},
"caniuse-lite": {
- "version": "1.0.30001591",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
- "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
+ "version": "1.0.30001599",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz",
+ "integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==",
"dev": true
},
"chalk": {
@@ -2062,9 +2063,9 @@
}
},
"electron-to-chromium": {
- "version": "1.4.687",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.687.tgz",
- "integrity": "sha512-Ic85cOuXSP6h7KM0AIJ2hpJ98Bo4hyTUjc4yjMbkvD+8yTxEhfK9+8exT2KKYsSjnCn2tGsKVSZwE7ZgTORQCw==",
+ "version": "1.4.713",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.713.tgz",
+ "integrity": "sha512-vDarADhwntXiULEdmWd77g2dV6FrNGa8ecAC29MZ4TwPut2fvosD0/5sJd1qWNNe8HcJFAC+F5Lf9jW1NPtWmw==",
"dev": true
},
"emoji-regex": {
diff --git a/package.json b/package.json
index 170322e3..facab29f 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,12 @@
{
"name": "titra",
- "version": "0.96.11",
+ "version": "0.97.0",
"private": true,
"scripts": {
"start": "meteor run"
},
"dependencies": {
- "@babel/runtime": "^7.24.0",
+ "@babel/runtime": "^7.24.1",
"@dashboardcode/bsmultiselect": "^1.1.18",
"@fortawesome/fontawesome-free": "^6.5.1",
"@fullcalendar/core": "6.1.11",
@@ -44,8 +44,8 @@
"vm2": "^3.9.19"
},
"devDependencies": {
- "@babel/core": "^7.24.0",
- "@babel/eslint-parser": "^7.23.10",
+ "@babel/core": "^7.24.3",
+ "@babel/eslint-parser": "^7.24.1",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-import-resolver-meteor": "^0.4.0",