From e3b602c273efbc3d610d151492cddaeb60b6ca7d Mon Sep 17 00:00:00 2001 From: Fabian Kromer Date: Thu, 21 Mar 2024 15:08:45 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20added=20individual=20task=20rates?= =?UTF-8?q?=20feature=20(implements=20#199)=20=F0=9F=90=9B=20fixed=20archi?= =?UTF-8?q?ved=20project=20times=20showing=20up=20when=20searching=20on=20?= =?UTF-8?q?the=20details=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- imports/api/dashboards/server/publications.js | 2 +- imports/api/globalsettings/globalsettings.js | 10 +- imports/api/projects/server/publications.js | 26 +++- imports/api/timecards/server/methods.js | 41 +++-- imports/api/timecards/server/publications.js | 9 +- .../details/components/detailtimetable.js | 35 ++++- .../ui/pages/track/components/tasksearch.html | 7 + .../ui/pages/track/components/tasksearch.js | 5 + .../pages/track/components/timetracker.html | 4 +- .../ui/pages/track/components/usersearch.js | 2 +- imports/ui/pages/track/tracktime.js | 17 +- imports/ui/translations/de.json | 6 +- imports/ui/translations/en.json | 6 +- imports/ui/translations/es.json | 9 +- imports/ui/translations/fr.json | 6 +- imports/ui/translations/ru.json | 6 +- imports/ui/translations/ukr.json | 6 +- imports/ui/translations/zh.json | 6 +- imports/utils/server_method_helpers.js | 27 +++- package-lock.json | 145 +++++++++--------- package.json | 8 +- 21 files changed, 249 insertions(+), 134 deletions(-) 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",