mirror of
https://github.com/wekan/wekan.git
synced 2025-12-30 14:18:48 +01:00
Converted Gantt from js to Jade.
Thanks to xet7 !
This commit is contained in:
parent
ce9afbcaca
commit
2d3bef9033
11 changed files with 384 additions and 283 deletions
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
+fullcalendar(calendarOptions)
|
||||
|
|
@ -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 = `<p style="padding: 20px; text-align: center; color: #999;">${TAPi18n.__('no-cards-in-gantt')}</p>`;
|
||||
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 += '<table class="gantt-table">';
|
||||
html += '<thead>';
|
||||
html += '<tr>';
|
||||
const taskHeader = `${TAPi18n.__('task')} ${TAPi18n.__('predicate-week')} ${weekInfo.week}`;
|
||||
html += `<th>${taskHeader}</th>`;
|
||||
weekdayLabels.forEach((lbl, idx) => {
|
||||
const formattedDate = formatDateByUserPreference(weekDates[idx], dateFormat, false);
|
||||
html += `<th>${formattedDate} ${lbl}</th>`;
|
||||
});
|
||||
html += '</tr></thead>';
|
||||
|
||||
// Rows: include cards that have any date in this week
|
||||
html += '<tbody>';
|
||||
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 += '<tr>';
|
||||
html += `<td class="js-gantt-task-cell" data-card-id="${card._id}" title="${card.title}">${card.title}</td>`;
|
||||
|
||||
// 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 += `<td class="${cellClass}" style="${cellStyle}" title="${cellTitle}"${cellDataAttrs}>${cellContent}</td>`;
|
||||
});
|
||||
|
||||
// Close row
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
// Close section for this week
|
||||
html += '</tbody></table>';
|
||||
});
|
||||
|
||||
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');
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
27
client/components/gantt/gantt.jade
Normal file
27
client/components/gantt/gantt.jade
Normal file
|
|
@ -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 .. .}}
|
||||
|
||||
217
client/components/gantt/gantt.js
Normal file
217
client/components/gantt/gantt.js
Normal file
|
|
@ -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 });
|
||||
47
client/components/gantt/ganttCard.css
Normal file
47
client/components/gantt/ganttCard.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
7
client/components/gantt/ganttCard.jade
Normal file
7
client/components/gantt/ganttCard.jade
Normal file
|
|
@ -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)
|
||||
45
client/components/gantt/ganttCard.js
Normal file
45
client/components/gantt/ganttCard.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue