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 += ``; - weekdayLabels.forEach((lbl, idx) => { - const formattedDate = formatDateByUserPreference(weekDates[idx], dateFormat, false); - html += ``; - }); - 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 += ``; - - // 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 += ``; - }); - - // Close row - html += ''; - }); - - // Close section for this week - html += '
${taskHeader}${formattedDate} ${lbl}
${card.title}${cellContent}
'; - }); - - 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