diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js
index 62629252d..74441021e 100644
--- a/client/components/activities/comments.js
+++ b/client/components/activities/comments.js
@@ -57,8 +57,9 @@ BlazeComponent.extendComponent({
BlazeComponent.extendComponent({
getComments() {
- const ret = this.data().comments();
- return ret;
+ const data = this.data();
+ if (!data || typeof data.comments !== 'function') return [];
+ return data.comments();
},
}).register("comments");
diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade
index 3f6e9dcb5..1a6535203 100644
--- a/client/components/boards/boardBody.jade
+++ b/client/components/boards/boardBody.jade
@@ -69,11 +69,4 @@ template(name="calendarView")
.calendar-view.swimlane
if currentCard
+cardDetails(currentCard)
- +fullcalendar(calendarOptions)
-template(name="ganttView")
- if isViewGantt
- .gantt-view.swimlane
- if currentCard
- +cardDetails(currentCard)
- .gantt-container
- #gantt-chart
\ No newline at end of file
+ +fullcalendar(calendarOptions)
\ No newline at end of file
diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js
index 8070f3019..4a22e07af 100644
--- a/client/components/boards/boardBody.js
+++ b/client/components/boards/boardBody.js
@@ -1,4 +1,5 @@
import { ReactiveCache } from '/imports/reactiveCache';
+import '../gantt/gantt.js';
import { TAPi18n } from '/imports/i18n';
import dragscroll from '@wekanteam/dragscroll';
import { boardConverter } from '/client/lib/boardConverter';
@@ -1436,268 +1437,4 @@ BlazeComponent.extendComponent({
* Gantt View Component
* Displays cards as a Gantt chart with start/due dates
*/
-BlazeComponent.extendComponent({
- template() {
- return 'ganttView';
- },
- onCreated() {
- this.autorun(() => {
- const board = Utils.getCurrentBoard();
- if (board) {
- // Subscribe to cards for the current board
- this.subscribe('allCards', board._id);
- this.subscribe('allLists', board._id);
- }
- });
- },
-
- onRendered() {
- this.autorun(() => {
- const board = Utils.getCurrentBoard();
- if (board && this.subscriptionsReady()) {
- this.renderGanttChart();
- }
- });
- },
-
- renderGanttChart() {
- const board = Utils.getCurrentBoard();
- if (!board) return;
-
- const ganttContainer = document.getElementById('gantt-chart');
- if (!ganttContainer) return;
-
- // Clear previous content
- ganttContainer.innerHTML = '';
-
- // Get all cards for the board
- const cards = Cards.find({ boardId: board._id }, { sort: { startAt: 1, dueAt: 1 } }).fetch();
-
- if (cards.length === 0) {
- ganttContainer.innerHTML = `
${TAPi18n.__('no-cards-in-gantt')}
`;
- return;
- }
-
- // Create a weekly HTML gantt view
- this.createWeeklyGanttView(cards, ganttContainer);
- },
- createWeeklyGanttView(cards, container) {
- const today = new Date();
- const currentUser = ReactiveCache.getCurrentUser && ReactiveCache.getCurrentUser();
- const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
-
- // Helpers to compute ISO week and start/end of week
- const getISOWeekInfo = d => {
- const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
- const dayNum = date.getUTCDay() || 7;
- date.setUTCDate(date.getUTCDate() + 4 - dayNum);
- const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
- const week = Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
- return { year: date.getUTCFullYear(), week };
- };
- const startOfISOWeek = d => {
- const date = new Date(d);
- const day = date.getDay() || 7; // Sunday -> 7
- if (day !== 1) date.setDate(date.getDate() - (day - 1));
- date.setHours(0,0,0,0);
- return date;
- };
-
- // Collect weeks that have any dates on cards
- const weeksMap = new Map(); // key: `${year}-W${week}` -> { year, week, start }
- const relevantCards = cards.filter(c => c.receivedAt || c.startAt || c.dueAt || c.endAt);
- relevantCards.forEach(card => {
- ['receivedAt','startAt','dueAt','endAt'].forEach(field => {
- if (card[field]) {
- const dt = new Date(card[field]);
- const info = getISOWeekInfo(dt);
- const key = `${info.year}-W${info.week}`;
- if (!weeksMap.has(key)) {
- weeksMap.set(key, { year: info.year, week: info.week, start: startOfISOWeek(dt) });
- }
- }
- });
- });
-
- // Sort weeks by start ascending (oldest first)
- const weeks = Array.from(weeksMap.values()).sort((a,b) => a.start - b.start);
-
- // Weekday labels
- const weekdayKeys = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];
- const weekdayLabels = weekdayKeys.map(k => TAPi18n.__(k));
-
- // Build HTML for all week tables
- let html = '';
- weeks.forEach(weekInfo => {
- const weekStart = new Date(weekInfo.start);
- const weekDates = Array.from({length:7}, (_,i) => {
- const d = new Date(weekStart);
- d.setDate(d.getDate() + i);
- d.setHours(0,0,0,0);
- return d;
- });
-
- // Table header
- html += '';
- html += '';
- html += '';
- const taskHeader = `${TAPi18n.__('task')} ${TAPi18n.__('predicate-week')} ${weekInfo.week}`;
- html += `| ${taskHeader} | `;
- weekdayLabels.forEach((lbl, idx) => {
- const formattedDate = formatDateByUserPreference(weekDates[idx], dateFormat, false);
- html += `${formattedDate} ${lbl} | `;
- });
- html += '
';
-
- // Rows: include cards that have any date in this week
- html += '';
- relevantCards.forEach(card => {
- const cardDates = {
- receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
- startAt: card.startAt ? new Date(card.startAt) : null,
- dueAt: card.dueAt ? new Date(card.dueAt) : null,
- endAt: card.endAt ? new Date(card.endAt) : null,
- };
- const isInWeek = Object.values(cardDates).some(dt => dt && getISOWeekInfo(dt).week === weekInfo.week && getISOWeekInfo(dt).year === weekInfo.year);
- if (!isInWeek) return;
-
- // Row header cell (task title)
- html += '';
- html += `| ${card.title} | `;
-
- // Weekday cells with icons/colors only on exact matching dates
- weekDates.forEach((dayDate, idx) => {
- let cellContent = '';
- let cellClass = '';
- let cellStyle = '';
- let cellTitle = '';
- let cellDateType = '';
-
- // Highlight today and weekends
- const isToday = dayDate.toDateString() === today.toDateString();
- if (isToday) {
- cellClass += ' ganttview-today';
- cellStyle += 'background-color: #fcf8e3 !important;';
- }
- const isWeekend = idx >= 5; // Saturday/Sunday
- if (isWeekend) {
- cellClass += ' ganttview-weekend';
- if (!isToday) cellStyle += 'background-color: #efefef !important;';
- }
-
- // Match specific date types
- if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === dayDate.toDateString()) {
- cellContent = '📥';
- cellStyle = 'background-color: #dbdbdb !important; color: #000 !important; font-size: 18px !important; font-weight: bold !important;';
- cellTitle = TAPi18n.__('card-received');
- cellDateType = 'received';
- }
- if (cardDates.startAt && cardDates.startAt.toDateString() === dayDate.toDateString()) {
- cellContent = '🚀';
- cellStyle = 'background-color: #90ee90 !important; color: #000 !important; font-size: 18px !important; font-weight: bold !important;';
- cellTitle = TAPi18n.__('card-start');
- cellDateType = 'start';
- }
- if (cardDates.dueAt && cardDates.dueAt.toDateString() === dayDate.toDateString()) {
- cellContent = '⏰';
- cellStyle = 'background-color: #ffd700 !important; color: #000 !important; font-size: 18px !important; font-weight: bold !important;';
- cellTitle = TAPi18n.__('card-due');
- cellDateType = 'due';
- }
- if (cardDates.endAt && cardDates.endAt.toDateString() === dayDate.toDateString()) {
- cellContent = '🏁';
- cellStyle = 'background-color: #ffb3b3 !important; color: #000 !important; font-size: 18px !important; font-weight: bold !important;';
- cellTitle = TAPi18n.__('card-end');
- cellDateType = 'end';
- }
-
- if (cellDateType) {
- cellClass += ' js-gantt-date-icon';
- }
- const cellDataAttrs = cellDateType ? ` data-card-id="${card._id}" data-date-type="${cellDateType}"` : '';
-
- html += `${cellContent} | `;
- });
-
- // Close row
- html += '
';
- });
-
- // Close section for this week
- html += '
';
- });
-
- container.innerHTML = html;
-
- // Add click handlers
- const taskCells = container.querySelectorAll('.js-gantt-task-cell');
- taskCells.forEach(cell => {
- cell.addEventListener('click', (e) => {
- const cardId = e.currentTarget.dataset.cardId;
- const card = ReactiveCache.getCard(cardId);
- if (!card) return;
-
- // Scroll the gantt container and viewport to top so the card details are visible
- if (container && typeof container.scrollIntoView === 'function') {
- container.scrollIntoView({ behavior: 'smooth', block: 'start' });
- }
- if (typeof window !== 'undefined' && typeof window.scrollTo === 'function') {
- window.scrollTo({ top: 0, behavior: 'smooth' });
- }
- const contentEl = document.getElementById('content');
- if (contentEl && typeof contentEl.scrollTo === 'function') {
- contentEl.scrollTo({ top: 0, behavior: 'smooth' });
- }
-
- // Open card the same way as clicking a minicard - set currentCard session
- // This shows the full card details overlay, not a popup
- // In desktop mode, add to openCards array to support multiple cards
- const isMobile = Utils.getMobileMode();
- if (!isMobile) {
- const openCards = Session.get('openCards') || [];
- if (!openCards.includes(cardId)) {
- openCards.push(cardId);
- Session.set('openCards', openCards);
- }
- }
- Session.set('currentCard', cardId);
- });
- });
-
- // Date icon click handlers: open the same edit popups as in swimlane cards
- const dateIconCells = container.querySelectorAll('.js-gantt-date-icon');
- dateIconCells.forEach(cell => {
- cell.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- const cardId = e.currentTarget.dataset.cardId;
- const dateType = e.currentTarget.dataset.dateType;
- const card = ReactiveCache.getCard(cardId);
- if (!card || !dateType) return;
-
- const popupMap = {
- received: 'editCardReceivedDate',
- start: 'editCardStartDate',
- due: 'editCardDueDate',
- end: 'editCardEndDate',
- };
- const popupName = popupMap[dateType];
- if (!popupName || !Popup || typeof Popup.open !== 'function') return;
-
- const openFn = Popup.open(popupName);
- // Supply the card as data context for the popup
- openFn.call({ currentData: () => card }, e, { dataContextIfCurrentDataIsUndefined: card });
- });
- });
- },
-
- isViewGantt() {
- const currentUser = ReactiveCache.getCurrentUser();
- if (currentUser) {
- return (currentUser.profile || {}).boardView === 'board-view-gantt';
- } else {
- return window.localStorage.getItem('boardView') === 'board-view-gantt';
- }
- },
-}).register('ganttView');
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index c104ca143..99eeaee5c 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -85,6 +85,7 @@ BlazeComponent.extendComponent({
isWatching() {
const card = this.currentData();
+ if (!card || typeof card.findWatcher !== 'function') return false;
return card.findWatcher(Meteor.userId());
},
@@ -161,8 +162,9 @@ BlazeComponent.extendComponent({
* @return is the list id the current list id ?
*/
isCurrentListId(listId) {
- const ret = this.data().listId == listId;
- return ret;
+ const data = this.data();
+ if (!data || typeof data.listId === 'undefined') return false;
+ return data.listId == listId;
},
onRendered() {
@@ -375,7 +377,7 @@ BlazeComponent.extendComponent({
const card = this.currentData() || this.data();
const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
const cardId = card && card._id;
-
+
if (boardId) {
// In desktop mode, remove from openCards array
const isMobile = Utils.getMobileMode();
@@ -383,19 +385,18 @@ BlazeComponent.extendComponent({
const openCards = Session.get('openCards') || [];
const filtered = openCards.filter(id => id !== cardId);
Session.set('openCards', filtered);
-
+
// If this was the current card, clear it
if (Session.get('currentCard') === cardId) {
Session.set('currentCard', null);
}
-
// Don't navigate away in desktop mode - just close the card
return;
}
-
+
// Mobile mode: Clear the current card session to close the card
Session.set('currentCard', null);
-
+
// Navigate back to board without card
const board = ReactiveCache.getBoard(boardId);
if (board) {
@@ -818,6 +819,7 @@ Template.editCardSortOrderForm.onRendered(function () {
Template.cardDetailsActionsPopup.helpers({
isWatching() {
+ if (!this || typeof this.findWatcher !== 'function') return false;
return this.findWatcher(Meteor.userId());
},
diff --git a/client/components/cards/checklists.js b/client/components/cards/checklists.js
index ade73818f..31c913940 100644
--- a/client/components/cards/checklists.js
+++ b/client/components/cards/checklists.js
@@ -275,8 +275,8 @@ BlazeComponent.extendComponent({
Template.checklists.helpers({
checklists() {
const card = ReactiveCache.getCard(this.cardId);
- const ret = card.checklists();
- return ret;
+ if (!card || typeof card.checklists !== 'function') return [];
+ return card.checklists();
},
});
diff --git a/client/components/boards/gantt.css b/client/components/gantt/gantt.css
similarity index 83%
rename from client/components/boards/gantt.css
rename to client/components/gantt/gantt.css
index 6a14f4a3b..81139f07b 100644
--- a/client/components/boards/gantt.css
+++ b/client/components/gantt/gantt.css
@@ -1,3 +1,28 @@
+/* Gantt chart cell background colors for Received, Start, Due, End (matching cardDetails) */
+.ganttview-received {
+ background-color: #dbdbdb !important;
+ color: #000 !important;
+ font-size: 18px !important;
+ font-weight: bold !important;
+}
+.ganttview-start {
+ background-color: #90ee90 !important;
+ color: #000 !important;
+ font-size: 18px !important;
+ font-weight: bold !important;
+}
+.ganttview-due {
+ background-color: #ffd700 !important;
+ color: #000 !important;
+ font-size: 18px !important;
+ font-weight: bold !important;
+}
+.ganttview-end {
+ background-color: #ffb3b3 !important;
+ color: #000 !important;
+ font-size: 18px !important;
+ font-weight: bold !important;
+}
/* Gantt View Styles */
.gantt-view {
diff --git a/client/components/gantt/gantt.jade b/client/components/gantt/gantt.jade
new file mode 100644
index 000000000..721020b5a
--- /dev/null
+++ b/client/components/gantt/gantt.jade
@@ -0,0 +1,27 @@
+//- Gantt Chart View Template
+template(name="ganttView")
+ link(rel="stylesheet" href="/client/components/gantt/gantt.css")
+ link(rel="stylesheet" href="/client/components/gantt/ganttCard.css")
+ .gantt-view
+ h2 {{_ 'board-view-gantt'}}
+ if hasSelectedCard
+ +ganttCard
+ each weeks
+ table.gantt-table
+ thead
+ tr
+ th {{_ 'task'}} {{_ 'predicate-week'}} {{week}}
+ each weekDays this
+ th
+ | {{formattedDate .}} {{weekdayLabel .}}
+ tbody
+ each cardsInWeek this
+ tr(data-card-id="{{cardId .}}")
+ td.js-gantt-task-cell
+ a.js-gantt-card-title(href="#")
+ +viewer
+ | {{cardTitle .}}
+ each weekDays ..
+ td(class="{{cellClasses .. .}}" data-card-id="{{cardId ..}}" data-date-type="{{cellContentClass .. .}}")
+ | {{cellContent .. .}}
+
diff --git a/client/components/gantt/gantt.js b/client/components/gantt/gantt.js
new file mode 100644
index 000000000..d10560dea
--- /dev/null
+++ b/client/components/gantt/gantt.js
@@ -0,0 +1,217 @@
+// Add click handler to ganttView for card titles
+Template.ganttView.events({
+ 'click .js-gantt-card-title'(event, template) {
+ event.preventDefault();
+ // Get card ID from the closest row's data attribute
+ const $row = template.$(event.currentTarget).closest('tr');
+ const cardId = $row.data('card-id');
+
+ if (cardId) {
+ template.selectedCardId.set(cardId);
+ }
+ },
+});
+import { Template } from 'meteor/templating';
+
+// Blaze template helpers for ganttView
+function getISOWeekInfo(d) {
+ const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
+ const dayNum = date.getUTCDay() || 7;
+ date.setUTCDate(date.getUTCDate() + 4 - dayNum);
+ const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
+ const week = Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
+ return { year: date.getUTCFullYear(), week };
+}
+function startOfISOWeek(d) {
+ const date = new Date(d);
+ const day = date.getDay() || 7;
+ if (day !== 1) date.setDate(date.getDate() - (day - 1));
+ date.setHours(0,0,0,0);
+ return date;
+}
+
+Template.ganttView.helpers({
+ weeks() {
+ const board = Utils.getCurrentBoard();
+ if (!board) return [];
+ const cards = Cards.find({ boardId: board._id }, { sort: { startAt: 1, dueAt: 1 } }).fetch();
+ const weeksMap = new Map();
+ const relevantCards = cards.filter(c => c.receivedAt || c.startAt || c.dueAt || c.endAt);
+ relevantCards.forEach(card => {
+ ['receivedAt','startAt','dueAt','endAt'].forEach(field => {
+ if (card[field]) {
+ const dt = new Date(card[field]);
+ const info = getISOWeekInfo(dt);
+ const key = `${info.year}-W${info.week}`;
+ if (!weeksMap.has(key)) {
+ weeksMap.set(key, { year: info.year, week: info.week, start: startOfISOWeek(dt) });
+ }
+ }
+ });
+ });
+ return Array.from(weeksMap.values()).sort((a,b) => a.start - b.start);
+ },
+ weekDays(week) {
+ const weekStart = new Date(week.start);
+ return Array.from({length:7}, (_,i) => {
+ const d = new Date(weekStart);
+ d.setDate(d.getDate() + i);
+ d.setHours(0,0,0,0);
+ return d;
+ });
+ },
+ weekdayLabel(day) {
+ const weekdayKeys = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];
+ return TAPi18n.__(weekdayKeys[day.getDay() === 0 ? 6 : day.getDay() - 1]);
+ },
+ formattedDate(day) {
+ const currentUser = ReactiveCache.getCurrentUser && ReactiveCache.getCurrentUser();
+ const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
+ return formatDateByUserPreference(day, dateFormat, false);
+ },
+ cardsInWeek(week) {
+ const board = Utils.getCurrentBoard();
+ if (!board) return [];
+ const cards = Cards.find({ boardId: board._id }).fetch();
+ return cards.filter(card => {
+ return ['receivedAt','startAt','dueAt','endAt'].some(field => {
+ if (card[field]) {
+ const dt = new Date(card[field]);
+ const info = getISOWeekInfo(dt);
+ return info.week === week.week && info.year === week.year;
+ }
+ return false;
+ });
+ });
+ },
+ cardTitle(card) {
+ return card.title;
+ },
+ cardId(card) {
+ return card._id;
+ },
+ cardUrl(card) {
+ if (!card) return '#';
+ const board = ReactiveCache.getBoard(card.boardId);
+ if (!board) return '#';
+ return FlowRouter.path('card', {
+ boardId: card.boardId,
+ slug: board.slug,
+ cardId: card._id,
+ });
+ },
+ cellContentClass(card, day) {
+ const cardDates = {
+ receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
+ startAt: card.startAt ? new Date(card.startAt) : null,
+ dueAt: card.dueAt ? new Date(card.dueAt) : null,
+ endAt: card.endAt ? new Date(card.endAt) : null,
+ };
+ if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) return 'ganttview-received';
+ if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) return 'ganttview-start';
+ if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) return 'ganttview-due';
+ if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) return 'ganttview-end';
+ return '';
+ },
+ cellContent(card, day) {
+ const cardDates = {
+ receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
+ startAt: card.startAt ? new Date(card.startAt) : null,
+ dueAt: card.dueAt ? new Date(card.dueAt) : null,
+ endAt: card.endAt ? new Date(card.endAt) : null,
+ };
+ if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) return '📥';
+ if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) return '🚀';
+ if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) return '⏰';
+ if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) return '🏁';
+ return '';
+ },
+ isToday(day) {
+ const today = new Date();
+ return day.toDateString() === today.toDateString();
+ },
+ isWeekend(day) {
+ const idx = day.getDay();
+ return idx === 0 || idx === 6;
+ },
+ hasSelectedCard() {
+ return Template.instance().selectedCardId.get() !== null;
+ },
+ selectedCard() {
+ const cardId = Template.instance().selectedCardId.get();
+ return cardId ? ReactiveCache.getCard(cardId) : null;
+ },
+ cellClasses(card, day) {
+ // Get the base class from cellContentClass logic
+ const cardDates = {
+ receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
+ startAt: card.startAt ? new Date(card.startAt) : null,
+ dueAt: card.dueAt ? new Date(card.dueAt) : null,
+ endAt: card.endAt ? new Date(card.endAt) : null,
+ };
+ let classes = '';
+ if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === day.toDateString()) classes = 'ganttview-received';
+ else if (cardDates.startAt && cardDates.startAt.toDateString() === day.toDateString()) classes = 'ganttview-start';
+ else if (cardDates.dueAt && cardDates.dueAt.toDateString() === day.toDateString()) classes = 'ganttview-due';
+ else if (cardDates.endAt && cardDates.endAt.toDateString() === day.toDateString()) classes = 'ganttview-end';
+
+ // Add conditional classes
+ const today = new Date();
+ if (day.toDateString() === today.toDateString()) classes += ' ganttview-today';
+ const idx = day.getDay();
+ if (idx === 0 || idx === 6) classes += ' ganttview-weekend';
+ if (classes.trim()) classes += ' js-gantt-date-icon';
+
+ return classes.trim();
+ }
+});
+
+Template.ganttView.onCreated(function() {
+ this.selectedCardId = new ReactiveVar(null);
+ // Provide properties expected by cardDetails component
+ this.showOverlay = new ReactiveVar(false);
+ this.mouseHasEnterCardDetails = false;
+});
+
+// Blaze onRendered logic for ganttView
+Template.ganttView.onRendered(function() {
+ const self = this;
+ this.autorun(() => {
+ // If you have legacy imperative rendering, keep it here
+ if (typeof renderGanttChart === 'function') {
+ renderGanttChart();
+ }
+ });
+ // Add click handler for date cells (Received, Start, Due, End)
+ this.$('.gantt-table').on('click', '.js-gantt-date-icon', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ const $cell = self.$(this);
+ const cardId = $cell.data('card-id');
+ let dateType = $cell.data('date-type');
+ // Remove 'ganttview-' prefix to match popup map
+ if (typeof dateType === 'string' && dateType.startsWith('ganttview-')) {
+ dateType = dateType.replace('ganttview-', '');
+ }
+ const popupMap = {
+ received: 'editCardReceivedDate',
+ start: 'editCardStartDate',
+ due: 'editCardDueDate',
+ end: 'editCardEndDate',
+ };
+ const popupName = popupMap[dateType];
+ if (!popupName || typeof Popup === 'undefined' || typeof Popup.open !== 'function') return;
+ const card = ReactiveCache.getCard(cardId);
+ if (!card) return;
+ const openFn = Popup.open(popupName);
+ openFn.call({ currentData: () => card }, e, { dataContextIfCurrentDataIsUndefined: card });
+ });
+
+});
+
+import markdownit from 'markdown-it';
+import { TAPi18n } from '/imports/i18n';
+import { formatDateByUserPreference } from '/imports/lib/dateUtils';
+import { ReactiveCache } from '/imports/reactiveCache';
+
+const md = markdownit({ breaks: true, linkify: true });
diff --git a/client/components/gantt/ganttCard.css b/client/components/gantt/ganttCard.css
new file mode 100644
index 000000000..d43c5ee93
--- /dev/null
+++ b/client/components/gantt/ganttCard.css
@@ -0,0 +1,47 @@
+.gantt-card-wrapper {
+ background: white;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ margin: 1rem 0;
+ padding: 1rem;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.gantt-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+ border-bottom: 1px solid #eee;
+ padding-bottom: 0.5rem;
+}
+
+.gantt-card-header h3 {
+ margin: 0;
+ color: #333;
+}
+
+.close-button {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ color: #666;
+ padding: 0;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.close-button:hover {
+ background-color: #f0f0f0;
+ color: #333;
+}
+
+.gantt-card-content {
+ max-height: 400px;
+ overflow-y: auto;
+}
\ No newline at end of file
diff --git a/client/components/gantt/ganttCard.jade b/client/components/gantt/ganttCard.jade
new file mode 100644
index 000000000..a88e3f3f1
--- /dev/null
+++ b/client/components/gantt/ganttCard.jade
@@ -0,0 +1,7 @@
+template(name="ganttCard")
+ .gantt-card-wrapper
+ .gantt-card-header
+ h3 {{_ 'card-details'}}
+ button.js-close-gantt-card.close-button ×
+ .gantt-card-content
+ +cardDetails(selectedCard)
\ No newline at end of file
diff --git a/client/components/gantt/ganttCard.js b/client/components/gantt/ganttCard.js
new file mode 100644
index 000000000..c2b07ccd2
--- /dev/null
+++ b/client/components/gantt/ganttCard.js
@@ -0,0 +1,45 @@
+BlazeComponent.extendComponent({
+ onCreated() {
+ // Provide the expected parent component properties for cardDetails
+ this.showOverlay = new ReactiveVar(false);
+ this.mouseHasEnterCardDetails = false;
+ },
+
+ selectedCard() {
+ // Get the selected card from the parent ganttView template
+ const parentView = this.view.parentView;
+ if (parentView && parentView.templateInstance) {
+ const cardId = parentView.templateInstance().selectedCardId.get();
+ return cardId ? ReactiveCache.getCard(cardId) : null;
+ }
+ return null;
+ },
+
+ events() {
+ return [
+ {
+ 'click .js-close-gantt-card'(event) {
+ // Find the parent ganttView template and clear the selected card
+ const parentView = this.view.parentView;
+ if (parentView && parentView.templateInstance) {
+ parentView.templateInstance().selectedCardId.set(null);
+ }
+ },
+ },
+ ];
+ },
+}).register('ganttCard');
+
+// Add click handler to ganttView for card titles
+Template.ganttView.events({
+ 'click .js-gantt-card-title'(event, template) {
+ event.preventDefault();
+ // Get card ID from the closest row's data attribute
+ const $row = template.$(event.currentTarget).closest('tr');
+ const cardId = $row.data('card-id');
+
+ if (cardId) {
+ template.selectedCardId.set(cardId);
+ }
+ },
+});
\ No newline at end of file