diff --git a/client/components/cards/cardCustomFields.js b/client/components/cards/cardCustomFields.js index 333d5e5cd..82c025503 100644 --- a/client/components/cards/cardCustomFields.js +++ b/client/components/cards/cardCustomFields.js @@ -1,8 +1,10 @@ import { TAPi18n } from '/imports/i18n'; import { DatePicker } from '/client/lib/datepicker'; +import { ReactiveCache } from '/imports/reactiveCache'; import { formatDateTime, formatDate, + formatDateByUserPreference, formatTime, getISOWeek, isValidDate, @@ -177,7 +179,9 @@ CardCustomField.register('cardCustomField'); } showDate() { - return calendar(this.date.get()); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(this.date.get(), dateFormat, true); } showISODate() { diff --git a/client/components/cards/cardDate.js b/client/components/cards/cardDate.js index 29e370b1c..d5649fae1 100644 --- a/client/components/cards/cardDate.js +++ b/client/components/cards/cardDate.js @@ -3,6 +3,7 @@ import { DatePicker } from '/client/lib/datepicker'; import { formatDateTime, formatDate, + formatDateByUserPreference, formatTime, getISOWeek, isValidDate, @@ -131,7 +132,9 @@ const CardDate = BlazeComponent.extendComponent({ }, showDate() { - return calendar(this.date.get()); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(this.date.get(), dateFormat, true); }, showISODate() { @@ -166,7 +169,10 @@ class CardReceivedDate extends CardDate { } showTitle() { - return `${TAPi18n.__('card-received-on')} ${format(this.date.get(), 'LLLL')}`; + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true); + return `${TAPi18n.__('card-received-on')} ${formattedDate}`; } events() { @@ -201,7 +207,10 @@ class CardStartDate extends CardDate { } showTitle() { - return `${TAPi18n.__('card-start-on')} ${format(this.date.get(), 'LLLL')}`; + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true); + return `${TAPi18n.__('card-start-on')} ${formattedDate}`; } events() { @@ -237,7 +246,10 @@ class CardDueDate extends CardDate { } showTitle() { - return `${TAPi18n.__('card-due-on')} ${format(this.date.get(), 'LLLL')}`; + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true); + return `${TAPi18n.__('card-due-on')} ${formattedDate}`; } events() { @@ -315,7 +327,10 @@ class CardCustomFieldDate extends CardDate { } showTitle() { - return `${format(this.date.get(), 'LLLL')}`; + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true); + return `${formattedDate}`; } classes() { @@ -334,7 +349,9 @@ CardCustomFieldDate.register('cardCustomFieldDate'); } showDate() { - return format(this.date.get(), 'L'); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(this.date.get(), dateFormat, true); } }.register('minicardReceivedDate')); @@ -344,7 +361,9 @@ CardCustomFieldDate.register('cardCustomFieldDate'); } showDate() { - return format(this.date.get(), 'YYYY-MM-DD HH:mm'); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(this.date.get(), dateFormat, true); } }.register('minicardStartDate')); @@ -354,7 +373,9 @@ CardCustomFieldDate.register('cardCustomFieldDate'); } showDate() { - return format(this.date.get(), 'YYYY-MM-DD HH:mm'); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(this.date.get(), dateFormat, true); } }.register('minicardDueDate')); @@ -364,7 +385,9 @@ CardCustomFieldDate.register('cardCustomFieldDate'); } showDate() { - return format(this.date.get(), 'YYYY-MM-DD HH:mm'); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(this.date.get(), dateFormat, true); } }.register('minicardEndDate')); @@ -374,7 +397,9 @@ CardCustomFieldDate.register('cardCustomFieldDate'); } showDate() { - return format(this.date.get(), 'L'); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(this.date.get(), dateFormat, true); } }.register('minicardCustomFieldDate')); @@ -391,7 +416,9 @@ class VoteEndDate extends CardDate { return classes; } showDate() { - return format(this.date.get(), 'L') + ' ' + format(this.date.get(), 'HH:mm'); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(this.date.get(), dateFormat, true); } showTitle() { return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`; @@ -418,7 +445,9 @@ class PokerEndDate extends CardDate { return classes; } showDate() { - return format(this.date.get(), 'l LT'); + const currentUser = ReactiveCache.getCurrentUser(); + const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD'; + return formatDateByUserPreference(this.date.get(), dateFormat, true); } showTitle() { return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`; diff --git a/client/components/cards/cardDetails.css b/client/components/cards/cardDetails.css index 8fbf0f717..fb68b2957 100644 --- a/client/components/cards/cardDetails.css +++ b/client/components/cards/cardDetails.css @@ -1,3 +1,31 @@ +/* Date Format Selector */ +.card-details-item-date-format { + margin-bottom: 10px; +} + +.card-details-item-date-format .card-details-item-title { + font-size: 14px; + font-weight: bold; + margin-bottom: 5px; + color: #333; +} + +.card-details-item-date-format .js-date-format-selector { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #fff; + font-size: 14px; + cursor: pointer; +} + +.card-details-item-date-format .js-date-format-selector:focus { + outline: none; + border-color: #007cba; + box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2); +} + .assignee { border-radius: 3px; display: block; diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index b3d44a52b..90b3fef31 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -113,6 +113,16 @@ template(name="cardDetails") if currentBoard.hasAnyAllowsDate hr + .card-details-item.card-details-item-date-format + h3.card-details-item-title + | 📅 + | {{_ 'date-format'}} + .card-details-item-content + select.js-date-format-selector + option(value="YYYY-MM-DD" selected="{{#if isDateFormat 'YYYY-MM-DD'}}selected{{/if}}") {{_ 'date-format-yyyy-mm-dd'}} + option(value="DD-MM-YYYY" selected="{{#if isDateFormat 'DD-MM-YYYY'}}selected{{/if}}") {{_ 'date-format-dd-mm-yyyy'}} + option(value="MM-DD-YYYY" selected="{{#if isDateFormat 'MM-DD-YYYY'}}selected{{/if}}") {{_ 'date-format-mm-dd-yyyy'}} + if currentBoard.allowsReceivedDate .card-details-item.card-details-item-received h3.card-details-item-title diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index df9e33a81..bbbd49a73 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -306,6 +306,10 @@ BlazeComponent.extendComponent({ const $tooltip = this.$('.card-details-header .copied-tooltip'); Utils.showCopied(promise, $tooltip); }, + 'change .js-date-format-selector'(event) { + const dateFormat = event.target.value; + Meteor.call('changeDateFormat', dateFormat); + }, 'click .js-open-card-details-menu': Popup.open('cardDetailsActions'), 'submit .js-card-description'(event) { event.preventDefault(); @@ -568,6 +572,11 @@ Template.cardDetails.helpers({ let ret = !!Utils.getPopupCardId(); return ret; }, + isDateFormat(format) { + const currentUser = ReactiveCache.getCurrentUser(); + if (!currentUser) return format === 'YYYY-MM-DD'; + return currentUser.getDateFormat() === format; + }, // Upload progress helpers hasActiveUploads() { return uploadProgressManager.hasActiveUploads(this._id); diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json index 48daee11b..ce889c142 100644 --- a/imports/i18n/data/en.i18n.json +++ b/imports/i18n/data/en.i18n.json @@ -354,6 +354,10 @@ "custom-field-text": "Text", "custom-fields": "Custom Fields", "date": "Date", + "date-format": "Date Format", + "date-format-yyyy-mm-dd": "YYYY-MM-DD HH:MM", + "date-format-dd-mm-yyyy": "DD-MM-YYYY HH:MM", + "date-format-mm-dd-yyyy": "MM-DD-YYYY HH:MM", "decline": "Decline", "default-avatar": "Default avatar", "delete": "Delete", diff --git a/imports/lib/dateUtils.js b/imports/lib/dateUtils.js index 2e781befc..884763488 100644 --- a/imports/lib/dateUtils.js +++ b/imports/lib/dateUtils.js @@ -36,6 +36,44 @@ export function formatDate(date) { return `${year}-${month}-${day}`; } +/** + * Format a date according to user's preferred format + * @param {Date|string} date - Date to format + * @param {string} format - Format string (YYYY-MM-DD, DD-MM-YYYY, MM-DD-YYYY) + * @param {boolean} includeTime - Whether to include time (HH:MM) + * @returns {string} Formatted date string + */ +export function formatDateByUserPreference(date, format = 'YYYY-MM-DD', includeTime = true) { + const d = new Date(date); + if (isNaN(d.getTime())) return ''; + + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hours = String(d.getHours()).padStart(2, '0'); + const minutes = String(d.getMinutes()).padStart(2, '0'); + + let dateString; + switch (format) { + case 'DD-MM-YYYY': + dateString = `${day}-${month}-${year}`; + break; + case 'MM-DD-YYYY': + dateString = `${month}-${day}-${year}`; + break; + case 'YYYY-MM-DD': + default: + dateString = `${year}-${month}-${day}`; + break; + } + + if (includeTime) { + return `${dateString} ${hours}:${minutes}`; + } + + return dateString; +} + /** * Format a time to HH:mm format * @param {Date|string} date - Date to format diff --git a/models/users.js b/models/users.js index 0d49d0579..712098e55 100644 --- a/models/users.js +++ b/models/users.js @@ -465,6 +465,15 @@ Users.attachSchema( type: Boolean, defaultValue: true, }, + 'profile.dateFormat': { + /** + * User-specified date format for displaying dates (includes time HH:MM). + */ + type: String, + optional: true, + allowedValues: ['YYYY-MM-DD', 'DD-MM-YYYY', 'MM-DD-YYYY'], + defaultValue: 'YYYY-MM-DD', + }, 'profile.zoomLevel': { /** * User-specified zoom level for board view (1.0 = 100%, 1.5 = 150%, etc.) @@ -1049,6 +1058,11 @@ Users.helpers({ return profile.startDayOfWeek; }, + getDateFormat() { + const profile = this.profile || {}; + return profile.dateFormat || 'YYYY-MM-DD'; + }, + getTemplatesBoardId() { return (this.profile || {}).templatesBoardId; }, @@ -1452,6 +1466,14 @@ Users.mutations({ }; }, + setDateFormat(dateFormat) { + return { + $set: { + 'profile.dateFormat': dateFormat, + }, + }; + }, + setBoardView(view) { return { $set: { @@ -1597,6 +1619,10 @@ Meteor.methods({ check(startDay, Number); ReactiveCache.getCurrentUser().setStartDayOfWeek(startDay); }, + changeDateFormat(dateFormat) { + check(dateFormat, String); + ReactiveCache.getCurrentUser().setDateFormat(dateFormat); + }, applyListWidth(boardId, listId, width, constraint) { check(boardId, String); check(listId, String);