Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <script>
- let currentEODUserId = null;
- function openEODSummary(userId) {
- currentEODUserId = userId;
- document.getElementById('eodSummaryModal').classList.remove('hidden');
- fetchAndRenderEOD(userId, 'last_7_days'); // default filter
- }
- function closeEODModal() {
- document.getElementById('eodSummaryModal').classList.add('hidden');
- }
- document.getElementById('closeEODModalBtn').addEventListener('click', closeEODModal);
- document.getElementById('dateFilter').addEventListener('change', () => {
- const filterValue = document.getElementById('dateFilter').value;
- fetchAndRenderEOD(currentEODUserId, filterValue);
- });
- async function fetchAndRenderEOD(userId, filterValue) {
- const tbody = document.getElementById('eodTableBody');
- tbody.innerHTML = '<tr><td colspan="4">Loading...</td></tr>';
- try {
- const res = await fetch(`/tasks/api/eod-report/?user=${userId}&filter=${filterValue}`, {
- credentials: 'include'
- });
- if (!res.ok) {
- throw new Error(`HTTP error! Status: ${res.status}`);
- }
- const data = await res.json();
- tbody.innerHTML = '';
- // Determine date list
- let allDates = [];
- if (filterValue === "last_7_days") {
- allDates = getPastDates(7);
- } else if (filterValue === "last_15_days") {
- allDates = getPastDates(15);
- } else {
- allDates = getMonthDates(filterValue); // month name β array of YYYY-MM-DD
- }
- allDates.forEach(date => {
- const entries = data.filter(item => item.date === date);
- if (entries.length > 0) {
- entries.forEach((entry, index) => {
- const row = document.createElement("tr");
- const dateCell = document.createElement("td");
- dateCell.textContent = index === 0 ? date : "";
- const projectCell = document.createElement("td");
- projectCell.textContent = entry.project_name;
- const summaryCell = document.createElement("td");
- summaryCell.innerHTML = `${entry.task_name} -- ${entry.text}
- <span class="status-chip ${entry.task_status === 'C' ? 'Complete' : 'Incomplete'}">
- ${entry.task_status === 'C' ? 'Complete' : 'Incomplete'}
- </span>`;
- const bountyCell = document.createElement("td");
- bountyCell.textContent = entry.bounty;
- row.append(dateCell, projectCell, summaryCell, bountyCell);
- tbody.appendChild(row);
- });
- } else {
- const row = document.createElement("tr");
- row.innerHTML = `
- <td>${date}</td>
- <td>-</td>
- <td>No EOD submitted today</td>
- <td>-</td>
- `;
- tbody.appendChild(row);
- }
- });
- } catch (err) {
- console.error(err);
- tbody.innerHTML = '<tr><td colspan="4">Error loading data</td></tr>';
- }
- }
- // Helpers
- // Generate past N days in YYYY-MM-DD
- function getPastDates(days) {
- const dates = [];
- const today = new Date();
- for (let i = 0; i < days; i++) {
- const d = new Date(today);
- d.setDate(today.getDate() - i);
- dates.push(formatDateee(d));
- }
- return dates;
- }
- // Generate all days of given month name
- function getMonthDates(monthName) {
- const monthIndex = new Date(`${monthName} 1, ${new Date().getFullYear()}`).getMonth();
- const year = new Date().getFullYear();
- const daysInMonth = new Date(year, monthIndex + 1, 0).getDate();
- const dates = [];
- for (let i = 1; i <= daysInMonth; i++) {
- dates.push(formatDateee(new Date(year, monthIndex, i)));
- }
- return dates;
- }
- // Format date to YYYY-MM-DD
- function formatDateee(date) {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- return `${year}-${month}-${day}`;
- }
- //end of eod
- document.title = `{{ project_detail.project_name}}`;
- // Add this to your existing script section
- document.addEventListener('DOMContentLoaded', () => {
- const clientView = "{{ client_view }}" === "True";
- if (clientView) {
- // Disable all clickable elements
- document.querySelectorAll('button, input[type="checkbox"], .add-task-btn, .edit-btn').forEach(element => {
- element.style.pointerEvents = 'none';
- if (element.tagName === 'BUTTON') {
- element.disabled = true;
- }
- });
- // Disable drag and drop
- document.querySelectorAll('[draggable="true"]').forEach(element => {
- element.draggable = false;
- });
- // Make all form inputs readonly
- document.querySelectorAll('input, textarea, select').forEach(element => {
- element.readOnly = true;
- if (element.tagName === 'SELECT') {
- element.disabled = true;
- }
- });
- // Keep task display toggle functionality
- document.querySelectorAll('.arrow-svg').forEach(arrow => {
- arrow.style.pointerEvents = 'auto';
- arrow.style.cursor = 'pointer';
- });
- // Keep filter dropdown functional
- const taskFilterDropdown = document.getElementById('taskDropdown');
- if (taskFilterDropdown) {
- taskFilterDropdown.disabled = false;
- taskFilterDropdown.style.pointerEvents = 'auto';
- }
- // Prevent default on all interactive elements except allowed ones
- document.addEventListener('click', (event) => {
- const allowedElements = ['.arrow-svg', '#taskDropdown'];
- const isAllowed = allowedElements.some(selector =>
- event.target.matches(selector) || event.target.closest(selector)
- );
- if (!isAllowed) {
- event.preventDefault();
- }
- }, true);
- // Add visual indication of read-only state
- document.querySelectorAll('.task-row, .subtask-row').forEach(row => {
- row.style.cursor = 'default';
- });
- // Disable all modals and popups
- const originalShowModal = window.showModal;
- window.showModal = function() {
- return false;
- };
- }
- });
- // Update existing functions to check for client view before executing
- const wrapWithClientCheck = (fn) => {
- return function(...args) {
- const clientView = "{{ client_view }}" === "True";
- if (clientView) {
- return false;
- }
- return fn.apply(this, args);
- };
- };
- // Wrap all interactive functions
- addTaskRow = wrapWithClientCheck(addTaskRow);
- addSubtaskRow = wrapWithClientCheck(addSubtaskRow);
- editTask = wrapWithClientCheck(editTask);
- deleteTask = wrapWithClientCheck(deleteTask);
- updateTaskStatus = wrapWithClientCheck(updateTaskStatus);
- openAssigneeDropdown = wrapWithClientCheck(openAssigneeDropdown);
- openDueDateDropdown = wrapWithClientCheck(openDueDateDropdown);
- openPriorityDropdown = wrapWithClientCheck(openPriorityDropdown);
- openBountyDropdown = wrapWithClientCheck(openBountyDropdown);
- updateProjectStatus = wrapWithClientCheck(updateProjectStatus);
- openTaskModal = wrapWithClientCheck(openTaskModal);
- drag = wrapWithClientCheck(drag);
- drop = wrapWithClientCheck(drop);
- allowDrop = wrapWithClientCheck(allowDrop);
- function getCookie(name) {
- let cookieValue = null;
- if (document.cookie && document.cookie !== '') {
- const cookies = document.cookie.split(';');
- for (let i = 0; i < cookies.length; i++) {
- const cookie = cookies[i].trim();
- if (cookie.substring(0, name.length + 1) === (name + '=')) {
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
- break;
- }
- }
- }
- return cookieValue;
- }
- const csrftoken = getCookie('csrftoken');
- if (!csrftoken) {
- console.error('CSRF token not found!');
- }
- const projectId = "{{ project_detail.id }}";
- const replacementMembers = {{ replacements| safe }};
- const team_members = {{ team_members | safe}};
- const milestoneStatuses = {{ milestone_statuses | safe }};
- const notifications = "{{notifs}}";
- const roles = {{ roles |safe }};
- let currentUserId = '';
- const members = {{ team_members | safe }};
- let filtered = "pending";
- const zoom = 1;
- let milestoneCount = Object.keys(milestoneStatuses).length;
- // Maintain a reference to the currently active dropdown
- let activeDropdown = null;
- // function pdhActive(elem) {
- // // Check if the clicked element is already active
- // if (activeDropdown === elem) {
- // elem.classList.remove('active-pdh'); // Close the active dropdown
- // activeDropdown = null; // Reset the activeDropdown reference
- // } else {
- // // Close any previously active dropdown
- // if (activeDropdown) {
- // activeDropdown.classList.remove('active-pdh');
- // }
- // // Open the new dropdown
- // elem.classList.add('active-pdh');
- // activeDropdown = elem; // Update the activeDropdown reference
- // }
- // }
- let elem1;
- function pdhActive(elem) {
- let activeDropdown = null;
- function handleDocumentClick(event) {
- elem1=event.target
- if (event.target === elem || elem.contains(event.target) && event.target.closest('svg')) {
- if (activeDropdown === elem) {
- elem.classList.remove('active-pdh');
- activeDropdown = null;
- document.removeEventListener('click', handleDocumentClick);
- } else {
- if (activeDropdown) {
- activeDropdown.classList.remove('active-pdh');
- }
- elem.classList.add('active-pdh');
- activeDropdown = elem;
- }
- } else if (!event.target.closest('.pdh-doc-dd')) {
- // If clicked outside the dropdown, close the dropdown
- if (activeDropdown) {
- activeDropdown.classList.remove('active-pdh');
- activeDropdown = null;
- document.removeEventListener('click', handleDocumentClick);
- }
- }
- }
- document.addEventListener('click', handleDocumentClick);
- }
- // Add an event listener to handle clicks outside the dropdowns
- document.addEventListener('click', (event) => {
- if (
- activeDropdown &&
- !activeDropdown.contains(event.target) && // Check if the click is outside the dropdown
- !event.target.closest('.dropdown-trigger') // Allow clicks on elements that trigger the dropdown
- ) {
- activeDropdown.classList.remove('active-pdh'); // Close the active dropdown
- activeDropdown = null; // Reset the activeDropdown reference
- }
- });
- function openThisDd(elem) {
- const dropdown = elem.querySelector(".sub-drop-down");
- const typeLabel = elem.querySelector('.milestone-type-block');
- // Function to handle document clicks
- function handleDocumentClick(event) {
- if (!elem.contains(event.target)) {
- dropdown.classList.remove("active-dd");
- document.removeEventListener("click", handleDocumentClick);
- }
- }
- // Toggle the dropdown
- const isActive = dropdown.classList.toggle("active-dd");
- // Add click handlers to the options
- const options = dropdown.querySelectorAll('.sub-dd-opt');
- options.forEach(option => {
- option.onclick = (e) => {
- e.stopPropagation();
- typeLabel.textContent = option.textContent;
- typeLabel.dataset.value = option.dataset.value;
- dropdown.classList.remove("active-dd");
- };
- });
- if (isActive) {
- document.addEventListener("click", handleDocumentClick);
- } else {
- document.removeEventListener("click", handleDocumentClick);
- }
- }
- function pinUnpinDoc(elem) {
- const docElem = elem.closest(".mdoc-document-list-content");
- const projectId = elem.dataset.projectId;
- const documentName = elem.dataset.documentName;
- const documentUrl = elem.dataset.documentUrl;
- fetch('/project/toggle-pin-document/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': '{{ csrf_token }}',
- },
- body: JSON.stringify({
- project_id: projectId,
- document_name: documentName,
- document_url: documentUrl
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- // Update UI based on server response
- docElem.setAttribute("data-pinned", data.pinned);
- // Update the Pin/Unpin text
- elem.textContent = data.pinned ? "Unpin Document" : "Pin Document";
- // Update pin icon visibility
- const pinIconContainer = elem.closest(".doc-list-opt-dd").parentElement.lastElementChild;
- if (pinIconContainer) {
- pinIconContainer.style.display = data.pinned ? "block" : "none";
- }
- // Sort documents
- const documentList = docElem.closest('.mdoc-document-list');
- if (documentList) {
- const documents = Array.from(documentList.children);
- documents.sort((a, b) => {
- const aPinned = a.getAttribute("data-pinned") === "true";
- const bPinned = b.getAttribute("data-pinned") === "true";
- if (aPinned !== bPinned) {
- return bPinned ? 1 : -1;
- }
- const aDate = parseDateFromString(a.getAttribute("data-created"));
- const bDate = parseDateFromString(b.getAttribute("data-created"));
- return bDate - aDate;
- });
- // Clear and repopulate the list
- while (documentList.firstChild) {
- documentList.removeChild(documentList.firstChild);
- }
- documents.forEach(doc => documentList.appendChild(doc));
- }
- // Close the dropdown
- const dropdown = elem.closest(".sub-drop-down");
- } else {
- console.error("Failed to update pin status:", data.message);
- }
- })
- .catch(error => {
- console.error("Error updating pin status:", error);
- });
- }
- function parseDateFromString(dateString) {
- if (!dateString) return new Date(0); // Return oldest possible date if no date string
- const [day, month, year] = dateString.split("/").map(Number);
- return new Date(2000 + year, month - 1, day);
- }
- function updateProjectStatus(proj_status, project_id){
- let status_name = {
- "on_track": {
- name: "On track",
- color: "#00B879",
- },
- "escalated": {
- name: "Escalated",
- color: "#A90420",
- },
- }
- fetch(`/api/update-priority/${project_id}/`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': csrftoken,
- },
- body: JSON.stringify({status: proj_status}),
- credentials: "include",
- })
- .then((response) => {
- if (!response.ok) {
- throw new Error('API call failed');
- }
- return response.json();
- })
- .then((data) => {
- let project_status_cont = document.querySelector("#project-status").innerHTML=`
- <div style="border-radius: 100%; width: 14px; height: 14px; background-color: ${status_name[proj_status].color};"></div>
- ${status_name[proj_status].name}
- `
- console.log('API response:', data);
- })
- .catch((error) => {
- console.error('Error:', error);
- });
- }
- function openPriorityDd(elem) {
- // Instead of opening a dropdown, we will just toggle the priority
- let currentPriority = elem.closest('div').querySelector("span").innerText;
- // Set the next priority
- let nextPriority;
- switch (currentPriority) {
- case "P1":
- nextPriority = "P2";
- break;
- case "P2":
- nextPriority = "P3";
- break;
- case "P3":
- nextPriority = "P1";
- break;
- default:
- nextPriority = "P1"; // Default to P1 if something is wrong
- break;
- }
- // Update the UI immediately
- elem.closest('div').querySelector("span").innerText = nextPriority;
- switch (nextPriority) {
- case "P1":
- elem.closest('div').style.fontWeight='bolder'
- elem.closest('div').style.borderColor='#94999d'
- break;
- case "P2":
- elem.closest('div').style.fontWeight='600'
- elem.closest('div').style.borderColor='#99a4ad'
- break;
- case "P3":
- elem.closest('div').style.fontWeight='400'
- elem.closest('div').style.borderColor='#CED4DA'
- break;
- default:
- break;
- }
- // Get project_id from the closest div or from data-attribute if needed
- let project_id = elem.getAttribute("data-project-id");
- // Call the function to update priority in the backend
- setPriority(nextPriority, project_id, elem);
- }
- function setPriority(priority, project_id, elem) {
- // Convert the priority to the corresponding string value (P1, P2, P3)
- let p;
- switch (priority) {
- case "P1":
- p = "P1";
- break;
- case "P2":
- p = "P2";
- break;
- case "P3":
- p = "P3";
- break;
- default:
- p = null;
- break;
- }
- // Make the API call to update the priority in the database
- fetch(`/api/update-priority/${project_id}/`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': csrftoken,
- },
- body: JSON.stringify({ priority: p }),
- credentials: "include",
- })
- .then((response) => {
- if (!response.ok) {
- throw new Error('API call failed');
- }
- return response.json();
- })
- .then((data) => {
- // Update the UI based on the response
- console.log('API response:', data);
- })
- .catch((error) => {
- console.error('Error:', error);
- });
- }
- //end project function
- async function endProject(element){
- try {
- const teamMembersResponse = await fetch(`/api/project/${element.id}/team-members/`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- if (!teamMembersResponse.ok) {
- throw new Error('Failed to fetch team members');
- }
- const teamMembersData = await teamMembersResponse.json();
- if (teamMembersData.length > 0) {
- alert('Cannot end the project. Please remove all team members first.');
- return;
- }
- const updateStatusResponse = await fetch(`/api/update-priority/${element.id}/`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': csrftoken,
- },
- body: JSON.stringify({ status: "ended" }),
- });
- if (!updateStatusResponse.ok) {
- throw new Error('Failed to update project status');
- }
- const updateStatusData = await updateStatusResponse.json();
- alert( 'Project ended successfully!');
- } catch (error) {
- console.error('Error:', error);
- alert('An error occurred. Please try again later.');
- }
- }
- function addTeamMemberAgain(button) {
- const memberId = button.closest('tr').id;
- const data = {
- member_id: memberId,
- project_id: projectId,
- };
- fetch('/project/addMemberBack/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(data),
- })
- .then(response => {
- if (response.ok) {
- return response.json();
- }
- throw new Error('Network response was not ok.');
- })
- .then(data => {
- // Handle success, e.g., update the UI or show a message
- console.log('Success:', data);
- window.location.reload();
- })
- .catch((error) => {
- alert(response.json);
- console.error('Error:', error);
- });
- }
- function toggleCategory(element) {
- const category = element.parentElement;
- category.classList.toggle('expanded');
- }
- function projectNotifClose() {
- const notifDiv = document.querySelector(".project-notifications-content");
- const notifAlert = document.querySelector(".notif-reddot");
- const arrow = document.getElementById('notif-chevron-icon');
- const arrowPath = arrow.querySelector('path');
- if (notifDiv.style.display === "block" || notifDiv.style.display === "") {
- notifDiv.style.display = "none";
- if (notifications=="True"){
- notifAlert.style.display = "block"
- }
- arrowPath.setAttribute('d', 'M1 1L7 7L13 1');
- } else {
- notifDiv.style.display = "block";
- notifAlert.style.display = "none"
- arrowPath.setAttribute('d', 'M13 7L7 1L1 7');
- }
- }
- function mdocToggleExpand() {
- const contentDiv = document.querySelector('.mdoc-content-div');
- const arrow = document.getElementById('mdoc-chevron-icon');
- const arrowPath = arrow.querySelector('path');
- if (contentDiv.style.display === "none" || contentDiv.style.display === "") {
- contentDiv.style.display = "block";
- arrowPath.setAttribute('d', 'M13 7L7 1L1 7');
- } else {
- contentDiv.style.display = "none";
- arrowPath.setAttribute('d', 'M1 1L7 7L13 1');
- }
- }
- function keyProjectExpand() {
- const viewModeDiv = document.getElementById('view-mode');
- const editModeDiv = document.getElementById('edit-mode');
- const arrow = document.getElementById('projectKey-details-chevron-icon');
- const arrowPath = arrow.querySelector('path');
- if (viewModeDiv.style.display === "none" || viewModeDiv.style.display === "") {
- viewModeDiv.style.display = "flex";
- editModeDiv.style.display = "none";
- arrowPath.setAttribute('d', 'M13 7L7 1L1 7');
- } else {
- viewModeDiv.style.display = "none";
- arrowPath.setAttribute('d', 'M1 1L7 7L13 1');
- }
- }
- let currentNotifId = null;
- /* SNOOZING NOTIFS */
- function toggleSnoozeDropdown(button) {
- const dropdown = button.parentElement.querySelector('.snooze-dropdown');
- if (dropdown.style.display === "flex") {
- dropdown.style.display = "none";
- } else {
- document.querySelectorAll('.snooze-dropdown').forEach(drop => drop.style.display = 'none');
- dropdown.style.display = "flex";
- const rect = button.getBoundingClientRect();
- dropdown.style.top = `${(rect.top + window.scrollY + button.offsetHeight)/zoom}px`;
- dropdown.style.left = `${(rect.left + window.scrollX)/zoom}px`;
- }
- }
- function closeSnoozeDropdown(closeBtn) {
- const dropdown = closeBtn.parentElement;
- dropdown.style.display = "none";
- }
- function confirmSnooze(notifId) {
- const dropdown = document.querySelector(`#notif-${notifId} .snooze-dropdown`);
- const days = dropdown.querySelector('.snooze-select').value;
- // Fetch to backend to handle snoozing logic
- fetch(`/snooze_notification/`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- notif_id: notifId,
- snooze_days: days
- }),
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- // Remove the notification from frontend
- const notifElement = document.getElementById(`notif-${notifId}`);
- if (notifElement) {
- notifElement.remove();
- }
- }
- })
- .catch(error => console.error('Error:', error));
- }
- /* NOTIF HANDLING */
- function selectSortButton(button, type, criteria) {
- document.querySelectorAll('.sort-button').forEach(btn => btn.classList.remove('selected'));
- button.classList.add('selected');
- sortNotifications(type, criteria);
- }
- function toggleDropdown() {
- var dropdown = document.getElementById("hat-dropdown");
- if (dropdown.style.opacity == '0' || dropdown.style.opacity === "") {
- dropdown.classList.add("visible")
- dropdown.style.transform = "scaleY(1)";
- } else {
- dropdown.classList.remove("visible")
- dropdown.style.transform = "scaleY(0)";
- }
- }
- function addHat(hatType) {
- const row = Number(currentUserId);
- const hatIcon = document.querySelector(`#hat-icon-${row}`);
- if (!hatIcon) {
- console.error(`No element found with ID: #hat-icon-${row}`);
- return;
- }
- const existingHat = hatIcon.querySelector('svg').children[0].getAttribute('fill');
- if (existingHat != 'none') {
- alert("This user already has a hat.");
- return;
- }
- let fillColor, tooltipText;
- if (hatType === 'projectManager') {
- fillColor = '#504CF5';
- tooltipText = 'Project Manager';
- } else if (hatType === 'devops') {
- fillColor = '#1EB88A';
- tooltipText = 'DevOps';
- } else {
- console.error(`Invalid hatType: ${hatType}`);
- return;
- }
- const hatSvg = `
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
- <path fill-rule="evenodd" clip-rule="evenodd" d="M15.885 10.0762C15.3475 11.2224 13.185 12.6112 10 12.6112C6.835 12.6112 4.58375 11.2399 4.11 10.0949C1.71875 10.2024 0 11.6324 0 12.4824C0 13.8924 4.47625 15.8924 10 15.8924C15.5225 15.8924 20 13.8924 20 12.4824C20 11.6212 18.2812 10.5237 15.885 10.0762Z" fill="${fillColor}"/>
- <path fill-rule="evenodd" clip-rule="evenodd" d="M12.5964 3.80859C11.9514 3.80859 10.6189 4.25109 10.0151 4.25109C9.41137 4.25109 8.07887 3.80859 7.43512 3.80859C6.04262 3.80859 5.17262 5.52359 5.05137 6.17984L5.02637 9.21359C5.72887 9.84609 7.04012 10.6411 8.47762 10.7611C8.97762 10.8023 9.50512 10.8273 10.0539 10.8273C10.6001 10.8273 11.1264 10.8023 11.6276 10.7611C13.0651 10.6398 14.2201 9.94359 14.9626 9.23484L14.9564 6.21859C14.8489 5.53609 13.9764 3.80859 12.5964 3.80859Z" fill="${fillColor}"/>
- </svg>
- `;
- hatIcon.innerHTML = `
- <div class="hat-container" style="position: relative; display: inline-block;">
- ${hatSvg}
- <span class="hat-tooltip" style="display: none; position: absolute; bottom: 125%; left: 50%; transform: translateX(-50%); padding: 6px 12px; background: black; color: white; border-radius: 4px; white-space: nowrap; font-size: 12px;">
- ${tooltipText}
- </span>
- </div>
- `;
- fetch('/project/updateHat/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- memberId: row,
- hatType: hatType,
- projectId : projectId
- })
- })
- .then(response => response.json())
- .then(data => {
- console.log('Hat updated successfully:', data);
- })
- .catch(error => {
- console.error('Error updating hat:', error);
- });
- const hatContainer = hatIcon.querySelector('.hat-container');
- const tooltip = hatIcon.querySelector('.hat-tooltip');
- hatContainer.addEventListener('mouseenter', () => {
- tooltip.style.display = 'block';
- });
- hatContainer.addEventListener('mouseleave', () => {
- tooltip.style.display = 'none';
- });
- }
- function removeHat() {
- const row = Number(currentUserId);
- const hatIcon = document.querySelector(`#hat-icon-${row}`);
- if (!hatIcon) {
- console.error(`No element found with ID: #hat-icon-${row}`);
- return;
- }
- const existingHat = hatIcon.querySelector('svg');
- if (existingHat) {
- hatIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
- <path fill-rule="evenodd" clip-rule="evenodd" d="M15.885 10.0762C15.3475 11.2224 13.185 12.6112 10 12.6112C6.835 12.6112 4.58375 11.2399 4.11 10.0949C1.71875 10.2024 0 11.6324 0 12.4824C0 13.8924 4.47625 15.8924 10 15.8924C15.5225 15.8924 20 13.8924 20 12.4824C20 11.6212 18.2812 10.5237 15.885 10.0762Z" fill="none"/>
- <path fill-rule="evenodd" clip-rule="evenodd" d="M12.5964 3.80859C11.9514 3.80859 10.6189 4.25109 10.0151 4.25109C9.41137 4.25109 8.07887 3.80859 7.43512 3.80859C6.04262 3.80859 5.17262 5.52359 5.05137 6.17984L5.02637 9.21359C5.72887 9.84609 7.04012 10.6411 8.47762 10.7611C8.97762 10.8023 9.50512 10.8273 10.0539 10.8273C10.6001 10.8273 11.1264 10.8023 11.6276 10.7611C13.0651 10.6398 14.2201 9.94359 14.9626 9.23484L14.9564 6.21859C14.8489 5.53609 13.9764 3.80859 12.5964 3.80859Z" fill="none"/>
- </svg>
- <span class="hat-tooltip" style="display: none; position: absolute; bottom: 125%; left: 50%; transform: translateX(-50%); padding: 6px 12px; background: black; color: white; border-radius: 4px; white-space: nowrap; font-size: 12px;">
- DevOps
- </span>`;
- } else {
- alert("This user doesn't have a hat assigned.");
- }
- fetch('/project/removeHat/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- memberId: row,
- projectId : projectId
- })
- })
- .then(response => response.json())
- .then(data => {
- console.log('Hat removed successfully:', data);
- })
- .catch(error => {
- console.error('Error removing hat:', error);
- });
- }
- function sortNotifications(type, criteria) {
- const notificationList = document.getElementById(`${type}-category`);
- const notifications = Array.from(notificationList.getElementsByClassName('notif-container'));
- notifications.sort((a, b) => {
- const aValue = a.getAttribute(`data-${criteria}`).toLowerCase();
- const bValue = b.getAttribute(`data-${criteria}`).toLowerCase();
- return aValue.localeCompare(bValue);
- });
- notifications.forEach((notif, index) => {
- notif.style.order = index;
- });
- }
- function handleNotificationClick(event,notifId) {
- event.preventDefault();
- fetch(`/project/notifications_read/`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- notif_id : notifId
- }),
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- const notifElement = document.getElementById(`notif-${notifId}`);
- if (notifElement) {
- notifElement.remove();
- }
- }
- })
- .catch(error => console.error('Error:', error));
- }
- /* MISSING KT */
- function toggleModal(userId) {
- const modal = document.getElementById("ktLinkModal");
- currentUserId = userId; // Store the user ID when the modal opens
- modal.style.display = modal.style.display === "flex" ? "none" : "flex";
- }
- function submitKTlink() {
- const ktLink = document.getElementById("p-ktLink").value;
- if (!ktLink) {
- alert("Please enter a KT link.");
- return;
- }
- fetch('/project/add_ktlink/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- user_id: currentUserId,
- kt_link: ktLink,
- project_id: projectId
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- alert('KT link added successfully!');
- const modal = document.getElementById("ktLinkModal");
- modal.style.display = "none";
- window.location.reload();
- } else {
- alert('Error: ' + data.message);
- }
- })
- .catch(error => {
- console.error('Error:', error);
- });
- }
- /* TEAMMEMBER EDITS */
- function addTeamMember() {
- event.preventDefault(); // Prevent the default form submission behavior
- const tbody = document.getElementById('team-members-tbody'); // Get the tbody element
- const tr = document.createElement('tr'); // Create a new table row
- tr.style.textAlign = 'left'; // Set text alignment
- tr.setAttribute('id', 'add-tr'); // Set an ID for the row
- tr.innerHTML = `
- <td>
- <select class="user-select select-dropdown" name="selected_user_id" onchange="updateMemberDetails(this)">
- <option value="">Select Name</option>
- ${replacementMembers.map(member => `<option value="${member.id}">${member.name}</option>`).join('')}
- </select>
- </td>
- <td id="role-column">
- <select class="role-select select-dropdown" name="role" onchange="updateUserSelect(this)">
- <option value="all">Select Role</option>
- ${roles.map(role => `<option value="${role.name}">${role.name}</option>`).join('')}
- </select>
- </td>
- <td id="availability-column">
- <input type="text" name="availability" placeholder="Available" />
- </td>
- <td id="contact-column">
- <div>Email: <input type="email" name="email" placeholder="Email" /></div>
- <div>Mob: <input type="tel" name="mobile" placeholder="Mobile" /></div>
- </td>
- <td>
- <span class="delete-icon" onclick="deleteRow(this)">✖</span>
- </td>
- <td>
- <button type="button" class="key-project-save-btn" onclick="saveMember(this)">Save</button>
- </td>
- `;
- tbody.appendChild(tr); // Append the new row to the tbody
- $(tr).find('.user-select, .role-select').select2();
- }
- function updateUserSelect(roleSelect) {
- const selectedRoleId = roleSelect.value;
- const userSelect = roleSelect.closest('tr').querySelector('.user-select');
- userSelect.innerHTML = '<option value="">Select Name</option>';
- if (selectedRoleId !="all") {
- const filteredMembers = replacementMembers.filter(member => member.department== selectedRoleId);
- userSelect.innerHTML += filteredMembers.map(member => `<option value="${member.id}">${member.name}</option>`).join('');
- }
- else{
- userSelect.innerHTML += replacementMembers.map(member => `<option value="${member.id}">${member.name}</option>`).join('');
- }
- $(userSelect).select2();
- }
- function deleteRow(button) {
- const row = button.closest('tr');
- row.remove();
- }
- function saveMember(button) {
- const row = button.closest('tr');
- const select = row.querySelector('select[name="selected_user_id"]');
- const selectedUserId = select.value;
- if (!selectedUserId) {
- alert('Please select a user.');
- return;
- }
- const role = row.querySelector('#role-column').textContent.trim();
- const availability = row.querySelector('#availability-column').textContent.trim();
- const contactInfo = row.querySelector('#contact-column').innerText.trim();
- const data = {
- user_id: selectedUserId,
- project_id: projectId
- };
- fetch('/project/addMember/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(data)
- })
- .then(response => response.json())
- .then(result => {
- if (result.success) {
- alert('Member saved successfully!');
- window.location.reload();
- } else {
- alert('Failed to save member.');
- }
- })
- .catch(error => {
- console.error('Error:', error);
- alert('An error occurred while saving the member.');
- });
- }
- //function for hide and show previous member
- function toggleMembers() {
- const previousMembers = document.querySelectorAll('.team-members-previous-member');
- const toggleButton = document.getElementById('toggle-members-btn');
- if (previousMembers.length > 0 && previousMembers[0].style.display === 'none') {
- // show previous members
- previousMembers.forEach(row => row.style.display = '');
- toggleButton.innerHTML = 'Hide Previous members <svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 7L7 1L1 7" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
- } else {
- // hide previous members
- previousMembers.forEach(row => row.style.display = 'none');
- toggleButton.innerHTML = `Show Previous members <svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M1 1L7 7L13 1" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- }
- }
- function showPopup(event, userId) {
- currentUserId = userId; //
- const svg = event.currentTarget;
- const popup = document.getElementById('team-member-action-popup');
- var dropdown = document.getElementById("hat-dropdown");
- // calculate position of the SVG element
- const rect = svg.getBoundingClientRect();
- const offsetX = window.scrollX;
- const offsetY = window.scrollY;
- // position the popup
- popup.style.left = `${(rect.left + offsetX)/zoom}px`;
- popup.style.top = `${(rect.bottom + offsetY)/zoom}px`;
- popup.classList.toggle("visible")
- // hide the popup when clicking outside of it
- function handleClickOutside(event) {
- if (!popup.contains(event.target) && !svg.contains(event.target)) {
- popup.classList.remove('visible')
- dropdown.classList.remove('visible')
- dropdown.style.transform="scaleY(0)"
- document.removeEventListener('click', handleClickOutside);
- }
- }
- document.addEventListener('click', handleClickOutside);
- }
- document.addEventListener("click", function (event) {
- const dropdown = document.querySelector(".project-status-dd");
- const spanElement = event.target.closest("span");
- if (!dropdown.contains(event.target) && !spanElement) {
- // If the click is outside the dropdown and span element, remove the visible class
- dropdown.classList.remove("visible");
- }
- });
- function openReplaceModal() {
- event.preventDefault();
- const replaceModal = document.getElementById('replace-modal');
- // const overlay = document.getElementById('overlay');
- // populate the modal with data from the selected row
- const row = document.getElementById(currentUserId);
- const teamMemberNameElement = row.querySelector('.team-member-name');
- const name = teamMemberNameElement.childNodes[0].textContent.trim();
- const role = row.cells[1].textContent;
- document.getElementById('replace-modal-role').textContent = `${role}: ${name}`;
- // populate select options dynamically
- const replaceSelect = document.getElementById('replace-select');
- $(replaceSelect).select2();
- replaceModal.style.display = 'block';
- // overlay.style.display = 'block';
- // Prevent closing modal when clicking inside
- replaceModal.addEventListener('click', function (event) {
- event.stopPropagation();
- });
- }
- function closeReplaceModal() {
- event.preventDefault();
- const replaceModal = document.getElementById('replace-modal');
- const overlay = document.getElementById('overlay');
- replaceModal.style.display = 'none';
- // overlay.style.display = 'none';
- }
- function replaceMember() {
- event.preventDefault();
- const replaceSelect = document.getElementById('replace-select').value;
- const ktLink = document.getElementById('kt-link').value;
- fetch('/project/replaceMember/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- current_user_id: currentUserId,
- replacement_user_id: replaceSelect,
- project_id: projectId,
- kt_link: ktLink
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- console.log('Member replaced successfully');
- window.location.reload();
- } else {
- console.error('Failed to replace member:', data.message);
- }
- })
- .catch(error => console.error('Error:', error));
- closeReplaceModal();
- }
- // fnction to open the Mark Absent modal
- function openMarkAbsentModal() {
- event.preventDefault();
- const row = document.getElementById(currentUserId);
- if (!row) return; // Ensure the row exists
- const teamMemberNameElement = row.querySelector('.team-member-name');
- const name = teamMemberNameElement.childNodes[0].textContent.trim();
- // populate the modal with the name
- const nameElement = document.getElementById('mark-absent-name');
- nameElement.textContent = ` Select a date to mark ${name} as absent.`;
- // Show the modal
- document.getElementById('mark-absent-modal').style.display = 'flex';
- document.getElementById('overlay').style.display = 'block';
- }
- // function to mark a member as absent
- function markAbsent() {
- if (event) {
- event.preventDefault();
- }
- const row = document.getElementById(currentUserId);
- if (!row) {
- console.error('Row not found');
- return;
- }
- const teamMemberNameElement = row.querySelector('.team-member-name');
- const name = teamMemberNameElement.childNodes[0].textContent.trim();
- const date = document.getElementById('absence-date').value;
- fetch('/project/mark_absent/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- user_id: currentUserId,
- date: date
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === "success") {
- console.log(`${name} marked absent on ${date}`);
- window.location.reload();
- } else {
- console.error('Failed to mark absent:', data.message);
- }
- })
- .catch(error => console.error('Error:', error));
- document.getElementById('absence-date').value = '';
- closeMarkAbsentModal();
- }
- // function to close the Mark Absent modal
- function closeMarkAbsentModal() {
- event.preventDefault();
- document.getElementById('mark-absent-modal').style.display = 'none';
- document.getElementById('overlay').style.display = 'none';
- }
- function openRemoveModal() {
- const removeModal = document.getElementById('remove-modal');
- // populate the modal with data from the selected row
- const row = document.getElementById(currentUserId);
- const teamMemberNameElement = row.querySelector('.team-member-name');
- const name = teamMemberNameElement.childNodes[0].textContent.trim();
- removeModal.style.display = 'block';
- // pervent closing modal when clicking inside
- removeModal.addEventListener('click', function (event) {
- event.stopPropagation();
- });
- event.preventDefault();
- }
- function removeMember() {
- event.preventDefault();
- const row = document.getElementById(currentUserId);
- if (!row) return;
- const teamMemberNameElement = row.querySelector('.team-member-name');
- const name = teamMemberNameElement.childNodes[0].textContent.trim();
- const ktLink = document.getElementById('rev-kt-link').value;
- const checkbox = document.getElementById('revoke-access-checkbox');
- fetch('/project/removeMember/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- user_id: currentUserId,
- project_id: projectId,
- revoke_repo: checkbox.checked,
- kt_link: ktLink
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- row.remove();
- window.location.reload();
- } else {
- console.error('Failed to remove team member:', data.message);
- }
- })
- .catch(error => console.error('Error:', error));
- closeRemoveModal();
- }
- function closeRemoveModal() {
- event.preventDefault();
- const removeModal = document.getElementById('remove-modal');
- const overlay = document.getElementById('overlay');
- removeModal.style.display = 'none';
- // overlay.style.display = 'none';
- }
- function deleteDocument(element) {
- // Get data from the clicked element
- const documentName = element.dataset.documentName;
- const documentUrl = element.dataset.documentUrl;
- if (!projectId) {
- console.error('Project ID not found');
- return;
- }
- if (!confirm(`Are you sure you want to delete "${documentName}"?`)) {
- return;
- }
- fetch('/project/delete-project-document/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': '{{ csrf_token }}',
- },
- body: JSON.stringify({
- project_id: projectId,
- document_name: documentName,
- document_url: documentUrl
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- // Remove the document from the UI
- const docElement = element.closest('.mdoc-document-list-content');
- if (docElement) {
- docElement.remove();
- }
- // Show success message
- alert('Document deleted successfully');
- } else {
- // Show error message
- alert('Error deleting document: ' + data.message);
- }
- })
- .catch(error => {
- console.error('Error:', error);
- alert('Error deleting document');
- });
- }
- function editDocument(element) {
- // Get data from the clicked element
- const projectId = element.dataset.projectId;
- const documentName = element.dataset.documentName;
- const documentUrl = element.dataset.documentUrl;
- // Store original values for later comparison
- const modal = document.getElementById('editDocumentModal');
- const form = document.getElementById('editDocumentForm');
- const nameInput = document.getElementById('editDocumentName');
- const urlInput = document.getElementById('editDocumentUrl');
- // Set current values
- nameInput.value = documentName;
- urlInput.value = documentUrl;
- // Store reference to the list item for updating UI later
- const docElement = element.closest('.mdoc-document-list-content');
- // Show modal
- modal.style.display = 'block';
- // Handle form submission
- form.onsubmit = function(e) {
- e.preventDefault();
- const newName = nameInput.value.trim();
- const newUrl = urlInput.value.trim();
- // Don't make API call if nothing changed
- if (newName === documentName && newUrl === documentUrl) {
- closeEditModal();
- return;
- }
- fetch('/project/edit-project-document/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': '{{ csrf_token }}',
- },
- body: JSON.stringify({
- project_id: projectId,
- old_name: documentName,
- old_url: documentUrl,
- new_name: newName,
- new_url: newUrl
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- // Update the UI
- const nameElement = docElement.querySelector('.mdoc-document-list-name');
- nameElement.textContent = newName;
- nameElement.href = newUrl;
- // Update the data attributes
- element.dataset.documentName = newName;
- element.dataset.documentUrl = newUrl;
- closeEditModal();
- alert('Document updated successfully');
- } else {
- alert('Error updating document: ' + data.message);
- }
- })
- .catch(error => {
- console.error('Error:', error);
- alert('Error updating document');
- });
- };
- }
- function copyDocumentLink(element) {
- let url = element.dataset.documentUrl;
- // If the URL starts with /media, prepend the full domain
- if (url.startsWith('/media')) {
- // For development
- if (window.location.hostname === 'localhost') {
- url = `http://localhost:8000${url}`;
- }
- // For production
- else {
- url = `https://${window.location.host}${url}`;
- }
- }
- // Create a temporary input element
- const tempInput = document.createElement('input');
- tempInput.value = url;
- document.body.appendChild(tempInput);
- // Select and copy the text
- tempInput.select();
- document.execCommand('copy');
- // Remove the temporary element
- document.body.removeChild(tempInput);
- // Provide visual feedback
- const originalText = element.textContent;
- element.textContent = 'Copied!';
- element.style.color = '#00B879'; // Green color for success
- // Reset the button after 2 seconds
- setTimeout(() => {
- element.textContent = originalText;
- element.style.color = ''; // Reset to default color
- }, 2000);
- }
- function closeEditModal() {
- const modal = document.getElementById('editDocumentModal');
- modal.style.display = 'none';
- document.getElementById('editDocumentForm').reset();
- }
- function togglePinDocument(element) {
- const projectId = element.dataset.projectId;
- const documentName = element.dataset.documentName;
- const documentUrl = element.dataset.documentUrl;
- fetch('/project/toggle-pin-document/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': '{{ csrf_token }}',
- },
- body: JSON.stringify({
- project_id: projectId,
- document_name: documentName,
- document_url: documentUrl
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- // Get the document container
- const documentContainer = element.closest('.mdoc-document-list-content');
- // Update pinned status visually
- documentContainer.setAttribute('data-pinned', data.pinned);
- element.textContent = data.pinned ? 'Unpin Document' : 'Pin Document';
- // Refresh document list to maintain order
- const documentList = document.getElementById('document-list');
- const documents = Array.from(documentList.children);
- // Sort documents (pinned first)
- documents.sort((a, b) => {
- const aPinned = a.getAttribute('data-pinned') === 'true';
- const bPinned = b.getAttribute('data-pinned') === 'true';
- return bPinned - aPinned;
- });
- // Clear and re-append sorted documents
- documentList.innerHTML = '';
- documents.forEach(doc => documentList.appendChild(doc));
- } else {
- alert('Error updating pin status: ' + data.message);
- }
- })
- .catch(error => {
- console.error('Error:', error);
- alert('Error updating pin status');
- });
- }
- /* PROJECT DETAIL CHANGES */
- let milestonesDict = {}; // This will store milestone details
- function showMilestoneDetailPopup(event,milestoneId) {
- const milestoneElement = document.getElementById(milestoneId);
- const popup = document.getElementById("milestone-popup");
- popup.dataset.milestoneId = milestoneId;
- const clickedDiv = event.currentTarget;
- const rect = clickedDiv.getBoundingClientRect();
- const offsetX = window.scrollX;
- const offsetY = window.scrollY;
- popup.style.left = `${(rect.left + offsetX)/zoom}px`;
- popup.style.top = `${(rect.bottom + offsetY)/zoom}px`;
- popup.style.display = 'flex';
- const handleClickOutside = (event) => {
- if (!popup.contains(event.target) && !clickedDiv.contains(event.target)) {
- closeMilestoneDetailPopup();
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- // Populate start and end dates from existing data if available
- const milestone = milestoneStatuses[milestoneId] || {};
- document.getElementById("rename-milestone").value = milestoneId ;
- document.getElementById("milestone-start-date").value = formatDateForInput(milestone.start) ;
- document.getElementById("milestone-end-date").value = formatDateForInput(milestone.end);
- document.getElementById("milestone-budget").value = milestone.budget || 0;
- const milestoneTypeLabel = document.querySelector('.milestone-type .milestone-type-block');
- if (milestone.type) {
- // Map the backend value to display text
- const typeMap = {
- 'retainer': 'Retainer Based',
- 'fixed': 'Fixed Cost'
- };
- milestoneTypeLabel.innerHTML = typeMap[milestone.type] || '';
- // Store the actual value as a data attribute
- milestoneTypeLabel.dataset.value = milestone.type;
- }
- }
- function closeMilestoneDetailPopup() {
- const milestoneId = document.getElementById('milestone-popup').getAttribute('data-milestone-id');
- // Get the start and end date values from the popup inputs
- const milestoneName = document.getElementById("rename-milestone").value;
- const startDateValue = document.getElementById("milestone-start-date").value;
- const endDateValue = document.getElementById("milestone-end-date").value;
- const milestoneBudgetValue = document.getElementById("milestone-budget").value;
- const typeLabel = document.querySelector('.milestone-type .milestone-type-block');
- const milestoneTypeValue = typeLabel.dataset.value || 'retailer';
- // Only update the current milestone instead of replacing everything
- if (milestoneId && milestoneStatuses[milestoneId]) {
- // Format the dates as required by Django
- const formattedStartDate = startDateValue ? formatDateForInput(startDateValue) : milestoneStatuses[milestoneId].start;
- const formattedEndDate = endDateValue ? formatDateForInput(endDateValue) : milestoneStatuses[milestoneId].end;
- // Prepare the updated milestone data
- milestoneStatuses[milestoneId] = {
- ...milestoneStatuses[milestoneId], // Preserve existing properties like ID
- start: formattedStartDate,
- end: formattedEndDate,
- budget: milestoneBudgetValue || milestoneStatuses[milestoneId].budget,
- type: milestoneTypeValue,
- };
- // Check if a new milestone name is provided and is different from the current ID
- if (milestoneName && milestoneName !== milestoneId) {
- // Remove the old entry and create a new one with the updated name
- const oldMilestoneData = {...milestoneStatuses[milestoneId]};
- delete milestoneStatuses[milestoneId];
- milestoneStatuses[milestoneName] = oldMilestoneData;
- // Update the data-milestone-id attribute with the new name
- document.getElementById('milestone-popup').setAttribute('data-milestone-id', milestoneName);
- const milestoneElements = document.getElementsByName('edit-' + milestoneId);
- const milestoneElement = milestoneElements.length > 0 ? milestoneElements[0] : null;
- if (milestoneElement) {
- milestoneElement.setAttribute('name', 'edit-' + milestoneName);
- milestoneElement.setAttribute('id', milestoneName);
- milestoneElement.children[0].textContent = milestoneName;
- }
- }
- }
- // Hide the popup
- document.getElementById('milestone-popup').style.display = 'none';
- }
- function endMilestone() {
- event.preventDefault();
- const milestoneId = document.getElementById("milestone-popup").dataset.milestoneId;
- milestoneStatuses[milestoneId].status = "completed";
- const milestoneElements = document.getElementsByName('edit-' + milestoneId);
- const milestoneElement = milestoneElements.length > 0 ? milestoneElements[0] : null;
- if (milestoneElement) {
- // Proceed with updating milestone color
- updateMilestoneColor(milestoneElement, "completed");
- } else {
- console.log("No element found with the specified name.");
- }
- closeMilestoneDetailPopup();
- }
- function addMilestone(event, element, type) {
- event.preventDefault();
- // Increment the milestone count for the new milestone
- milestoneCount++;
- milestoneStatuses["M"+milestoneCount]={"id":"0","status":"incomplete","start":"","end":"","budget":0.0,"type":"retainer"}
- let span = document.createElement('span');
- span.className='display-milestone';
- span.id=`M${milestoneCount}`
- span.setAttribute('name', `edit-${span.id}`);
- span.innerHTML=`
- <p>M${milestoneCount}</p>
- <svg width="10" height="6" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M1 1L7 7L13 1" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
- </svg>
- `
- // Insert the new milestone input after the current element
- element.parentElement.insertAdjacentElement('beforebegin', span);
- span.addEventListener('click', (e)=>{
- showMilestoneDetailPopup(e, span.id);
- })
- }
- function removeMilestone(element) {
- event.preventDefault();
- // Remove the milestone input div
- element.parentElement.remove();
- milestoneCount--;
- }
- function addInputTextField(element, name) {
- event.preventDefault();
- const div = document.createElement('div');
- div.className = 'client-input-row';
- div.style.position="relative"
- div.innerHTML = `
- <input class="input-style client-name-input" type="text" id= "${name}" name="${name}" placeholder="Add a ${name.replace('-', ' ')}">
- <span class="delete-icon" onclick="removeInputField(this)">✖</span>
- `;
- // element.insertAdjacentElement('beforebegin', div);
- element.parentElement.insertAdjacentElement('afterend', div);
- }
- let currentTaskId = ""
- // Track event listeners in an object or array
- let eventListeners = [];
- async function openTaskModal(taskId) {
- let taskData = {};
- try {
- // Make the API call to get task data
- const response = await fetch(`/project/get/tasks/${taskId}`);
- if (!response.ok) {
- throw new Error('Failed to fetch task data');
- }
- // Parse the JSON response
- taskData = await response.json();
- currentTaskId = taskId;
- populateData(taskData);
- // 1. Description Field Event Listener with Timeout (for saving after delay)
- const descriptionField = document.getElementById('task-description');
- let timeoutId; // To hold the timeout reference
- const descriptionListener = function () {
- clearTimeout(timeoutId);
- timeoutId = setTimeout(async function () {
- // Make API call to save the updated description
- await saveDescription(taskId, descriptionField.value);
- }, 1000); // Delay the API call by 1 second after typing stops
- };
- descriptionField.addEventListener('change', descriptionListener);
- eventListeners.push({ element: descriptionField, type: 'change', listener: descriptionListener });
- const enterListener = async function (event) {
- if (event.key === 'Enter') {
- // Save the description when Enter key is pressed
- await saveDescription(taskId, descriptionField.value);
- }
- };
- // Listen for keydown event to capture Enter key press
- descriptionField.addEventListener('keydown', enterListener);
- eventListeners.push({ element: descriptionField, type: 'keydown', listener: enterListener });
- // 2. Type Button Toggle (Task / Checkpoint)
- const typeButton = document.getElementById('task-type');
- const typeButtonListener = function () {
- const newType = taskData.type == "Checkpoint" ? 'Task' : 'Checkpoint';
- taskData.type = newType;
- typeButton.textContent = newType;
- if (newType == "Checkpoint"){
- typeButton.style.background = "orange";
- }
- else{
- typeButton.style.background = "#504CF5";
- }
- // Optionally, update backend with the new type
- saveTaskType(taskId, newType);
- };
- typeButton.addEventListener('click', typeButtonListener);
- eventListeners.push({ element: typeButton, type: 'click', listener: typeButtonListener });
- // 3. Name Field Toggle to Input
- const nameField = document.getElementById('task-title');
- const nameFieldListener = function () {
- const inputField = document.createElement('input');
- inputField.value = nameField.textContent; // Set the current name as value
- inputField.className = 'name-input';
- inputField.addEventListener('blur', async function () {
- // Save the updated name on blur (focus out)
- await saveTaskName(taskId, inputField.value);
- nameField.textContent = inputField.value; // Set name to the new value
- nameField.style.display = 'block';
- inputField.remove(); // Remove the input field
- });
- inputField.addEventListener('keydown', async function (event) {
- if (event.key === 'Enter') {
- // Save the updated name when Enter is pressed
- await saveTaskName(taskId, inputField.value);
- nameField.textContent = inputField.value; // Set name to the new value
- nameField.style.display = 'block'; // Show the name field again
- inputField.remove(); // Remove the input field
- }
- });
- nameField.style.display = 'none'; // Hide the original name
- nameField.parentElement.appendChild(inputField); // Append the input field
- inputField.focus(); // Focus the input field immediately
- };
- nameField.addEventListener('click', nameFieldListener);
- eventListeners.push({ element: nameField, type: 'click', listener: nameFieldListener });
- } catch (error) {
- console.error('Error fetching task data:', error);
- alert('Unable to load task details. Please try again later.');
- }
- }
- // Function to remove event listeners
- function closeTaskModal() {
- const modalOverlay = document.getElementById('task-modal');
- modalOverlay.style.display = 'none'; // Hide the modal
- // Remove all event listeners that were added
- eventListeners.forEach(item => {
- item.element.removeEventListener(item.type, item.listener);
- });
- // Clear the event listeners array
- eventListeners = [];
- }
- // Function to save the description (on change after timeout)
- async function saveDescription(taskId, description) {
- try {
- const response = await fetch(`/project/update-description/${taskId}/`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ description: description }),
- });
- if (response.ok) {
- console.log('Description saved');
- } else {
- console.error('Failed to save description');
- }
- } catch (error) {
- console.error('Error saving description:', error);
- }
- }
- // Function to save the task type (on button click toggle)
- async function saveTaskType(taskId, type) {
- try {
- const response = await fetch(`/project/update-type/${taskId}/`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ type: type }),
- });
- if (response.ok) {
- console.log('Task type saved');
- } else {
- console.error('Failed to save task type');
- }
- } catch (error) {
- console.error('Error saving task type:', error);
- }
- }
- // Function to save the task name (on blur)
- async function saveTaskName(taskId, name) {
- try {
- const response = await fetch(`/project/rename/${taskId}/`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ name: name }),
- });
- if (response.ok) {
- console.log('Task name saved');
- } else {
- console.error('Failed to save task name');
- }
- } catch (error) {
- console.error('Error saving task name:', error);
- }
- }
- function populateData(taskData){
- const modalOverlay = document.getElementById('task-modal');
- const taskModal = modalOverlay.querySelector('.task-modal');
- const assignIcon = document.getElementById('task-assignee')
- assignee = "No assignee";
- if (taskData.assignee){
- assignIcon.innerText = taskData.assignee;
- }
- else{
- assignIcon.innerText = assignee;
- }
- // Populate the task data in the modal
- document.getElementById('task-project').innerText = taskData.project;
- document.getElementById('task-project').style.paddingRight = "5px";
- document.getElementById('task-milestone').style.paddingLeft = "5px";
- document.getElementById('task-milestone').innerText = taskData.milestone;
- const typeButton = document.getElementById('task-type')
- typeButton.innerText = taskData.type;
- if (taskData.type== "Checkpoint"){
- typeButton.style.background = "orange";
- }
- else{
- typeButton.style.background = "#504CF5";
- }
- document.getElementById('task-title').innerText = taskData.title;
- document.getElementById('task-status').innerText = taskData.status;
- document.getElementById('task-start-date').innerText = taskData.start_date;
- document.getElementById('task-due-date').innerText = taskData.due_date || "No Due Date";
- document.getElementById('task-description').value = taskData.description;
- // Dynamically populate links
- if (taskData.links){
- populateLinks(taskData.links);
- }
- if (taskData.prs){
- populatePRs(taskData.prs);
- }
- if (taskData.activity) {
- populateActivitySection(taskData.activity);
- }
- else{
- const activitiesList = document.getElementById('activities-list');
- activitiesList.innerHTML = '';
- const activityItem = document.createElement('div');
- activityItem.classList.add('activity-item');
- activityItem.innerHTML = "No activity yet."
- activitiesList.appendChild(activityItem);
- }
- // Show the modal overlay (making it visible)
- modalOverlay.style.display = 'block';
- }
- // Function to close the modal
- // Function to dynamically populate Links as a styled list with name and URL
- function populateLinks(links) {
- const linksContainer = document.getElementById('links-container');
- linksContainer.innerHTML = ''; // Clear existing links
- const ul = document.createElement('ul'); // Create an unordered list for links
- ul.id = "links-ul";
- ul.classList.add('link-list'); // Adding class for styling
- links.forEach(link => {
- const li = document.createElement('li');
- li.classList.add('link-item'); // Applying item style
- // Assuming each link object has a 'name' and 'url' property
- li.innerHTML = `
- <div class="link-item-container">
- <a href="${link.url}" target="_blank" class="link-text">${link.name}</a>
- <button title="Delete Link" onclick="deleteItem(this, 'link', '${link.url}')" class="remove-btn">
- <svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M3 3L13 13M3 13L13 3" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </button>
- </div>
- `;
- ul.appendChild(li);
- });
- linksContainer.appendChild(ul);
- }
- function populateActivitySection(activityData) {
- const activitiesList = document.getElementById('activities-list');
- activitiesList.innerHTML = ''; // Clear previous activities
- activityData.forEach(activity => {
- const activityItem = document.createElement('div');
- activityItem.classList.add('activity-item');
- // Create content and timestamp elements
- const contentElement = document.createElement('div');
- contentElement.classList.add('activity-content');
- contentElement.textContent = activity.content;
- const timestampElement = document.createElement('div');
- timestampElement.classList.add('activity-timestamp');
- timestampElement.textContent = activity.timestamp;
- timestampElement.style.color = "#ccc";
- timestampElement.title = "Timestamp";
- // Append content and timestamp to the activity item
- activityItem.appendChild(contentElement);
- activityItem.appendChild(timestampElement);
- // Append activity item to the activities list
- activitiesList.appendChild(activityItem);
- });
- }
- // Function to dynamically populate PRs as a styled list
- function populatePRs(prs) {
- const prsContainer = document.getElementById('prs-container');
- prsContainer.innerHTML = ''; // Clear existing PRs
- const ul = document.createElement('ul'); // Create an unordered list for PRs
- ul.id = "prs-ul";
- ul.classList.add('pr-list'); // Adding class for styling
- prs.forEach(pr => {
- const li = document.createElement('li');
- li.classList.add('link-item'); // Applying item style
- li.classList.add('pr-hover'); // Adding hover class to show delete button
- let color = "red"
- if (pr.state == "closed"){
- color = "green";
- }
- li.innerHTML = `
- <div class="pr-item-container link-item-container">
- <a href="${pr.url}" target="_blank" class="pr-text">${pr.name}
- <span id="pr-state" style="color:${color}"> (${pr.state}) </span>
- </a>
- <button title="Delete PR" onclick="deleteItem(this, 'pr', '${pr.url}')" class="remove-btn">
- <svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M3 3L13 13M3 13L13 3" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </button>
- </div>
- `;
- li.title = pr.state;
- ul.appendChild(li);
- });
- prsContainer.appendChild(ul);
- prsContainer.appendChild(ul);
- }
- function deleteItem(button, type, url) {
- const item = button.closest('.link-item'); // Find the parent item (PR or link)
- // Send the DELETE request to the backend
- fetch(`/project/task/delete_item/`, {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ type: type, url: url,task_id: currentTaskId }) // Send both type and url to backend
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- item.remove();
- } else {
- alert("Error deleting item.");
- }
- })
- .catch(error => {
- console.error("Error:", error);
- alert("There was an issue deleting the item.");
- });
- }
- function addTaskInputField(type) {
- const container = document.getElementById(type + '-container');
- const inputGroup = document.createElement('div');
- inputGroup.classList.add('input-group');
- inputGroup.innerHTML = `
- <div class="input-fields-container">
- <input class="dynamic-input" type="text" placeholder="Enter ${type === 'links' ? 'link' : 'PR'} name">
- <input class="dynamic-input" type="text" placeholder="Enter ${type === 'links' ? 'link' : 'PR'} URL">
- </div>
- <div class="button-container">
- <button class="add-button save-button" onclick="saveLink(this, '${type}')">Save</button>
- <button class="remove-button" onclick="removeTaskInputField(this)">Remove</button>
- </div>
- `;
- container.appendChild(inputGroup);
- }
- function removeTaskInputField(button) {
- button.parentElement.parentElement.remove();
- }
- function saveLink(button, type) {
- const inputFields = button.parentElement.previousElementSibling.querySelectorAll('input');
- const name = inputFields[0].value;
- const url = inputFields[1].value;
- if (name && url) {
- const data = {
- name: name,
- url: url
- };
- // Send the data to the backend API
- fetch('/project/task/save_link/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- type: type,
- data: data,
- task_id : currentTaskId
- })
- })
- .then(response => response.json())
- .then(data => {
- ul = document.getElementById(`${type}-ul`);
- const li = document.createElement('li');
- li.classList.add('link-item'); // Applying item style
- // Assuming each link object has a 'name' and 'url' property
- li.innerHTML = `
- <div class="link-item-container">
- <a href="${url}" target="_blank" class="pr-text">${name}</a>
- <button title="Delete PR" onclick="deleteItem(this, '${type}', '${url}')" class="remove-btn">
- <svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M3 3L13 13M3 13L13 3" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </button>
- </div>
- `;
- ul.appendChild(li);
- button.parentElement.parentElement.remove(); // Remove the input group after saving
- })
- .catch(error => {
- alert("There was an error saving the link.");
- console.error('Error:', error);
- });
- } else {
- alert("Please enter both name and URL.");
- }
- }
- function addInputField(element, name) {
- event.preventDefault();
- const div = document.createElement('div');
- div.className = 'project-details-input';
- div.innerHTML = `
- <label for="${name}"></label>
- <div>
- <input class="input-style" type="text" id= "${name}" name="${name}" placeholder="Add a ${name.replace('-', ' ')}">
- <span class="delete-icon" onclick="removeInputField(this)">✖</span>
- </div>
- `;
- element.parentElement.parentElement.parentElement.insertAdjacentElement('afterend', div);
- }
- function removeInputField(element) {
- element.parentElement.remove();
- }
- function removeInputTextField(element) {
- element.parentElement.parentElement.remove();
- }
- function openModal() {
- document.getElementById('modal').style.display = 'flex';
- }
- function closeModal() {
- document.getElementById('modal').style.display = 'none';
- }
- /* GIT FUNCTIONALITY */
- let typingTimer; // Timer identifier
- const typingDelay = 1000; // Time in ms (1 second)
- const repoNameInput = document.getElementById('repo-name');
- const statusElement = document.getElementById('repo-status');
- // Function to check repository name availability
- async function checkRepoAvailability(repoName, owner, org) {
- try {
- const response = await fetch('/project/check-repo-exists/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- name: repoName,
- owner: owner,
- org: org // Include the org if it is provided
- }),
- });
- const data = await response.json();
- if (response.ok) {
- return data.exists; // Returns true or false
- } else {
- throw new Error(data.error || 'Unknown error while checking repository');
- }
- } catch (error) {
- console.error('Error checking repository:', error.message);
- return null; // Error occurred
- }
- }
- // Function to handle typing event
- async function handleTyping() {
- const repoName = repoNameInput.value.trim();
- const owner = document.getElementById('owner').value;
- if (repoName) {
- const exists = await checkRepoAvailability(repoName, owner);
- if (exists === null) {
- statusElement.textContent = 'Error checking repository availability. Please try again.';
- statusElement.style.color = 'red';
- } else if (exists) {
- statusElement.textContent = `${repoName} is already taken.`;
- statusElement.style.color = 'red';
- } else {
- statusElement.textContent = `${repoName} is available.`;
- statusElement.style.color = 'green';
- }
- } else {
- statusElement.textContent = 'Please enter a repository name.';
- statusElement.style.color = 'red';
- }
- }
- // Event listener for typing
- repoNameInput.addEventListener('keyup', () => {
- clearTimeout(typingTimer); // Clear the timer if the user is still typing
- typingTimer = setTimeout(handleTyping, typingDelay); // Set a new timer
- });
- // Optional: Clear the status if the user starts typing again
- repoNameInput.addEventListener('keydown', () => {
- clearTimeout(typingTimer); // Clear the timer to prevent premature checks
- statusElement.textContent = ''; // Clear the status text
- });
- async function createRepo(repoName, owner, org) {
- try {
- const response = await fetch('/project/create-repo/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- name: repoName,
- owner: owner,
- org: org, // Include org if it's an organization
- project_id: "{{project_detail.id}}"
- }),
- });
- const data = await response.json();
- if (response.ok) {
- return { success: true, message: data.message };
- } else {
- throw new Error(data.error || 'Unknown error while creating repository');
- }
- } catch (error) {
- console.error('Error creating repository:', error);
- return { success: false, message: error};
- }
- }
- // Function to handle repository creation process
- async function createRepository() {
- const repoName = document.getElementById('repo-name').value.trim();
- const owner = document.getElementById('owner').value; // Can be a user or an org
- const org = document.getElementById('organization').value; // Optional org selection
- const statusElement = document.getElementById('repo-status');
- // Validate inputs
- if (!repoName || !owner) {
- statusElement.textContent = 'Please provide both owner and repository name.';
- statusElement.style.color = 'red';
- return;
- }
- // Check if the repository already exists based on whether it's an org or user
- const exists = await checkRepoAvailability(repoName, owner, org);
- // Handle errors from the availability check
- if (exists === null) {
- statusElement.textContent = 'Error checking repository availability. Please try again.';
- statusElement.style.color = 'red';
- return;
- }
- // Inform the user if the repository already exists
- if (exists) {
- statusElement.textContent = `${repoName} already exists.`;
- statusElement.style.color = 'red';
- } else {
- // Attempt to create the repository
- const result = await createRepo(repoName, owner, org);
- if (result.success) {
- // Show success message and reload the page
- statusElement.textContent = result.message;
- statusElement.style.color = 'green';
- window.location.reload();
- } else {
- // Show error message if creation failed
- statusElement.textContent = result.message;
- statusElement.style.color = 'red';
- }
- }
- }
- function toggleCreateRepoForm() {
- const repoForm = document.getElementById("repo-form");
- const existingRepositories = document.getElementById("modal-content-exisiting-repo");
- // Toggle visibility
- if (repoForm.style.display === "none") {
- repoForm.style.display = "flex";
- existingRepositories.style.display = "none"; // Hide existing repositories
- } else {
- repoForm.style.display = "none";
- existingRepositories.style.display = "flex"; // Show existing repositories
- }
- }
- function openAssignRepoModal() {
- document.getElementById("assignRepoModal").style.display = "block";
- const currentUserIdAsNumber = Number(currentUserId); // Convert the global variable to a number
- const member = members.find(member => member.id === currentUserIdAsNumber);
- if (member) {
- var name = member.username;
- if (member.name && member.name!== " ") {
- name = member.name;
- } else {
- name = member.username;
- }
- document.getElementById("assign-modal-title").innerText = "Assign GitHub Repo to " + name;
- if (!member.github_username) {
- // Display the warning message and disable the form
- document.getElementById("github-warning").innerHTML =
- name + " has not set their GitHub username. </br> Please ask them to set their github username before assigning repositories.";
- document.getElementById("github-warning").style.display = "block";
- // Disable checkboxes and submit button
- disableForm(true);
- } else {
- // Hide the warning message and enable the form
- document.getElementById("github-warning").style.display = "none";
- disableForm(false);
- }
- } else {
- console.error("Member not found.");
- }
- }
- function openRevokeRepoModal() {
- document.getElementById("revokeRepoModal").style.display = "flex";
- const currentUserIdAsNumber = Number(currentUserId);
- const member = members.find(member => member.id === currentUserIdAsNumber);
- if (member) {
- var name = member.username;
- if (member.name && member.name!== " ") {
- name = member.name;
- } else {
- name = member.username;
- }
- document.getElementById("revoke-modal-title").innerText = "Revoke GitHub Repo from " + name;
- if (!member.github_username) {
- // Display the warning message and disable the form
- document.getElementById("github-warning").innerHTML =
- name + " has not set their GitHub username. </br> Please ask them to set their github username before revoking repositories.";
- document.getElementById("github-warning").style.display = "block";
- // Disable checkboxes and submit button
- disableForm(true);
- } else {
- // Hide the warning message and enable the form
- document.getElementById("github-warning").style.display = "none";
- disableForm(false);
- }
- } else {
- console.error("Member not found.");
- }
- }
- function disableForm(disable) {
- const checkboxes = document.querySelectorAll('.assign-repo-form input[type="checkbox"]');
- const submitButton = document.querySelector('.assign-repo-submit-btn');
- checkboxes.forEach(checkbox => {
- checkbox.disabled = disable;
- if (disable) {
- checkbox.classList.add('disabled-checkbox'); // Add a class to style disabled checkboxes
- } else {
- checkbox.classList.remove('disabled-checkbox'); // Remove the class
- }
- });
- submitButton.disabled = disable; // Disable the button
- if (disable) {
- submitButton.classList.add('disabled-button'); // Add disabled class
- } else {
- submitButton.classList.remove('disabled-button'); // Remove disabled class
- }
- }
- function closeAssignRepoModal() {
- const checkboxes = document.querySelectorAll('#assignRepoModal input[type="checkbox"]');
- checkboxes.forEach(checkbox => checkbox.checked = false);
- document.getElementById("assignRepoModal").style.display = "none";
- }
- function closeRevokeRepoModal() {
- const checkboxes = document.querySelectorAll('#revokeRepoModal input[type="checkbox"]');
- checkboxes.forEach(checkbox => checkbox.checked = false);
- document.getElementById("revokeRepoModal").style.display = "none";
- }
- async function submitAssignRepoForm(event) {
- event.preventDefault();
- const checkboxes = document.querySelectorAll('input[name="repo"]:checked');
- const selectedRepos = Array.from(checkboxes).map(cb => cb.value);
- const statusElement = document.getElementById('status-message');
- if (selectedRepos.length === 0) {
- alert('Please select at least one repository.');
- return;
- }
- try {
- for (let repoName of selectedRepos) {
- const response = await fetch('/project/assign-repo/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- repo_name: repoName,
- user_id: currentUserId,
- projectId : projectId,
- }),
- });
- const data = await response.json();
- if (response.ok) {
- alert(` ${repoName} assigned successfully`);
- closeAssignRepoModal();
- } else {
- alert(`Error : ${data.error.message || data.error || 'Unknown error'}`);
- }
- }
- } catch (error) {
- alert(`An error occurred: ${error.message}`);
- }
- }
- async function submitRevokeRepoForm(event) {
- event.preventDefault();
- const checkboxes = document.querySelectorAll('input[name="revokerepo"]:checked');
- const selectedRepoIds = Array.from(checkboxes).map(cb => cb.value);
- const statusElement = document.getElementById('status-message');
- if (selectedRepoIds.length === 0) {
- alert('Please select at least one repository.');
- return;
- }
- try {
- // Make a single API request with the selected repository IDs and user ID
- const response = await fetch('/project/revoke-repo/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- repo_ids: selectedRepoIds,
- user_id: currentUserId,
- }),
- });
- const data = await response.json();
- if (response.ok) {
- alert(data.message);
- closeRevokeRepoModal();
- if (data.errors){
- console.log(data.errors);
- }
- } else {
- if (data.status === 'error' && data.message === 'User not found.') {
- alert('Error: User not found.');
- } else if (data.status === 'error' && data.message === 'No repositories found for the provided repository IDs.') {
- alert('Error: No repositories found.');
- } else {
- alert(`Error: ${data.message || 'Unknown error occurred.'}`);
- }
- }
- } catch (error) {
- alert(`An error occurred: ${error.message}`);
- }
- }
- /* MILESTONE CHANGES */
- function updateMilestoneColor(milestoneElement, status) {
- switch (status) {
- case 'completed':
- milestoneElement.style.backgroundColor = '#50CF66';
- milestoneElement.style.color = '#08090A';
- break;
- case 'on-progress':
- milestoneElement.style.backgroundColor = '#FCC418';
- milestoneElement.style.color = '#08090A';
- break;
- default:
- milestoneElement.style.backgroundColor = '#CED4DA';
- milestoneElement.style.color = '#939CA3';
- break;
- }
- }
- function showMilestonePopup(event) {
- const clickedDiv = event.currentTarget;
- const milestonePopup = document.getElementById('milestone-action-popup');
- const rect = clickedDiv.getBoundingClientRect();
- const offsetX = window.scrollX;
- const offsetY = window.scrollY;
- milestonePopup.style.left = `${(rect.left + offsetX)/zoom}px`;
- milestonePopup.style.top = `${(rect.bottom + offsetY)/zoom}px`;
- milestonePopup.style.display = 'block';
- // store the clicked milestone id in data-attribute
- milestonePopup.setAttribute('data-current-milestone', clickedDiv.id);
- const handleClickOutside = (event) => {
- if (!milestonePopup.contains(event.target) && !clickedDiv.contains(event.target)) {
- milestonePopup.style.display = 'none';
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- }
- function setMilestoneStatus(status) {
- const currentMilestoneId = document.getElementById('milestone-action-popup').getAttribute('data-current-milestone');
- if (currentMilestoneId) {
- milestoneStatuses[currentMilestoneId].status = status;
- const milestoneElement = document.getElementById(currentMilestoneId);
- updateMilestoneColor(milestoneElement, status);
- // hide the popup after updating
- document.getElementById('milestone-action-popup').style.display = 'none';
- fetch('/project/update_milestone/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- milestoneStatuses: milestoneStatuses,
- project_id: projectId
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- window.location.reload();
- console.log('Milestone statuses updated successfully.');
- } else {
- console.error('Error:', data.message);
- }
- })
- .catch(error => console.error('Error:', error));
- }
- }
- /* ADDING NEW LINKS */
- function addNewUrl() {
- // Create the new link-add div elements dynamically
- const newLinkAddDiv = document.createElement('div');
- const newLinkNameInput = document.createElement('input');
- const newUrlInput = document.createElement('input');
- const removeButton = document.createElement('div');
- // Set class and attributes
- newLinkAddDiv.className = 'link-add';
- newLinkNameInput.type = 'text';
- newLinkNameInput.placeholder = 'Add Link name';
- newUrlInput.type = 'text';
- newUrlInput.placeholder = 'Add URL';
- removeButton.className = 'remove-button';
- removeButton.textContent = 'x';
- removeButton.onclick = (event) => removeThisUrl(event);
- // append inputs and remove button to link-add div
- newLinkAddDiv.appendChild(newLinkNameInput);
- newLinkAddDiv.appendChild(newUrlInput);
- newLinkAddDiv.appendChild(removeButton);
- // find the link-list div and append new link-add div to it
- const linkList = document.getElementById('link-list');
- linkList.appendChild(newLinkAddDiv);
- // show the save button
- document.getElementById('save-button').style.display = 'inline-block';
- }
- ////function for remove url added
- function removeThisUrl(event) {
- // find the link-aadd div that contains the remove button
- const linkAddDiv = event.target.parentElement;
- // remve the div
- linkAddDiv.parentNode.removeChild(linkAddDiv);
- // hide the save button if no link-add divs are present
- const linkList = document.getElementById('link-list');
- if (linkList.children.length === 0) {
- document.getElementById('save-button').style.display = 'none';
- }
- }
- //function for saving new links
- function saveNewLinks() {
- const linkList = document.getElementById('link-list');
- const documentList = document.getElementById('document-list');
- linkList.querySelectorAll('.link-add').forEach(linkAddDiv => {
- const linkName = linkAddDiv.querySelector('input[placeholder="Add Link name"]').value;
- const linkUrl = linkAddDiv.querySelector('input[placeholder="Add URL"]').value;
- if (linkName && linkUrl) {
- // ceate new link element
- const newLinkDiv = document.createElement('div');
- newLinkDiv.style.display = 'flex';
- newLinkDiv.style.justifyContent = 'space-between';
- const newLink = document.createElement('a');
- newLink.href = linkUrl;
- newLink.style.textDecoration = 'underline';
- newLink.style.color = '#2F6CE5';
- newLink.target = '_blank';
- newLink.textContent = linkName;
- const timeAgo = document.createElement('div');
- timeAgo.style.color = '#939CA3';
- timeAgo.textContent = 'Just now';
- newLinkDiv.appendChild(newLink);
- newLinkDiv.appendChild(timeAgo);
- documentList.appendChild(newLinkDiv);
- fetch('/project/addnewlink/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- link_name: linkName,
- link_url: linkUrl,
- project_id: "{{project_detail.id}}"
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- console.log('Link saved successfully.');
- } else {
- console.error('Error saving link:', data.error);
- }
- })
- .catch(error => {
- console.error('Error:', error);
- });
- }
- });
- // hide the link list and save button
- linkList.innerHTML = '';
- }
- /* EMAIL TEMPLATES */
- function preSprintDocEmailTemplate() {
- let template = document.getElementById('pre-sprint-doc-email-template').innerText;
- // template = template.replace(/\s+/g, ' ').trim();
- const link = "https://www.figma.com/design/re7NRY22F5JxDITK8z59Rv/3262-%3A-Intranet---Banao-integration-to-Intranet?node-id=3843-8124&node-type=frame&t=4h98xXAjCTNxycRa-0"
- template = `Greetings from Banao Technologies!\n\n` +
- `This is to inform you that we are kick-starting the development hereon 18.09.2023 and starting with the first iterations of designs.\n\n` +
- `PFA the link to the PSD : ${link}\n\n` +
- ` β’ The main objective of this document is to get further clarity on the existing agreed upon flow/expectations, especially wrt the design and flow expectations.\n` +
- ` β’ These documents will have niche details regarding the next sprint/milestone (adhering to the scope in the original proposal).\n` +
- ` β’ Anything that is not part of this document will be considered a change request during the execution of the milestone/sprint later on.\n` +
- ` β’ The common things that are missed out by the clients in initial requirements include:\n` +
- ` β’ Loading, Error, New User (Empty) states\n` +
- ` β’ User specific logics.\n` +
- ` β’ Prefer to add your text in a different font/colour.\n\n` +
- `A pre-sprint discussion will happen soon and we will ask for clarity on the points and flag any of the niche points during the discussion later in case they fall outside the scope (confirming to the proposal/FRD).\n\n` +
- `Looking Forward to connecting over a call.\n` +
- `Please feel free to put your queries and Iβd be happy to assist with.\n\n` +
- `Regards,\n\n` +
- `Rishi\n` +
- `Project Manager,\n` +
- `Banao Technologies\n` +
- `(https://banao.tech/)`;
- navigator.clipboard.writeText(template).then(function () {
- alert("Email template copied to clipboard!");
- }, function (err) {
- alert("Error copying template: " + err);
- });
- }
- function closeEmailTemplate() {
- document.getElementById("pre-sprint-doc-email-modal").style.display = "none";
- }
- function designConfirmationEmailTemplate() {
- let template = document.getElementById('design-confirmation-email-template').innerText;
- navigator.clipboard.writeText(template).then(function () {
- alert("Email template copied to clipboard!");
- }, function (err) {
- alert("Error copying template: " + err);
- });
- }
- function closedesignConfirmationEmailTemplate() {
- document.getElementById("design-confirmation-email-modal").style.display = "none";
- }
- function designLockEmailTemplate() {
- let template = document.getElementById('design-lock-email-template').innerText;
- navigator.clipboard.writeText(template).then(function () {
- alert("Email template copied to clipboard!");
- }, function (err) {
- alert("Error copying template: " + err);
- });
- }
- function closedesignLockEmailTemplate() {
- document.getElementById("design-lock-email-modal").style.display = "none";
- }
- function milestoneTimelineEmailTemplate() {
- let template = document.getElementById('milestone-timeline-email-template').innerText;
- navigator.clipboard.writeText(template).then(function () {
- alert("Email template copied to clipboard!");
- }, function (err) {
- alert("Error copying template: " + err);
- });
- }
- function closeMilestoneTimelineEmailTemplate() {
- document.getElementById("milestone-timeline-email-modal").style.display = "none";
- }
- function milestoneClosureEmailTemplate() {
- let template = document.getElementById('milestone-closure-email-template').innerText;
- navigator.clipboard.writeText(template).then(function () {
- alert("Email template copied to clipboard!");
- }, function (err) {
- alert("Error copying template: " + err);
- });
- }
- function closeMilestoneClosureEmailTemplate() {
- document.getElementById("milestone-closure-email-modal").style.display = "none";
- }
- function projectClosureEmailTemplate() {
- let template = document.getElementById('project-closure-email-template').innerText;
- // template = template.replace(/\s+/g, ' ').trim();
- navigator.clipboard.writeText(template).then(function () {
- alert("Email template copied to clipboard!");
- }, function (err) {
- alert("Error copying template: " + err);
- });
- }
- function closeProjectClosureEmailTemplate() {
- document.getElementById("project-closure-email-modal").style.display = "none";
- }
- function supportPeriodEmailTemplate() {
- let template = document.getElementById('support-period-email-template').innerText;
- // template = template.replace(/\s+/g, ' ').trim();
- navigator.clipboard.writeText(template).then(function () {
- alert("Email template copied to clipboard!");
- }, function (err) {
- alert("Error copying template: " + err);
- });
- }
- function closeSupportPeriodEmailTemplate() {
- document.getElementById("support-period-email-modal").style.display = "none";
- }
- /* Internal tasks funcs */
- function formatDateForInput(dateString) {
- if (!dateString) return ''; // Return empty if no date provided
- // Parse the date from the string (assuming it's in ISO or valid date format)
- let date = new Date(dateString);
- // Extract the year, month (note: JS months are zero-indexed), and day
- let year = date.getFullYear();
- let month = ('0' + (date.getMonth() + 1)).slice(-2); // Add 1 to month since it's zero-indexed
- let day = ('0' + date.getDate()).slice(-2); // Pad with leading zero if necessary
- // Return formatted as YYYY-MM-DD
- return `${year}-${month}-${day}`;
- }
- function deleteTask(deleteBtn, taskId) {
- const row = deleteBtn.closest('tr');
- const parentTaskId = row.getAttribute('data-task-id');
- const confirmDelete = confirm('Are you sure you want to delete this task? Note: All subtasks will be deleted (if any)');
- if (!confirmDelete) {
- return;
- }
- fetch(`/project/delete-task/`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- task_id: taskId,
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- // If this is a subtask
- if (row.classList.contains('subtask-row')) {
- // Only remove this specific subtask row
- row.remove();
- // Update parent task's subtask count and status
- if (parentTaskId) {
- const parentRow = document.getElementById(parentTaskId);
- const subtaskCountSpan = parentRow.querySelector('.subtask-count');
- let currentCount = parseInt(subtaskCountSpan.textContent.replace(/\D/g, '')) || 0;
- currentCount--;
- subtaskCountSpan.textContent = `(${currentCount})`;
- // Update the parent task's progress
- updateProgress(parentRow);
- // If this was the last subtask, hide the toggle arrow
- if (currentCount === 0) {
- const toggleSvg = parentRow.querySelector('#togglesvg');
- const checkbox = parentRow.querySelector('.circular-checkbox');
- if (toggleSvg) {
- toggleSvg.style.display = 'none';
- }
- if (checkbox) {
- checkbox.style.display = 'block';
- }
- }
- }
- } else {
- // This is a main task - remove it and all its subtasks
- let nextRow = row.nextElementSibling;
- row.remove();
- while (nextRow && nextRow.classList.contains('subtask-row') &&
- nextRow.getAttribute('data-task-id') === taskId) {
- const rowToRemove = nextRow;
- nextRow = nextRow.nextElementSibling;
- rowToRemove.remove();
- }
- }
- } else {
- alert('Error deleting task: ' + data.message);
- }
- })
- .catch(error => {
- console.error('Error:', error);
- alert('Error deleting task.');
- });
- }
- function saveTask(saveBtn) {
- let row = saveBtn.closest('tr');
- let taskId = row.id; // Assuming the row's id is the task id
- let newName = row.querySelector('.edit-task-name').value.trim() || 'Unnamed task'; // Default to "Unnamed task" if blank
- let newAssignee = row.querySelector('.assignee-input select').value || 'Unassigned'; // Default to 'Unassigned'
- let newDueDate = row.querySelector('.edit-due-date').value || ''; // Allow empty due date
- let newBounties = row.querySelector('.edit-bounties').value.trim() || '0'; // Default to '0' if blank
- // Perform the AJAX request (or other save logic)
- fetch(`/project/update-task/`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- task_id: taskId,
- task_name: newName,
- assignee: newAssignee,
- due_date: newDueDate,
- bounties: newBounties
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.status === 'success') {
- // Replace the input fields with updated values
- let assigneeName = 'Unassigned';
- let assignee = team_members.find(member => member.id == newAssignee);
- if (assignee) {
- assigneeName = assignee.name; // Extract name from team_members array
- }
- row.querySelector('.task-name').innerHTML = newName || 'Unnamed task'; // Default display if empty
- row.querySelector('.assignee-input').innerHTML = assigneeName || 'Unassigned'; // Display 'Unassigned' if empty
- if (newDueDate){
- row.querySelector('.date-input').value = newDueDate || ''; // Leave empty if no date
- }
- row.querySelector('.bountie-input').innerHTML = newBounties || '1'; // Default to '0' if empty
- // Replace the save button with the pen icon again
- saveBtn.parentElement.outerHTML = `
- <svg class="edit-task-svg" onclick="makeTaskEditable(this)" style="cursor: pointer;" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M2 16.5V21H6.5L18.51 8.99L14.01 4.49L2 16.5ZM22.41 7.34C22.8 6.95 22.8 6.32 22.41 5.93L18.07 1.59C17.68 1.2 17.05 1.2 16.66 1.59L14.91 3.34L20.66 9.09L22.41 7.34Z" fill="#6D747A"/>
- </svg>`;
- } else {
- alert('Error saving task');
- }
- })
- .catch(error => {
- console.error('Error:', error);
- });
- }
- function getInitials(username) {
- const nameParts = username.trim().split(/\s+/);
- return nameParts.map(part => part[0].toUpperCase()).join('');
- }
- function createInitialsCircle(initials, username, assignIcon,taskId) {
- const circle = document.createElement('div');
- circle.className = 'initials-circle';
- circle.style.width = '25px';
- circle.style.height = '25px';
- circle.style.borderRadius = '50%';
- circle.style.display = 'flex';
- circle.style.alignItems = 'center';
- circle.style.justifyContent = 'center';
- circle.style.backgroundColor = '#7c4dff';
- circle.style.color = '#fff';
- circle.style.cursor = 'pointer';
- circle.style.fontSize = '14px';
- circle.textContent = initials;
- assignIcon.innerHTML = '';
- assignIcon.appendChild(circle);
- assignIcon.title = username;
- assignIcon.style.margin = "2px";
- assignIcon.addEventListener('click', () => openAssigneeDropdown(taskId,assignIcon));
- }
- function openAssigneeDropdown(taskId, button) {
- const existingDropdown = document.querySelector('.custom-dropdown');
- if (existingDropdown) {
- existingDropdown.remove();
- return;
- }
- if (taskId == "none"){
- taskId = currentTaskId;
- }
- let dropdown = document.createElement('div');
- dropdown.className = 'custom-dropdown';
- dropdown.style.position = 'absolute';
- dropdown.style.background = 'white';
- dropdown.style.border = '1px solid #ddd';
- dropdown.style.padding = '5px';
- dropdown.style.zIndex = '1000';
- dropdown.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.2)';
- dropdown.style.maxHeight = '200px';
- dropdown.style.overflowY = 'auto';
- let searchInput = document.createElement('input');
- searchInput.type = 'text';
- searchInput.placeholder = 'Search team members';
- searchInput.id="search-team-members";
- searchInput.style.width = '100%';
- searchInput.style.padding = '5px';
- searchInput.style.border = '1px solid #ddd';
- searchInput.style.boxSizing = 'border-box';
- searchInput.style.marginBottom = '10px';
- dropdown.appendChild(searchInput);
- let memberList = document.createElement('div');
- dropdown.appendChild(memberList);
- team_members.forEach(member => {
- const memberItem = document.createElement('div');
- memberItem.textContent = member.name;
- memberItem.style.padding = '5px 10px';
- memberItem.style.cursor = 'pointer';
- memberItem.style.backgroundColor = 'white';
- memberItem.addEventListener('mouseover', () => {
- memberItem.style.backgroundColor = '#f1f1f1';
- });
- memberItem.addEventListener('mouseout', () => {
- memberItem.style.backgroundColor = 'white';
- });
- memberItem.addEventListener('click', () => {
- const initials = getInitials(member.name);
- createInitialsCircle(initials, member.name, button, taskId);
- selectedMember = member.name;
- selectedMemberData = {"id": member.id, "name": member.name};
- fetch('/project/save-assignee/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ taskId: taskId, assigneeId: member.id })
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- console.log('Assignee saved successfully');
- } else {
- console.error('Failed to save assignee');
- }
- })
- .catch(error => {
- console.error('Error:', error);
- });
- dropdown.remove();
- });
- memberList.appendChild(memberItem);
- });
- const rect = button.getBoundingClientRect();
- dropdown.style.left = `${rect.left}px`;
- dropdown.style.top = `${rect.bottom + window.scrollY}px`;
- document.body.appendChild(dropdown);
- const handleClickOutside = (event) => {
- if(event.target.id!="search-team-members"){
- if (!button.contains(event.target)) {
- dropdown.remove();
- document.removeEventListener('click', handleClickOutside);
- }
- }
- };
- document.addEventListener('click', handleClickOutside);
- searchInput.addEventListener('input', function () {
- const query = searchInput.value.toLowerCase();
- const items = memberList.querySelectorAll('div');
- items.forEach(item => {
- const text = item.textContent.toLowerCase();
- if (text.indexOf(query) === -1) {
- item.style.display = 'none';
- } else {
- item.style.display = 'block';
- }
- });
- });
- const clearButton = document.createElement('button');
- clearButton.textContent = 'Clear';
- clearButton.style.padding = '5px 10px';
- clearButton.style.marginTop = '10px';
- clearButton.style.cursor = 'pointer';
- clearButton.style.border = 'none';
- clearButton.style.background = "lightgrey";
- clearButton.addEventListener('click', () => {
- selectedMember = null;
- selectedMemberData = {};
- button.title = "select assignee";
- fetch('/project/save-assignee/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ taskId: taskId, assigneeId: "none" })
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- console.log('Assignee saved successfully');
- } else {
- console.error('Failed to save assignee');
- }
- })
- .catch(error => {
- console.error('Error:', error);
- });
- button.innerHTML =
- `<svg class="assignee-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" style="width: 24px; height: 24px; fill: #ABABAB;">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <path fill-rule="evenodd" clip-rule="evenodd" d="M3 18C3 15.3945 4.66081 13.1768 6.98156 12.348C7.61232 12.1227 8.29183 12 9 12C9.70817 12 10.3877 12.1227 11.0184 12.348C11.3611 12.4703 11.6893 12.623 12 12.8027C12.3107 12.623 12.6389 12.4703 12.9816 12.348C13.6123 12.1227 14.2918 12 15 12C15.7082 12 16.3877 12.1227 17.0184 12.348C19.3392 13.1768 21 15.3945 21 18V21H15.75V19.5H19.5V18C19.5 15.5147 17.4853 13.5 15 13.5C14.4029 13.5 13.833 13.6163 13.3116 13.8275C14.3568 14.9073 15 16.3785 15 18V21H3V18ZM9 11.25C8.31104 11.25 7.66548 11.0642 7.11068 10.74C5.9977 10.0896 5.25 8.88211 5.25 7.5C5.25 5.42893 6.92893 3.75 9 3.75C10.2267 3.75 11.3158 4.33901 12 5.24963C12.6842 4.33901 13.7733 3.75 15 3.75C17.0711 3.75 18.75 5.42893 18.75 7.5C18.75 8.88211 18.0023 10.0896 16.8893 10.74C16.3345 11.0642 15.689 11.25 15 11.25C14.311 11.25 13.6655 11.0642 13.1107 10.74C12.6776 10.4869 12.2999 10.1495 12 9.75036C11.7001 10.1496 11.3224 10.4869 10.8893 10.74C10.3345 11.0642 9.68896 11.25 9 11.25ZM13.5 18V19.5H4.5V18C4.5 15.5147 6.51472 13.5 9 13.5C11.4853 13.5 13.5 15.5147 13.5 18ZM11.25 7.5C11.25 8.74264 10.2426 9.75 9 9.75C7.75736 9.75 6.75 8.74264 6.75 7.5C6.75 6.25736 7.75736 5.25 9 5.25C10.2426 5.25 11.25 6.25736 11.25 7.5ZM15 5.25C13.7574 5.25 12.75 6.25736 12.75 7.5C12.75 8.74264 13.7574 9.75 15 9.75C16.2426 9.75 17.25 8.74264 17.25 7.5C17.25 6.25736 16.2426 5.25 15 5.25Z" />
- </g>
- </svg>`
- dropdown.remove();
- });
- button.addEventListener('click', () => openAssigneeDropdown(taskId,button));
- dropdown.appendChild(clearButton);
- }
- function openPriorityDropdown(taskId, button) {
- // Check if the dropdown is already visible and remove it
- const existingDropdown = document.querySelector('.custom-dropdown');
- if (existingDropdown) {
- existingDropdown.remove();
- return; // Don't create a new dropdown if one is already visible
- }
- // Create the custom dropdown for priority options
- let dropdown = document.createElement('div');
- dropdown.className = 'custom-dropdown';
- dropdown.style.position = 'absolute';
- dropdown.style.background = 'white';
- dropdown.style.border = '1px solid #ddd';
- dropdown.style.padding = '5px';
- dropdown.style.zIndex = '1000';
- dropdown.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.2)';
- dropdown.style.maxHeight = '200px';
- dropdown.style.overflowY = 'auto';
- dropdown.style.minWidth = '200px';
- // Define the priority options
- const priorities = {
- urgent: "Urgent",
- high: "High",
- normal: "Normal",
- low: "Low"
- };
- // Create the dropdown options
- Object.keys(priorities).forEach(priority => {
- const option = document.createElement('div');
- option.textContent = priorities[priority];
- option.style.padding = '5px 10px';
- option.style.cursor = 'pointer';
- option.style.backgroundColor = 'white';
- // Hover effect for the dropdown items
- option.addEventListener('mouseover', () => {
- option.style.backgroundColor = '#f1f1f1';
- });
- option.addEventListener('mouseout', () => {
- option.style.backgroundColor = 'white';
- });
- // When an option is selected, update the flag color
- option.addEventListener('click', () => {
- selectedPriority = priority;
- const svgElement = button.querySelector('svg');
- // Select the paths inside the SVG
- const priorityIconPath = svgElement.querySelector('#priority-icon-path');
- const priorityIconExc = svgElement.querySelector('#priority-iconborder');
- const priorityOutline = svgElement.querySelector('#priority-icon-exclamation');
- // Define color changes for different priorities
- const colors = {
- urgent: "red",
- high: "gold",
- normal: "transparent",
- low: "grey"
- };
- // Change the color of the flag icon based on selected priority
- if (selectedPriority !== "none") {
- priorityIconPath.setAttribute('fill', colors[selectedPriority]);
- priorityIconPath.setAttribute('stroke',colors[selectedPriority]);
- if (colors[selectedPriority] == "red" )
- {
- priorityOutline.setAttribute('stroke', 'white');
- }
- else if (colors[selectedPriority] == "transparent"){
- priorityOutline.setAttribute('stroke', 'grey');
- priorityIconPath.setAttribute('stroke', 'grey');
- }
- else{
- priorityOutline.setAttribute('stroke', 'black');
- }
- }
- // Remove the dropdown after selection
- dropdown.remove();
- fetch('/project/update-priority/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- taskId: taskId,
- priority: selectedPriority
- })
- })
- .then(response => response.json())
- .then(data => {
- console.log('Priority updated successfully', data);
- })
- .catch(error => {
- console.error('Error updating priority:', error);
- });
- });
- dropdown.appendChild(option);
- });
- // Create a "Clear" button as part of the dropdown
- const clearButton = document.createElement('button');
- clearButton.textContent = 'Clear';
- clearButton.style.padding = '5px 10px';
- clearButton.style.marginTop = '10px';
- clearButton.style.cursor = 'pointer';
- clearButton.style.border = 'none';
- clearButton.style.background = "lightgrey";
- // When the "Clear" button is clicked, reset to original state
- clearButton.addEventListener('click', () => {
- const priorityIconPath = document.getElementById('priority-icon-path');
- priorityIconPath.setAttribute('fill', "blue");
- priorityIconPath.setAttribute('stroke', "blue");
- // Remove the dropdown after clicking "Clear"
- dropdown.remove();
- fetch('/project/update-priority/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- taskId: taskId,
- priority: 'normal'
- })
- })
- .then(response => response.json())
- .then(data => {
- console.log('Priority updated successfully', data);
- })
- .catch(error => {
- console.error('Error updating priority:', error);
- });
- });
- dropdown.appendChild(clearButton);
- // Position the dropdown below the icon
- const rect = button.getBoundingClientRect();
- const zoomFactor = 1; // Optional zoom adjustment
- dropdown.style.left = `${rect.left / zoomFactor}px`; // Adjust for zoom level
- dropdown.style.top = `${(rect.bottom + window.scrollY) / zoomFactor}px`; // Adjust for zoom level and page scroll
- document.body.appendChild(dropdown);
- const handleClickOutside = (event) => {
- if (!button.contains(event.target)) {
- dropdown.remove();
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- }
- function openDueDateDropdown(taskId, dateIcon) {
- // Check if the date picker is already visible and remove it
- const existingDropdown = document.querySelector('.date-picker-dropdown');
- if (existingDropdown) {
- existingDropdown.remove();
- return;
- }
- if (taskId == "none"){
- taskId = currentTaskId;
- }
- // Create the date input field (it will appear as a dropdown)
- const dateInput = document.createElement('input');
- dateInput.type = 'date';
- dateInput.className = 'date-icon-input';
- dateInput.addEventListener('change', function () {
- selectedDate = new Date(this.value);
- const formattedDate = selectedDate.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: '2-digit' });
- // Update the icon's HTML with the selected date
- dateIcon.innerHTML = `${formattedDate}`;
- dateIcon.offsetWidth;
- dateIcon.style.width = "auto";
- dateIcon.style.minWidth = "100px";
- // Check if this is a checkpoint task and update flag color if needed
- const row = dateIcon.closest('tr');
- if (row && row.classList.contains('task-row-checkpoint')) {
- const flagIcon = row.querySelector('.flag-icon svg path');
- if (flagIcon) {
- const today = new Date();
- today.setHours(0, 0, 0, 0);
- // If task is marked incomplete (not green)
- if (flagIcon.getAttribute('fill') !== 'green') {
- // Update color based on the new date
- if (selectedDate > today) {
- flagIcon.setAttribute('fill', 'grey'); // Future date = grey
- } else {
- flagIcon.setAttribute('fill', '#DD1D43'); // Past date = red
- }
- }
- }
- }
- // Hide the dropdown after date selection
- dropdown.remove();
- });
- // Create a dropdown container for the date input field
- const dropdown = document.createElement('div');
- dropdown.classList.add('date-picker-dropdown', 'input-style');
- dropdown.style.position = 'absolute';
- dropdown.style.zIndex = '9999';
- dropdown.style.width = "200px";
- const rect = dateIcon.getBoundingClientRect();
- const zoomFactor = 1;
- dropdown.style.left = `${rect.left / zoomFactor}px`;
- dropdown.style.top = `${(rect.bottom + window.scrollY) / zoomFactor}px`;
- dropdown.appendChild(dateInput);
- // Create a "Clear" button in the next row
- const clearButton = document.createElement('button');
- clearButton.textContent = 'Clear';
- clearButton.classList.add('clear-button');
- clearButton.style.marginTop = '10px';
- clearButton.style.padding = '5px 10px';
- clearButton.style.cursor = 'pointer';
- clearButton.style.backgroundColor = '#f0f0f0';
- clearButton.style.border = '1px solid #ddd';
- clearButton.style.borderRadius = '4px';
- clearButton.style.fontSize = '14px';
- clearButton.addEventListener('click', () => {
- fetch('/project/update-duedate/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- taskId: taskId,
- date: ""
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- dateInput.value = '';
- dateIcon.style.width = "35px";
- dateIcon.style.minWidth = "35px";
- dateIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_iconCarrier">
- <path d="M19 2H5C3.9 2 3 2.9 3 4V20C3 21.1 3.9 22 5 22H19C20.1 22 21 21.1 21 20V4C21 2.9 20.1 2 19 2ZM19 20H5V8H19V20ZM5 4H7V6H5V4ZM19 4H17V6H19V4Z" fill="#ababab"/>
- </g>
- </svg>`;
- dropdown.remove();
- } else {
- alert('Failed to clear due date. Please try again.');
- }
- })
- .catch(error => {
- console.error('Error clearing due date:', error);
- alert('Failed to clear due date. Please try again.');
- });
- });
- const row = dateIcon.closest('tr');
- if (row && row.classList.contains('task-row-checkpoint')) {
- const flagIcon = row.querySelector('.flag-icon svg path');
- if (flagIcon && flagIcon.getAttribute('fill') !== 'green') {
- // No date = red by default
- flagIcon.setAttribute('fill', '#DD1D43');
- }
- }
- dropdown.appendChild(clearButton);
- document.body.appendChild(dropdown);
- const handleClickOutside = (event) => {
- if (!dateIcon.contains(event.target) && !dateInput.contains(event.target)) {
- dropdown.remove();
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- dateInput.addEventListener('change', function () {
- const selectedDate = new Date(this.value);
- const formatted = selectedDate ?
- `${selectedDate.getFullYear()}-${String(selectedDate.getMonth() + 1).padStart(2, '0')}-${String(selectedDate.getDate()).padStart(2, '0')}`
- : '';
- // Store original icon state
- const originalIcon = dateIcon.innerHTML;
- const originalWidth = dateIcon.style.width;
- const originalMinWidth = dateIcon.style.minWidth;
- fetch('/project/update-duedate/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- taskId: taskId,
- date: formatted
- })
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- const { label, color } = getDateLabelAndColor(formatted);
- dateIcon.innerHTML = label;
- dateIcon.style.color = color;
- dateIcon.style.width = "auto";
- dateIcon.style.minWidth = "100px";
- dropdown.remove();
- } else {
- throw new Error('Failed to update due date');
- }
- })
- .catch(error => {
- console.error('Error:', error);
- // Revert to original state
- dateIcon.innerHTML = originalIcon;
- dateIcon.style.width = originalWidth;
- dateIcon.style.minWidth = originalMinWidth;
- alert('Failed to update due date. Please try again.');
- });
- });
- }
- function openBountyDropdown(taskId, bountyIcon)
- {
- let currentValue = bountyIcon.value || "";
- const container = document.createElement('div');
- container.classList.add('bounty-container');
- container.style.display = 'inline-flex';
- container.style.alignItems = 'center';
- container.style.gap = '5px';
- container.style.width = '35px';
- // Create input field if clicking on the current text
- const inputField = document.createElement('input');
- inputField.type = 'text';
- inputField.className = 'bounty-input';
- inputField.value = currentValue;
- inputField.style.textAlign = 'center';
- inputField.style.border = '1px solid #ababab';
- inputField.style.borderRadius = '4px';
- inputField.style.backgroundColor = 'transparent';
- inputField.style.width = "35px";
- // Append the input field into the container
- container.appendChild(inputField);
- // Replace the SVG or text with the container holding the input field
- bountyIcon.innerHTML = '';
- bountyIcon.appendChild(container);
- inputField.focus();
- const value = inputField.value.trim(); // Get the trimmed value
- if (value) {
- currentValue = value; // Save the value entered by the user
- currentHtml = value;}
- inputField.addEventListener('blur', function () {
- const value = inputField.value.trim(); // Get the trimmed value
- // If the input is not empty, save the new value
- if (value) {
- currentValue = value; // Save the value entered by the user
- currentHtml = value;
- }
- else {
- currentValue = 1;
- currentHtml = 1;
- }
- fetch('/project/update-bounty/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- taskId: taskId,
- bounty: currentValue
- })
- })
- .then(response => response.json())
- .then(data => {
- console.log('Priority updated successfully', data);
- })
- .catch(error => {
- console.error('Error updating priority:', error);
- });
- bountyIcon.innerHTML = currentHtml;
- });
- inputField.addEventListener('keydown', function(event) {
- if (event.key === 'Enter') {
- const value = inputField.value.trim(); // Get the trimmed value
- // If the input is not empty, save the new value
- if (value) {
- currentValue = value; // Save the value entered by the user
- currentHtml = value;
- }
- else {
- currentValue = 1;
- currentHtml = 1;
- }
- fetch('/project/update-bounty/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- taskId: taskId,
- bounty: currentValue
- })
- })
- .then(response => response.json())
- .then(data => {
- console.log('Priority updated successfully', data);
- })
- .catch(error => {
- console.error('Error updating priority:', error);
- });
- bountyIcon.innerHTML = currentHtml;
- }
- });
- }
- document.querySelectorAll('.bountie-input').forEach(bounty => {
- bounty.addEventListener('click', function(event) {
- const userCondition = document.getElementById('user-condition').dataset.userAllowed;
- if (userCondition === "true") {
- const button = bounty.querySelector('.bounty-btn');
- const id = button.id;
- openBountyDropdown(id,button);
- }
- });
- });
- let originalOrder = []; // Array to store the original order of rows
- let sortState = {}; // Store the sort state for each column (og, asc, desc)
- function storeOriginalOrder(table) {
- const rows = Array.from(table.querySelectorAll('tbody tr'));
- originalOrder = rows.map(row => row.cloneNode(true)); // Clone rows to preserve original order
- console.log(originalOrder)
- }
- function restoreOriginalOrder(table) {
- const tbody = table.querySelector('tbody');
- tbody.innerHTML = ''; // Clear current rows
- originalOrder.forEach(row => tbody.appendChild(row)); // Append original rows
- }
- function getColumnIndex(column, key) {
- const table = document.getElementById(`milestone-${key}`); // Get the table element dynamically using the key
- const headers = Array.from(table.querySelectorAll('th'));
- return headers.indexOf(column); // Get the index of the clicked column
- }
- function groupRowsByTaskId(rows) {
- const grouped = {};
- rows.forEach(row => {
- const taskId = row.getAttribute('data-task-id');
- if (!grouped[taskId]) {
- grouped[taskId] = [];
- }
- grouped[taskId].push(row);
- });
- return Object.values(grouped); // Return an array of grouped rows
- }
- function sortTable(column, key) {
- const table = document.getElementById(`milestone-${key}`); // Get the table element dynamically using the key
- if (!table) return; // Exit if the table doesn't exist
- // Check if original order is already stored, otherwise store it
- if (originalOrder.length === 0) {
- storeOriginalOrder(table);
- }
- const rows = Array.from(table.querySelectorAll('tbody tr')); // Get all rows in the table body
- const filteredRows = rows.filter(row => !row.classList.contains('add-task-rows')); // Exclude 'add-task-row'
- const index = getColumnIndex(column, key); // Get the column index based on the clicked <th>
- // Group rows by task_id (main task and subtasks together)
- const groupedRows = groupRowsByTaskId(filteredRows);
- // Initialize the sort state for this column if not already initialized
- if (!sortState[key]) {
- sortState[key] = 'og'; // Default to original (og)
- }
- // Determine the sort direction based on current state
- let direction = sortState[key];
- // Sort rows based on the column and direction
- groupedRows.sort((groupA, groupB) => {
- const cellA = groupA[0].cells[index].textContent.trim();
- const cellB = groupB[0].cells[index].textContent.trim();
- // If sorting by date, parse the date before comparing
- if (column.dataset.sort === 'date' || column.dataset.sort === 'end' || column.dataset.sort === 'due') {
- const datecellA = groupA[0].cells[index];
- const cellValueA = datecellA.getAttribute('value');
- const datecellB = groupB[0].cells[index];
- const cellValueB = datecellB.getAttribute('value');
- // Parse date values, handling null or invalid dates by assigning a minimum placeholder date
- const dateA = cellValueA ? new Date(cellValueA) : new Date(0); // Use Jan 1, 1970 for null/invalid
- const dateB = cellValueB ? new Date(cellValueB) : new Date(0);
- // Compare dates, treating nulls/invalids as the lowest values
- if (isNaN(dateA) && isNaN(dateB)) return 0; // Both invalid, treat as equal
- if (isNaN(dateA)) return direction === 'asc' ? 1 : -1; // A is invalid, so it should come first
- if (isNaN(dateB)) return direction === 'asc' ? -1 : 1; // B is invalid, so it should come first
- // Sort normally if both dates are valid
- return direction === 'asc' ? dateB - dateA : dateA - dateB ;
- }
- if (column.dataset.sort === 'priority') {
- const priorityMap = {
- urgent: 4,
- high: 3,
- normal: 2,
- low: 1
- };
- const priorityA = groupA[0].cells[index].getAttribute('value') || 'low'; // Default to 'low' if null
- const priorityB = groupB[0].cells[index].getAttribute('value') || 'low';
- const priorityValueA = priorityMap[priorityA.toLowerCase()] || 0; // Default to 0 if priority not in map
- const priorityValueB = priorityMap[priorityB.toLowerCase()] || 0;
- return direction === 'asc' ? priorityValueB - priorityValueA : priorityValueA - priorityValueB;
- }
- const a = column.dataset.sort === 'progress' ? parseInt(cellA) : cellA.toLowerCase();
- const b = column.dataset.sort === 'progress' ? parseInt(cellB) : cellB.toLowerCase();
- return direction === 'asc' ? (a > b ? -1 : 1) : (a < b ? -1 : 1);
- });
- if (column.dataset.sort === 'date' || column.dataset.sort === 'end' || column.dataset.sort === 'due') {
- groupedRows.forEach(group => {
- if (group.length > 1) {
- // Separate parent task from subtasks
- const parentTask = group[0];
- const subtasks = group.slice(1);
- // Sort subtasks by due date
- subtasks.sort((rowA, rowB) => {
- const datecellA = rowA.cells[index];
- const cellValueA = datecellA.getAttribute('value');
- const datecellB = rowB.cells[index];
- const cellValueB = datecellB.getAttribute('value');
- const dateA = cellValueA ? new Date(cellValueA) : new Date(0);
- const dateB = cellValueB ? new Date(cellValueB) : new Date(0);
- if (isNaN(dateA) && isNaN(dateB)) return 0;
- if (isNaN(dateA)) return direction === 'asc' ? 1 : -1;
- if (isNaN(dateB)) return direction === 'asc' ? -1 : 1;
- return direction === 'asc' ? dateB - dateA : dateA - dateB;
- });
- // Reconstruct the group with parent task first, then sorted subtasks
- group.length = 0;
- group.push(parentTask, ...subtasks);
- }
- });
- }
- // Rebuild the table with the sorted rows
- const tbody = table.querySelector('tbody');
- const addTaskRows = Array.from(table.querySelectorAll('tbody tr.add-task-rows'));
- tbody.innerHTML = ''; // Clear existing rows
- // Append sorted rows (main tasks and subtasks)
- groupedRows.forEach(group => {
- group.forEach(row => tbody.appendChild(row)); // Append all rows in the group (main task and subtasks)
- });
- // Always append 'add-task-row' last
- addTaskRows.forEach(row => tbody.appendChild(row)); // Add all 'add-task-row' rows at the bottom
- // Toggle the sorting order for the next time
- if (direction === 'og') {
- sortState[key] = 'asc'; // If it's original, next will be ascending
- } else if (direction === 'asc') {
- sortState[key] = 'desc'; // If it was ascending, next will be descending
- } else {
- sortState[key] = 'og'; // If it was descending, next will be original
- restoreOriginalOrder(table); // Restore original (custom) order
- }
- // Add the appropriate class and icon for the clicked column
- const icon = column.querySelector('.sort-icon');
- icon.classList.remove('fa-sort-up', 'fa-sort-down', 'fa-sort');
- if (sortState[key] === 'asc') {
- column.classList.add('sorted-asc');
- icon.classList.add('fa-sort-down');
- icon.title = "Ascending";
- } else if (sortState[key] === 'desc') {
- column.classList.add('sorted-desc');
- icon.classList.add('fa-sort-up');
- icon.title = "Descending";
- } else {
- column.classList.add('sorted-original');
- icon.classList.add('fa-sort');
- icon.title = "Custom Order";
- }
- }
- function updateProgress(taskRow, alltask=false) {
- let subtaskRow = taskRow.nextElementSibling; // Start checking from the next row
- let totalSubtasks = 0;
- let remainingSubtasks = 0;
- // Get the task ID from the task row to ensure uniqueness
- const taskId = taskRow.id;
- // Traverse until we hit the next task row
- while (subtaskRow && !subtaskRow.classList.contains('task-row') && !subtaskRow.classList.contains('task-row-checkpoint')) {
- if (subtaskRow.classList.contains('subtask-row') && !subtaskRow.classList.contains('mark-as-complete')) {
- totalSubtasks++;
- const checkbox = subtaskRow.querySelector('input[type="checkbox"]');
- if (!checkbox.checked) {
- remainingSubtasks++;
- }
- }
- subtaskRow = subtaskRow.nextElementSibling; // Move to the next row
- }
- // Get the progress element for the current task and update it
- const progressElement = taskRow.querySelector('.progress-text');
- if (progressElement) {
- progressElement.textContent = `${remainingSubtasks}`;
- }
- const completedSubtasks = totalSubtasks - remainingSubtasks;
- let completionPercentage = totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0;
- const statusBar = taskRow.querySelector('.status-bar-container');
- const statusLength = taskRow.querySelector('.status-bar-completed');
- const tooltip = taskRow.querySelector('.progress-tooltip');
- const noSubtasksMessage = taskRow.querySelector('.no-subtasks-message');
- if (totalSubtasks === 0) {
- if (statusBar) {
- statusBar.style.display = "none";
- }
- if (tooltip) {
- tooltip.textContent = '';
- }
- if (noSubtasksMessage) {
- noSubtasksMessage.style.display = 'block';
- }
- } else {
- if (noSubtasksMessage) {
- noSubtasksMessage.style.display = 'none'; // Hide "No Subtasks" message
- }
- if (statusBar) {
- statusBar.style.display = "block";
- statusLength.style.width = `${completionPercentage}%`;
- }
- if (tooltip) {
- tooltip.textContent = `${remainingSubtasks} out of ${totalSubtasks} tasks remaining`;
- }
- }
- if(remainingSubtasks == 0 && totalSubtasks > 0 && !alltask){
- document.getElementById(`${taskRow.getAttribute("data-task-id")}`).setAttribute('hidden', 'true');
- document.getElementById(`${taskRow.getAttribute("data-task-id")}`).setAttribute('value', 'C');
- }
- return completionPercentage;
- }
- function updateAllStatusBars(completionPercentages) {
- const statusBars = document.querySelectorAll('.status-bar-completed');
- statusBars.forEach((bar, index) => {
- const percentage = completionPercentages[index] || 0;
- bar.style.width = percentage + '%';
- });
- }
- document.addEventListener('change', function(event) {
- if (event.target.matches('.subtask-row input[type="checkbox"]')) {
- let thisRow = event.target.closest('tr');
- let taskRow = thisRow.previousElementSibling;
- while (taskRow && !taskRow.classList.contains('task-row')) {
- taskRow = taskRow.previousElementSibling;
- }
- updateProgress(taskRow);
- }
- });
- document.querySelectorAll('.add-task-rows').forEach(subtaskRow => {
- subtaskRow.addEventListener('click', function(event) {
- if (rowIsOpen == false){
- const button = subtaskRow.querySelector('.add-task-btn');
- addTaskRow(button);
- rowIsOpen = true;
- }
- });
- });
- const dropdown = document.getElementById('taskDropdown');
- const tableRows = document.querySelectorAll('#task-table-body tr');
- // Update the dropdown change handler
- dropdown.addEventListener('change', function() {
- const filterValue = this.value;
- filtered = filterValue;
- // Get all tasks and subtasks
- const allTasks = document.querySelectorAll('.task-row, .subtask-row, .task-row-checkpoint');
- allTasks.forEach(row => {
- // Skip special rows
- if (row.classList.contains('add-task-rows') ||
- row.classList.contains('add-task-row') ||
- row.classList.contains('mark-complete')) {
- return;
- }
- const taskStatus = row.getAttribute('value');
- const isSubtask = row.classList.contains('subtask-row');
- const parentTaskId = row.getAttribute('data-task-id');
- if (filterValue === 'all') {
- // Remove hidden attribute regardless of status
- row.removeAttribute('hidden');
- // For parent tasks, handle subtask visibility
- if (!isSubtask) {
- const subtasks = document.querySelectorAll(`.subtask-row[data-task-id="${row.id}"]`);
- subtasks.forEach(subtask => {
- if (row.dataset.open === "true") {
- subtask.removeAttribute('hidden');
- } else {
- subtask.style.display = 'none';
- }
- });
- }
- } else if (filterValue === 'pending') {
- if (taskStatus === 'C') {
- row.setAttribute('hidden', 'true');
- } else {
- row.removeAttribute('hidden');
- // For parent tasks, ensure subtasks follow the same rule
- if (!isSubtask) {
- const subtasks = document.querySelectorAll(`.subtask-row[data-task-id="${row.id}"]`);
- subtasks.forEach(subtask => {
- if (subtask.getAttribute('value') === 'C') {
- subtask.setAttribute('hidden', 'true');
- } else if (row.dataset.open === "true") {
- subtask.removeAttribute('hidden');
- }
- });
- }
- }
- }
- });
- // Update progress for all parent tasks
- document.querySelectorAll('.task-row').forEach(row => {
- if (!row.classList.contains('subtask-row')) {
- updateProgress(row, true);
- }
- });
- });
- function toggleSubtask(svgElement) {
- const taskRow = svgElement.closest('tr');
- const milestoneSection = svgElement.closest('table');
- const milestoneHeader = milestoneSection.querySelector('th');
- const milestoneName = milestoneHeader.textContent.trim();
- const arrowSvg = taskRow.querySelector('.arrow-svg');
- const taskName = taskRow.querySelector('.task-name').textContent.trim();
- const taskId = taskRow.id;
- // generate a unique class name
- const taskIdentifier = `subtask-${taskName.replace(/\s+/g, '-').toLowerCase()}-milestoneName`;
- // add unique classname
- let nextRow = taskRow.nextElementSibling;
- while (nextRow && nextRow.classList.contains('subtask-row')) {
- nextRow.classList.add(`subtask-${taskId}`);
- nextRow = nextRow.nextElementSibling;
- }
- // select all subtasks
- const subtasks = document.querySelectorAll(`.subtask-${taskId}`);
- // toggle the display of the subtask rows
- subtasks.forEach(row => {
- if (row.style.display === "none"){
- if (arrowSvg) {
- arrowSvg.classList.add('rotated');
- }
- taskRow.dataset.open = "true";
- }
- else{
- if (arrowSvg) {
- arrowSvg.classList.remove('rotated');
- }
- taskRow.dataset.open = "false";
- }
- row.style.display = (row.style.display === 'none' || row.style.display === '') ? 'table-row' : 'none';
- });
- }
- function toggleTaskCompletion(taskId, button) {
- const taskRow = button.closest('.task-row');
- const isCompleted = taskRow.getAttribute('value') =="C";
- const newStatus = isCompleted ? 'I' : 'C';
- taskRow.setAttribute('value',newStatus);
- updateTaskStatus(taskId, newStatus);
- const parentRow = taskRow.parentElement;
- const subtasks = parentRow.querySelectorAll('.subtask-row');
- if ( newStatus == 'C'){
- subtasks.forEach(subtaskRow => {
- if ( !subtaskRow.getAttribute('hidden') == true){
- if (subtaskRow.classList.contains('subtask-row') && !subtaskRow.classList.contains('add-subtask-row')) {
- const subtaskId = subtaskRow.getAttribute('data-subtask-id');
- const checkbox = subtaskRow.querySelector('input[type="checkbox"]');
- checkbox.checked = checkbox.checked ? false : true;
- if (subtaskId && subtaskId != "") {
- updateTaskStatus(subtaskId, newStatus);
- }
- }
- }
- }
- );}
- if (isCompleted) {
- button.innerText = "Mark as Complete";
- } else {
- // If the task was incomplete, show the main task row
- taskRow.setAttribute('hidden', true);
- let nextRow = taskRow.nextElementSibling;
- while (nextRow) {
- if (nextRow.classList.contains('task-row') || nextRow.classList.contains('add-task-row') || nextRow.classList.contains('task-row-checkpoint')) {
- break; // Stop hiding/showing when we hit the next task row or add task row
- }
- nextRow.hidden = true; // Toggle the visibility of the following rows
- nextRow = nextRow.nextElementSibling; // Move to the next row
- }
- button.innerText = "Mark as Incomplete"; // Change button text to "Mark as Incomplete"
- }
- // Update the onclick handler
- button.setAttribute('onclick', `toggleTaskCompletion(${taskId}, this)`); // Change the click handler to the same function
- }
- // Function to update task status
- function updateTaskStatus(taskId, checkbox) {
- const isChecked = checkbox.checked;
- const newStatus = isChecked ? 'C' : 'I';
- const taskRow = checkbox.closest('tr');
- // Store references to related elements
- const parentTaskId = taskRow.getAttribute('data-task-id');
- const isSubtask = taskRow.classList.contains('subtask-row');
- let parentRow = null;
- if (isSubtask) {
- parentRow = document.getElementById(parentTaskId);
- }
- fetch(`/project/update-subtask-status/${taskId}/`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ status: newStatus })
- })
- .then(response => {
- if (!response.ok) {
- throw new Error('Failed to update task status.');
- }
- return response.json();
- })
- .then(data => {
- // Update task's status attributes
- taskRow.setAttribute('data-status', newStatus);
- taskRow.setAttribute('value', newStatus);
- if (filtered === "pending") {
- if (isChecked) {
- taskRow.setAttribute('hidden', 'true');
- // If it's a subtask, handle parent task visibility
- if (isSubtask && parentRow) {
- let allSubtasksComplete = true;
- const siblingSubtasks = document.querySelectorAll(`.subtask-row[data-task-id="${parentTaskId}"]`);
- siblingSubtasks.forEach(subtask => {
- if (subtask.getAttribute('value') !== 'C') {
- allSubtasksComplete = false;
- }
- });
- if (allSubtasksComplete) {
- parentRow.setAttribute('hidden', 'true');
- }
- }
- }
- } else if (filtered === "all") {
- // In "all" view, never hide the task
- taskRow.removeAttribute('hidden');
- // If it's a parent task, ensure subtasks visibility is maintained
- if (!isSubtask) {
- const subtasks = document.querySelectorAll(`.subtask-row[data-task-id="${taskId}"]`);
- subtasks.forEach(subtask => {
- if (taskRow.dataset.open === "true") {
- subtask.removeAttribute('hidden');
- }
- });
- }
- }
- // Update progress indicators
- const progressBar = taskRow.querySelector('.status-bar-completed');
- if (progressBar) {
- progressBar.style.width = isChecked ? '100%' : '0%';
- }
- const tooltip = taskRow.querySelector('.progress-tooltip');
- if (tooltip) {
- tooltip.textContent = isChecked ? 'Task completed' : 'Task pending';
- }
- // Update parent task progress if this is a subtask
- if (isSubtask && parentRow) {
- updateProgress(parentRow);
- }
- })
- .catch(error => {
- console.error('Error updating task status:', error);
- checkbox.checked = !isChecked;
- alert('Failed to update task status. Please try again.');
- });
- }
- function updateSubtaskStatus(subtaskId, checkBox) {
- if (!checkBox) return;
- checkBox.checked = !checkBox.checked; // Revert the automatic change
- const newStatus = !checkBox.checked ? 'C' : 'I';
- const parentRow = checkBox.parentElement.parentElement.parentElement.parentElement;
- fetch(`/project/update-subtask-status/${subtaskId}/`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ status: newStatus })
- })
- .then(response => {
- if (!response.ok) {
- throw new Error('Failed to update subtask status.');
- }
- checkBox.checked = !checkBox.checked;
- if (newStatus == "C" && filtered=="pending"){
- parentRow.setAttribute('hidden', true);
- }
- parentRow.setAttribute('data-status', newStatus);
- return response.json();
- })
- .then(data => {
- console.log('Subtask status updated successfully:', data);
- })
- .catch(error => {
- console.error('Error updating subtask status:', error);
- });
- }
- function formatDueDate(dateValue){
- const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
- const day = String(dateValue.getDate()).padStart(2, '0'); // Ensures day is 2 digits, e.g., 01, 02
- const month = monthNames[dateValue.getMonth()]; // Get the month abbreviation
- const year = dateValue.getFullYear(); // Get the full year
- // Combine to form the formatted date
- const formattedDate = `${day} ${month} ${year}`;
- return formattedDate;
- }
- function getDateLabelAndColor(dueDate) {
- const today = new Date();
- const yesterday = new Date(today);
- yesterday.setDate(today.getDate() - 1);
- const tomorrow = new Date(today);
- tomorrow.setDate(today.getDate() + 1);
- const dueDateObj = new Date(dueDate);
- // Check if the due date is yesterday, today, or tomorrow
- if (dueDateObj.toDateString() === yesterday.toDateString()) {
- return { label: "Yesterday", color: "red" };
- } else if (dueDateObj.toDateString() === today.toDateString()) {
- return { label: "Today", color: "orange" };
- } else if (dueDateObj.toDateString() === tomorrow.toDateString()) {
- return { label: "Tomorrow", color: "blue" };
- } else if (dueDateObj < today) {
- return { label: formatDueDate(dueDateObj), color: "red" }; // Overdue
- } else {
- return { label: formatDueDate(dueDateObj), color: "black" }; // Future date
- }
- }
- let draggedTask = null;
- let draggedSubtasks = [];
- let isSubtaskDragged = false;
- // Function to capture the dragged task and subtasks (if any)
- function drag(event) {
- resetDragState();
- // console.log("hi");
- draggedTask = event.target;
- draggedSubtasks = getSubtasks(draggedTask);
- isSubtaskDragged = draggedTask.classList.contains('subtask-row');
- draggedTask.classList.add('dragging');
- draggedSubtasks.forEach(subtask => subtask.classList.add('dragging'));
- // console.log(draggedTask)
- }
- // Highlight only the current target task
- function allowDrop(event) {
- event.preventDefault();
- const targetRow = event.target.closest('.task-row, .subtask-row, .task-row-checkpoint');
- removeHighlightFromRows();
- if (targetRow && targetRow !== draggedTask) {
- targetRow.classList.add('highlight');
- }
- }
- // Handle drop event to reassign subtask if necessary
- function drop(event) {
- event.preventDefault();
- const targetRow = event.target.closest('.task-row, .subtask-row, .task-row-checkpoint');
- if (targetRow && targetRow !== draggedTask) {
- if (isSubtaskDragged && targetRow.classList.contains('task-row') && !targetRow.classList.contains('subtask-row') && targetRow.id !== draggedTask.dataset.taskId) {
- oldtaskid = draggedTask.dataset.taskId;
- moveSubtaskToNewParent(draggedTask, targetRow);
- try{
- const toggleSvg = targetRow.querySelector('#togglesvg');
- const toggleSpan = targetRow.querySelector('#togglespan');
- if (toggleSvg){
- if (toggleSvg.style.display == "none"){
- toggleSvg.classList.add('rotated');
- }
- toggleSvg.style.display = "flex";
- toggleSpan.style.display = "none";
- }
- const subtaskCountSpan = targetRow.querySelector('.subtask-count');
- let currentCount = parseInt(subtaskCountSpan.textContent.replace(/\D/g, '')) || 0;
- currentCount++;
- subtaskCountSpan.textContent = `(${currentCount})`;
- const parentrow = document.getElementById(oldtaskid);
- const subtaskspan = parentrow.querySelector('.subtask-count');
- let currentCount2 = parseInt(subtaskspan.textContent.replace(/\D/g, '')) || 0;
- currentCount2--;
- subtaskspan.textContent = `(${currentCount2})`;
- }
- catch (error) {
- console.log(error);
- }
- }
- else if (targetRow.classList.contains('subtask-row')){
- taskId = targetRow.dataset.taskId;
- row = document.getElementById(`${taskId}`);
- if (row.id !== draggedTask.dataset.taskId){
- moveSubtaskToNewParent(draggedTask,row)
- }
- else{
- moveTaskWithSubtasks(draggedTask, targetRow);
- updateOrderAfterDrop(draggedTask, targetRow);
- }
- }
- else {
- moveTaskWithSubtasks(draggedTask, targetRow);
- updateOrderAfterDrop(draggedTask, targetRow);
- }
- }
- resetDragState();
- }
- // Move the subtask under a new parent task in the UI and update parent ID
- function moveSubtaskToNewParent(subtask, newParentTask) {
- const allRows = Array.from(document.querySelectorAll('.task-row, .subtask-row'));
- const newParentIndex = allRows.indexOf(newParentTask);
- // Place the subtask immediately below the new parent task
- newParentTask.after(subtask);
- subtask.dataset.taskId = newParentTask.id;
- // Update backend with the new parent ID
- updateSubtaskParentInBackend(subtask.id, newParentTask.id);
- }
- // AJAX request to update subtask's parent ID in the backend
- async function updateSubtaskParentInBackend(subtaskId, newParentId) {
- try {
- const response = await fetch('/project/update-subtask-parent/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- subtask_id: subtaskId,
- new_parent_id: newParentId
- })
- });
- if (!response.ok) throw new Error('Failed to update subtask parent');
- } catch (error) {
- console.error(error);
- }
- }
- // Move the main task and its subtasks as a group
- function moveTaskWithSubtasks(draggedTask, targetRow) {
- const allRows = Array.from(document.querySelectorAll('.task-row, .subtask-row, .task-row-checkpoint'));
- const targetIndex = allRows.indexOf(targetRow);
- const draggedIndex = allRows.indexOf(draggedTask);
- if (draggedIndex > targetIndex) {
- targetRow.before(draggedTask);
- draggedSubtasks.forEach(subtask => targetRow.before(subtask));
- } else {
- targetRow.after(draggedTask);
- draggedSubtasks.forEach(subtask => draggedTask.after(subtask));
- }
- }
- // Get all subtasks for a given main task
- function getSubtasks(task) {
- const taskId = task.id;
- return Array.from(document.querySelectorAll(`.subtask-row[data-task-id="${taskId}"]`));
- }
- // Remove all highlight effects
- function removeHighlightFromRows() {
- document.querySelectorAll('.highlight').forEach(row => row.classList.remove('highlight'));
- }
- // Reset the drag state
- function resetDragState() {
- if (draggedTask) {
- draggedTask.classList.remove('dragging');
- draggedSubtasks.forEach(subtask => subtask.classList.remove('dragging'));
- removeHighlightFromRows();
- }
- draggedTask = null;
- draggedSubtasks = [];
- isSubtaskDragged = false;
- }
- // Update the task order on the backend after drop
- async function updateOrderAfterDrop(draggedTask, targetRow) {
- const draggedTaskId = draggedTask.id;
- const targetTaskId = targetRow.id;
- try {
- const response = await fetch('/project/update-task-order/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- dragged_task_id: draggedTaskId,
- target_task_id: targetTaskId
- })
- });
- if (!response.ok) throw new Error('Failed to update task order');
- } catch (error) {
- console.error(error);
- }
- }
- async function saveTaskToBackend(task) {
- try {
- const response = await fetch('/project/add_task/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(task)
- });
- if (!response.ok) {
- throw new Error('Failed to save task');
- }
- const result = await response.json();
- return result.task_id;
- } catch (error) {
- console.error('Error saving task:', error);
- alert('Failed to save task. Please try again.');
- return null;
- }
- }
- //function to add subtask
- async function addSubtask(buttonElement,data,handleKeydown,row) {
- const milestoneSection = buttonElement.closest('table');
- const milestoneHeader = milestoneSection.querySelector('thead tr th:first-child');
- const milestoneText = milestoneHeader ? milestoneHeader.textContent.trim() : null;
- const milestoneSectionInput = buttonElement.closest('tr');
- const inputRow = buttonElement.closest('tr')
- const taskName = buttonElement.dataset.subtask;
- // we are selecting the inputs from the correct row
- const subtaskInput = data.name;
- const assigneeInput = data.member.id;
- const dateInput = data.date;
- const bountieInput = data.bounty;
- const selectedAssignee = data.member.name;
- const priorityText = data.priority;
- let taskRow = inputRow.previousElementSibling;
- taskRow = inputRow;
- let { label, color } = { label: `<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_iconCarrier">
- <path d="M19 2H5C3.9 2 3 2.9 3 4V20C3 21.1 3.9 22 5 22H19C20.1 22 21 21.1 21 20V4C21 2.9 20.1 2 19 2ZM19 20H5V8H19V20ZM5 4H7V6H5V4ZM19 4H17V6H19V4Z" fill="#ababab"/>
- </g>
- </svg>`, color: "black" }; // Initialize with default values
- if (dateInput) {
- const formatted = `${dateInput.getFullYear()}-${String(dateInput.getMonth() + 1).padStart(2, '0')}-${String(dateInput.getDate()).padStart(2, '0')}`;
- ({ label, color } = getDateLabelAndColor(formatted));
- }
- const toggleSvg = taskRow.querySelector('#togglesvg');
- const toggleSpan = taskRow.querySelector('#togglespan');
- if (toggleSvg){
- if (toggleSvg.style.display == "none"){
- toggleSvg.classList.add('rotated');
- }
- toggleSvg.style.display = "flex";
- toggleSpan.style.display = "none";
- }
- // validate the task name input
- if (!subtaskInput.trim()) {
- alert("Please enter a valid task name.");
- return;
- }
- let formattedDate = null;
- if (dateInput){
- duedate = dateInput;
- formattedDate = `${duedate.getFullYear()}-${String(duedate.getMonth() + 1).padStart(2, '0')}-${String(duedate.getDate()).padStart(2, '0')}`;
- }
- else{
- formattedDate = null;
- duedate = null;
- }
- if (subtaskInput.trim() === "") {
- alert("Please enter a subtask.");
- return;
- }
- if (dateInput.value){
- duedate = dateInput.value;
- }
- else{
- duedate = null;
- }
- let priorityIcon = "";
- if (priorityText){
- priorityIcon = priorityMapping[priorityText.toLowerCase()].icon;
- }
- else{
- priorityIcon = priorityMapping['normal'].icon;
- }
- //BACKEND Call
- const taskData = {
- name: subtaskInput,
- assignee: assigneeInput,
- dueDate: duedate,
- bounty: bountieInput,
- parentId : data.parent,
- project_ID: projectId,
- milestone: milestoneText
- };
- const taskId = await saveTaskToBackend(taskData);
- // create a new tr for the subtask
- const newRow = document.createElement('tr');
- newRow.classList.add("subtask-row");
- newRow.style.display="table-row"
- newRow.style.textAlign = 'left';
- newRow.style.height = '40px';
- newRow.id = taskId;
- newRow.setAttribute('draggable', 'true');
- newRow.setAttribute('ondragstart', 'drag(event)');
- newRow.setAttribute('ondragover', 'allowDrop(event)');
- newRow.setAttribute('ondrop', 'drop(event)');
- newRow.setAttribute('data-text-id',data.parent);
- // ineerhtml
- newRow.innerHTML = `
- <td class="sub-task-elem" style="padding: 16px 32px; display:flex; flex-direction:row; justify-content: space-between; align-items: center;">
- <div style="display: flex; flex-direction: row; align-items: center; gap: 4px;">
- <label class="circular-checkbox"> <input class="taskCheckbox" onchange="updateSubtaskStatus(${taskId}, this)"
- type="checkbox" name=""> </label>
- <span onclick="openTaskModal(${taskId})" class="task-name" id="task-name-${taskId}"> ${subtaskInput}</span> </div>
- <div style="display: flex; gap: 10px;">
- <!-- Edit Icon to Rename Task -->
- <button title="Rename Task" onclick="editTask(${taskId})" class="edit-btn">
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M7.37333 4.01333L7.98667 4.62667L1.94667 10.6667H1.33333V10.0533L7.37333 4.01333ZM9.77333 0C9.60667 0 9.43333 0.0666666 9.30667 0.193333L8.08667 1.41333L10.5867 3.91333L11.8067 2.69333C12.0667 2.43333 12.0667 2.01333 11.8067 1.75333L10.2467 0.193333C10.1133 0.06 9.94667 0 9.77333 0ZM7.37333 2.12667L0 9.5V12H2.5L9.87333 4.62667L7.37333 2.12667Z" fill="#939CA3"/>
- </svg>
- </button>
- <button title="Delete Task" onclick="deleteTask(this,${taskId})" class="edit-btn">
- <svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M3 3L13 13M3 13L13 3" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </button>
- </div>
- </td>
- <td style="border-left: 1px solid #CED4DA; padding-left: 10px;"></td>
- <td class="assignee-input" style="border-left: 1px solid #CED4DA; padding-left: 10px; display: flex; justify-content: center; align-items: center;">
- <button title="Assign Assignee" id="${taskId}" class="assign-btn" onclick="openAssigneeDropdown(${taskId},this)" style="border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- ${selectedAssignee ? `
- <div class="initials-circle" style="width: 25px; height: 25px; border-radius: 50%; display: flex; justify-content: center; align-items: center; background-color: #7c4dff; color: #fff; font-size: 14px; cursor: pointer;">
- ${getInitials(selectedAssignee)}
- </div>` : `
- <svg class="assignee-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" style="width: 24px; height: 24px; fill: #ABABAB;">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <path fill-rule="evenodd" clip-rule="evenodd" d="M3 18C3 15.3945 4.66081 13.1768 6.98156 12.348C7.61232 12.1227 8.29183 12 9 12C9.70817 12 10.3877 12.1227 11.0184 12.348C11.3611 12.4703 11.6893 12.623 12 12.8027C12.3107 12.623 12.6389 12.4703 12.9816 12.348C13.6123 12.1227 14.2918 12 15 12C15.7082 12 16.3877 12.1227 17.0184 12.348C19.3392 13.1768 21 15.3945 21 18V21H15.75V19.5H19.5V18C19.5 15.5147 17.4853 13.5 15 13.5C14.4029 13.5 13.833 13.6163 13.3116 13.8275C14.3568 14.9073 15 16.3785 15 18V21H3V18ZM9 11.25C8.31104 11.25 7.66548 11.0642 7.11068 10.74C5.9977 10.0896 5.25 8.88211 5.25 7.5C5.25 5.42893 6.92893 3.75 9 3.75C10.2267 3.75 11.3158 4.33901 12 5.24963C12.6842 4.33901 13.7733 3.75 15 3.75C17.0711 3.75 18.75 5.42893 18.75 7.5C18.75 8.88211 18.0023 10.0896 16.8893 10.74C16.3345 11.0642 15.689 11.25 15 11.25C14.311 11.25 13.6655 11.0642 13.1107 10.74C12.6776 10.4869 12.2999 10.1495 12 9.75036C11.7001 10.1496 11.3224 10.4869 10.8893 10.74C10.3345 11.0642 9.68896 11.25 9 11.25ZM13.5 18V19.5H4.5V18C4.5 15.5147 6.51472 13.5 9 13.5C11.4853 13.5 13.5 15.5147 13.5 18ZM11.25 7.5C11.25 8.74264 10.2426 9.75 9 9.75C7.75736 9.75 6.75 8.74264 6.75 7.5C6.75 6.25736 7.75736 5.25 9 5.25C10.2426 5.25 11.25 6.25736 11.25 7.5ZM15 5.25C13.7574 5.25 12.75 6.25736 12.75 7.5C12.75 8.74264 13.7574 9.75 15 9.75C16.2426 9.75 17.25 8.74264 17.25 7.5C17.25 6.25736 16.2426 5.25 15 5.25Z" />
- </g>
- </svg>`
- }
- </button>
- </td>
- <td class="date-input" value="${duedate}" style=" justify-items:center; align-items: center; border-left: 1px solid #CED4DA; padding-left: 10px; color: ${color};">
- <button title="Change Date" class="assign-btn"
- {% if is_user_spm or request.user == project_detail.manager or hat_type == 'projectManager' %}
- onclick="openDueDateDropdown(${taskId},this)"
- {% endif %}
- style="border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- ${label}
- </button>
- </td>
- <td class="priority-input" value="${priorityText}" title="${priorityText}" style="border-left: 1px solid #CED4DA; padding-left: 10px; text-align: center; vertical-align: middle;justify-content: center; display:flex;">
- <button title="Change Priority" class="assign-btn"
- onclick="openPriorityDropdown(${taskId},this)"
- style="border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- ${priorityIcon || '-' }
- </button>
- </td>
- <td class="bountie-input" style="padding-left: 10px; justify-content: center; justify-items:center;">
- <button title="Change Bounties" id="${taskId}" value="${bountieInput || '1'}" class="assign-btn bounty-btn"
- {% if is_user_spm or request.user == project_detail.manager or hat_type == 'projectManager' %}
- onclick="openBountyDropdown(${taskId},this)"
- {% endif %} style="justify-content: center;width:35px;border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- <div>${bountieInput || '-'}</div>
- </button>
- </td>
- `;
- // insert
- row.parentNode.insertBefore(newRow,row);
- row.remove();
- const subtaskCountSpan = taskRow.querySelector('.subtask-count');
- let currentCount = parseInt(subtaskCountSpan.textContent.replace(/\D/g, '')) || 0;
- currentCount++;
- subtaskCountSpan.textContent = `(${currentCount})`;
- document.removeEventListener('keydown', handleKeydown);
- updateProgress(taskRow);
- enforceTableCellHeight(35);
- }
- function updateParentTaskAfterDeletion(parentTaskId) {
- // Number(elem.querySelector('.subtask-count').innerText.match(/\((\d+)\)/)[1])
- const parentTask = document.getElementById(parentTaskId);
- if (parentTask) {
- const remainingSubtasks = parentTask.querySelector('.subtask-count');
- let remainingSubtasksCount = Number(remainingSubtasks.innerText.match(/\((\d+)\)/)[1]) - 1;
- remainingSubtasks.innerText=`(${remainingSubtasksCount})`
- const toggleSvg = parentTask.querySelector('#togglesvg');
- const checkbox = parentTask.querySelector('.circular-checkbox');
- if (remainingSubtasksCount === 0) {
- if (toggleSvg) {
- toggleSvg.style.display = 'none';
- }
- if (checkbox) {
- checkbox.style.display = 'block';
- }
- }
- }
- }
- function addSubtaskRow(button,taskId) {
- // Find the row containing the button
- const addTaskRow = button.closest('tr');
- const isOpen = addTaskRow.dataset.open === 'true';
- const parentTask = button.closest('tr');
- const toggleSvg = parentTask.querySelector('#togglesvg');
- const checkbox = parentTask.querySelector('.circular-checkbox');
- if (toggleSvg) {
- toggleSvg.style.display = 'flex';
- if (checkbox) {
- checkbox.style.display = 'none'; // Hide the checkbox when subtasks are added
- }
- }
- let task_Row = button.closest('tr');
- let arrow_Svg = task_Row.querySelector('.arrow-svg');
- if (!isOpen && !arrow_Svg.classList.contains('rotated')){
- addTaskRow.dataset.open = 'true';
- arrow_Svg.classList.add('rotated');
- toggleSubtask(button);
- }
- // Create a new row element
- const newRow = document.createElement('tr');
- newRow.classList.add ("task-row","add-task-row");
- newRow.style.textAlign = "left";
- newRow.style.height = "35px";
- rowIsOpen = true;
- // Create a cell that spans multiple columns
- const newCell = document.createElement('td');
- newCell.style.padding = "6px 0px 6px 60px";
- newCell.style.width = "50%";
- newCell.style.borderLeft = "1px solid #CED4DA";
- newCell.style.borderRight = "none";
- newCell.colSpan = "1";
- newCell.className = 'add-task-td';
- const newCell2 = document.createElement('td');
- // newCell2.style.width = "10%";
- newCell2.style.padding = "10px";
- // newCell2.style.display = "flex";
- // newCell2.style.flexDirection = "row";
- // newCell2.style.gap = "5px"; // Space between icons
- newCell2.colSpan = "3";
- newCell2.border = "none";
- newCell2.borderRight = "none";
- newCell2.borderLeft = "none";
- const newCell3 = document.createElement('td');
- newCell3.style.borderLeft = "none";
- newCell3.style.width = "10%";
- newCell3.colSpan = "2";
- newCell3.style.gap = "10px"; // Space between icons
- newCell3.borderRight = 'none';
- const taskInput = document.createElement('input');
- taskInput.type = "text";
- taskInput.classList.add("task-input-name","task-input");
- taskInput.placeholder = "Enter Task Name";
- taskInput.style.width = "100%";
- const iconBtnWrapper = document.createElement("div");
- iconBtnWrapper.classList.add('icon-btn-wrapper')
- // Helper function to create icon buttons with tooltip and onclick event
- function createIconButton(iconHTML, tooltipText, onClickHandler) {
- const button = document.createElement('button');
- button.className = "icon-button";
- button.style.height = "30px";
- button.style.width = "30px";
- button.style.display = "flex";
- button.style.alignItems = "center";
- button.style.justifyContent = "center";
- button.style.padding = "2px";
- button.innerHTML = iconHTML;
- button.title = tooltipText; // Tooltip
- button.onclick = onClickHandler; // Assign onclick function
- button.style.border = "2px solid #ababab";
- return button;
- }
- let selectedPriority = "";
- let selectedDate = "";
- let selectedOption = "";
- let selectedMember = "";
- let selectedMemberData = {};
- let currentValue = "";
- // Your exact icons with tooltips and onclick handlers
- const priorityIcon = createIconButton(
- `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
- <g id="SVGRepo_iconCarrier">
- <path
- id="priority-icon-path-add"
- d="M12 4L20 20H4L12 4Z"
- fill = "none"
- stroke="#ababab"
- stroke-width="1.5"
- stroke-linecap="round"
- stroke-linejoin="round"
- />
- <path
- d="M12 8L12 12"
- id = "priority-icon-exclamation-add"
- stroke="#ababab"
- stroke-width="1.5"
- stroke-linecap="round"
- stroke-linejoin="round"
- />
- <path
- id = "priority-iconborder-add"
- d="M12 16.01L12.01 15.9989"
- stroke="#ababab"
- stroke-width="1.5"
- stroke-linecap="round"
- stroke-linejoin="round"
- />
- </g>
- </svg>
- `,
- "Assign Priority",
- () => {
- // Check if the dropdown is already visible and remove it
- const existingDropdown = document.querySelector('.custom-dropdown');
- if (existingDropdown) {
- existingDropdown.remove();
- return; // Don't create a new dropdown if one is already visible
- }
- // Create the custom dropdown for priority options
- let dropdown = document.createElement('div');
- dropdown.className = 'custom-dropdown';
- dropdown.style.position = 'absolute';
- dropdown.style.background = 'white';
- dropdown.style.border = '1px solid #ddd';
- dropdown.style.padding = '5px';
- dropdown.style.zIndex = '1000';
- dropdown.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.2)';
- dropdown.style.maxHeight = '200px';
- dropdown.style.overflowY = 'auto';
- dropdown.style.minWidth = '200px';
- // Define the priority options
- const priorities = {
- urgent: "Urgent",
- high: "High",
- normal: "Normal",
- low: "Low"
- };
- // Create the dropdown options
- Object.keys(priorities).forEach(priority => {
- const option = document.createElement('div');
- option.textContent = priorities[priority];
- option.style.padding = '5px 10px';
- option.style.cursor = 'pointer';
- option.style.backgroundColor = 'white';
- // Hover effect for the dropdown items
- option.addEventListener('mouseover', () => {
- option.style.backgroundColor = '#f1f1f1';
- });
- option.addEventListener('mouseout', () => {
- option.style.backgroundColor = 'white';
- });
- // When an option is selected, update the flag color
- option.addEventListener('click', () => {
- selectedPriority = priority;
- const priorityIconPath = document.getElementById('priority-icon-path-add');
- const priorityIconExc = document.getElementById('priority-iconborder-add');
- const priorityOutline = document.getElementById('priority-icon-exclamation-add');
- // Define color changes for different priorities
- const colors = {
- urgent: "red",
- high: "gold",
- normal: "transparent",
- low: "grey"
- };
- // Change the color of the flag icon based on selected priority
- if (selectedPriority !== "none") {
- priorityIconPath.setAttribute('fill', colors[selectedPriority]);
- priorityIconPath.setAttribute('stroke',colors[selectedPriority]);
- if (colors[selectedPriority] == "red" )
- {
- priorityIconExc.setAttribute('stroke', 'white');
- }
- else if (colors[selectedPriority] == "transparent"){
- priorityOutline.setAttribute('stroke', 'grey');
- priorityIconPath.setAttribute('stroke', 'grey');
- }
- else{
- priorityIconExc.setAttribute('stroke', 'black');
- priorityOutline.setAttribute('stroke', 'black');
- }
- }
- // Remove the dropdown after selection
- dropdown.remove();
- });
- dropdown.appendChild(option);
- });
- // Create a "Clear" button as part of the dropdown
- const clearButton = document.createElement('button');
- clearButton.textContent = 'Clear';
- clearButton.style.padding = '5px 10px';
- clearButton.style.marginTop = '10px';
- clearButton.style.cursor = 'pointer';
- clearButton.style.border = 'none';
- clearButton.style.background = "lightgrey";
- // When the "Clear" button is clicked, reset to original state
- clearButton.addEventListener('click', () => {
- const priorityIconPath = document.getElementById('priority-icon-path');
- priorityIconPath.setAttribute('fill', 'none');
- priorityIconPath.setAttribute('stroke', '#ababab');
- // Remove the dropdown after clicking "Clear"
- dropdown.remove();
- });
- dropdown.appendChild(clearButton);
- // Position the dropdown below the icon
- const rect = priorityIcon.getBoundingClientRect();
- const zoomFactor = 1; // Optional zoom adjustment
- dropdown.style.left = `${rect.left / zoomFactor}px`; // Adjust for zoom level
- dropdown.style.top = `${(rect.bottom + window.scrollY) / zoomFactor}px`; // Adjust for zoom level and page scroll
- document.body.appendChild(dropdown);
- const handleClickOutside = (event) => {
- if (!priorityIcon.contains(event.target)) {
- dropdown.remove();
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- }
- );
- const taskIcon = createIconButton(
- `<svg width="30px" height="30px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#000000">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier"> <path color="#000000" d="M7.992 0A2.008 2.008 0 0 0 6 2H5c-.657 0-1.178.06-1.617.225-.439.164-.79.461-.998.838-.415.752-.37 1.673-.385 2.931v5.012c.015 1.258-.03 2.179.385 2.932.208.376.56.673.998.838.439.164.96.224 1.617.224h3.133c.021-.05.031-.102.059-.15l.482-.85H5c-.592 0-1.006-.063-1.265-.16-.26-.098-.372-.203-.473-.387C3.06 13.087 3.015 12.259 3 11V6c.015-1.259.06-2.087.262-2.453.101-.184.213-.29.473-.387C3.995 3.062 4.408 3 5 3v.999h6V3c.593 0 1.006.063 1.266.16.26.098.371.203.472.387.202.366.247 1.194.262 2.453v2.832c.31.115.582.323.752.62v.001l.248.438V5.994c-.015-1.258.031-2.179-.385-2.932a1.88 1.88 0 0 0-.998-.837C12.179 2.06 11.657 2 11 2H9.996a2.008 2.008 0 0 0-1.992-2zm.01 1c.559 0 1 .442 1 1a.99.99 0 0 1-1 1 .982.982 0 0 1-.922-.61A1.01 1.01 0 0 1 7.003 2c0-.558.441-1 1-1zM5 6v1h6.012V6zm0 2v1h6.012V8zm-.01 2v1h3v-1z" fill="gray" font-family="sans-serif" font-weight="400" overflow="visible" style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;isolation:auto;mix-blend-mode:normal;marker:none" white-space="normal"/> <path class="warning" color="#000000" d="M12.48 9.729a.443.443 0 0 0-.36.22l-3.061 5.397a.437.437 0 0 0 .379.654h6.125a.437.437 0 0 0 .379-.654l-3.059-5.397a.442.442 0 0 0-.402-.22zM12 11h1v.168c0 .348-.016.667-.047.957-.03.29-.069.581-.115.875h-.666a12.898 12.898 0 0 1-.125-.875 9.146 9.146 0 0 1-.047-.957zm.5 3a.5.5 0 1 1 0 1 .5.5 0 0 1 0-1z" fill="#ababab" fill-rule="evenodd" font-family="sans-serif" font-weight="400" overflow="visible" style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;isolation:auto;mix-blend-mode:normal" white-space="normal"/> </g>
- </svg>`,
- "Select Type", // Default text on the button
- () => {
- // Check if the dropdown is already visible and remove it
- const existingDropdown = document.querySelector('.custom-dropdown');
- if (existingDropdown) {
- existingDropdown.remove();
- return; // Don't create a new dropdown if one is already visible
- }
- // Create the dropdown container
- let dropdown = document.createElement('div');
- dropdown.className = 'custom-dropdown';
- dropdown.style.position = 'absolute';
- dropdown.style.background = 'white';
- dropdown.style.border = 'none';
- dropdown.style.padding = '4px 0';
- dropdown.style.zIndex = '1000';
- dropdown.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.2)';
- dropdown.style.maxHeight = '200px';
- dropdown.style.overflowY = 'auto';
- dropdown.style.minWidth = '200px';
- dropdown.style.borderRadius = '8px';
- // Define the options for Task/Checkpoint
- const options = [
- {
- title: "Task",
- icon: `<svg width="24px" height="24px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#000000">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier"> <path color="#000000" d="M7.992 0A2.008 2.008 0 0 0 6 2H5c-.657 0-1.178.06-1.617.225-.439.164-.79.461-.998.838-.415.752-.37 1.673-.385 2.931v5.012c.015 1.258-.03 2.179.385 2.932.208.376.56.673.998.838.439.164.96.224 1.617.224h3.133c.021-.05.031-.102.059-.15l.482-.85H5c-.592 0-1.006-.063-1.265-.16-.26-.098-.372-.203-.473-.387C3.06 13.087 3.015 12.259 3 11V6c.015-1.259.06-2.087.262-2.453.101-.184.213-.29.473-.387C3.995 3.062 4.408 3 5 3v.999h6V3c.593 0 1.006.063 1.266.16.26.098.371.203.472.387.202.366.247 1.194.262 2.453v2.832c.31.115.582.323.752.62v.001l.248.438V5.994c-.015-1.258.031-2.179-.385-2.932a1.88 1.88 0 0 0-.998-.837C12.179 2.06 11.657 2 11 2H9.996a2.008 2.008 0 0 0-1.992-2zm.01 1c.559 0 1 .442 1 1a.99.99 0 0 1-1 1 .982.982 0 0 1-.922-.61A1.01 1.01 0 0 1 7.003 2c0-.558.441-1 1-1zM5 6v1h6.012V6zm0 2v1h6.012V8zm-.01 2v1h3v-1z" fill="gray" font-family="sans-serif" font-weight="400" overflow="visible" style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;isolation:auto;mix-blend-mode:normal;marker:none" white-space="normal"/> <path class="warning" color="#000000" d="M12.48 9.729a.443.443 0 0 0-.36.22l-3.061 5.397a.437.437 0 0 0 .379.654h6.125a.437.437 0 0 0 .379-.654l-3.059-5.397a.442.442 0 0 0-.402-.22zM12 11h1v.168c0 .348-.016.667-.047.957-.03.29-.069.581-.115.875h-.666a12.898 12.898 0 0 1-.125-.875 9.146 9.146 0 0 1-.047-.957zm.5 3a.5.5 0 1 1 0 1 .5.5 0 0 1 0-1z" fill="#ababab" fill-rule="evenodd" font-family="sans-serif" font-weight="400" overflow="visible" style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;isolation:auto;mix-blend-mode:normal" white-space="normal"/> </g>
- </svg>`
- },
- {
- title: "Checkpoint",
- icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16" fill="#000000">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier"> <path fill="#ababab" fill-rule="evenodd" d="M4,4 L9,4 C9.55228,4 10,3.55228 10,3 C10,2.44772 9.55228,2 9,2 L4,2 C2.89543,2 2,2.89543 2,4 L2,12 C2,13.1046 2.89543,14 4,14 L12,14 C13.1046,14 14,13.1046 14,12 L14,10 C14,9.44771 13.5523,9 13,9 C12.4477,9 12,9.44771 12,10 L12,12 L4,12 L4,4 Z M15.2071,2.29289 C14.8166,1.90237 14.1834,1.90237 13.7929,2.29289 L8.5,7.58579 L7.70711,6.79289 C7.31658,6.40237 6.68342,6.40237 6.29289,6.79289 C5.90237,7.18342 5.90237,7.81658 6.29289,8.20711 L7.79289,9.70711 C7.98043,9.89464 8.23478,10 8.5,10 C8.76522,10 9.01957,9.89464 9.20711,9.70711 L15.2071,3.70711 C15.5976,3.31658 15.5976,2.68342 15.2071,2.29289 Z"/> </g>
- </svg>`
- },
- ];
- // Create dropdown options
- options.forEach(option => {
- const item = document.createElement('div');
- item.style.cursor = 'pointer';
- item.style.padding = '8px 10px';
- item.style.backgroundColor = 'white';
- item.style.display ="flex";
- item.style.flexDirection ="row";
- item.style.gap = "8px"
- item.innerHTML = `${option.icon} ${option.title}`;
- // Add click handler to set the button text and close the dropdown
- item.addEventListener('click', () => {
- selectedOption = option.title;
- taskIcon.innerHTML = option.icon; // Update button text
- dropdown.remove(); // Close the dropdown
- });
- dropdown.appendChild(item);
- });
- // Append the dropdown to the button's position
- taskIcon.appendChild(dropdown);
- const rect = taskIcon.getBoundingClientRect();
- const zoomFactor = 1; // Optional zoom adjustment
- dropdown.style.left = `${rect.left / zoomFactor}px`; // Adjust for zoom level
- dropdown.style.top = `${(rect.bottom + window.scrollY) / zoomFactor}px`; // Adjust for zoom level and page scroll
- document.body.appendChild(dropdown);
- const handleClickOutside = (event) => {
- if (!taskIcon.contains(event.target)) {
- dropdown.remove();
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- }
- );
- const bountyIcon = createIconButton(
- `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512" fill="#000000">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <title>ionicons-v5-p</title>
- <circle cx="256" cy="160" r="128" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <path d="M143.65,227.82,48,400l86.86-.42a16,16,0,0,1,13.82,7.8L192,480l88.33-194.32" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <path d="M366.54,224,464,400l-86.86-.42a16,16,0,0,0-13.82,7.8L320,480,256,339.2" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <circle cx="256" cy="160" r="64" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- </g>
- </svg>`,
- "Assign Bounties",
- () => {
- // If the current element is an input field, save the value and revert to the text
- const container = document.createElement('div');
- container.classList.add('bounty-container');
- container.style.display = 'inline-flex';
- container.style.alignItems = 'center'; // Align items vertically centered
- container.style.gap = '5px'; // Small gap between icon and input
- container.style.width = '35px'; // Fixed width for the container
- // Create input field if clicking on the current text
- const inputField = document.createElement('input');
- inputField.type = 'text';
- inputField.className = 'bounty-input';
- inputField.value = currentValue; // Set the current value in the input
- inputField.style.width = '35px'; // Match the width
- inputField.style.padding = '5px';
- inputField.style.fontSize = '16px';
- inputField.style.textAlign = 'center';
- inputField.style.border = '1px solid #ababab';
- inputField.style.borderRadius = '4px';
- inputField.style.backgroundColor = 'transparent'; // Keep background transparent
- inputField.style.color = '#ababab'; // Match the SVG color
- // Append the input field into the container
- container.appendChild(inputField);
- // Replace the SVG or text with the container holding the input field
- bountyIcon.innerHTML = '';
- bountyIcon.appendChild(container);
- // Focus the input field when it's displayed
- inputField.focus();
- const value = inputField.value.trim(); // Get the trimmed value
- if (value) {
- currentValue = value; // Save the value entered by the user
- currentHtml = value;}
- // When the user finishes editing (on blur), save the value and update the display
- inputField.addEventListener('blur', function () {
- const value = inputField.value.trim(); // Get the trimmed value
- // If the input is not empty, save the new value
- if (value) {
- currentValue = value; // Save the value entered by the user
- currentHtml = value;
- }
- else {
- currentValue = "";
- currentHtml = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512" fill="#000000">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <title>ionicons-v5-p</title>
- <circle cx="256" cy="160" r="128" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <path d="M143.65,227.82,48,400l86.86-.42a16,16,0,0,1,13.82,7.8L192,480l88.33-194.32" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <path d="M366.54,224,464,400l-86.86-.42a16,16,0,0,0-13.82,7.8L320,480,256,339.2" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <circle cx="256" cy="160" r="64" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- </g>
- </svg>`
- }
- // Revert the icon to show the current value (either new or original)
- bountyIcon.innerHTML = currentHtml;
- bountyIcon.style.width = "35px";
- bountyIcon.style.minWidth = "35px";
- bountyIcon.style.color = '#ababab';
- });
- }
- );
- const assignIcon = createIconButton(
- `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <path fill-rule="evenodd" clip-rule="evenodd" d="M3 18C3 15.3945 4.66081 13.1768 6.98156 12.348C7.61232 12.1227 8.29183 12 9 12C9.70817 12 10.3877 12.1227 11.0184 12.348C11.3611 12.4703 11.6893 12.623 12 12.8027C12.3107 12.623 12.6389 12.4703 12.9816 12.348C13.6123 12.1227 14.2918 12 15 12C15.7082 12 16.3877 12.1227 17.0184 12.348C19.3392 13.1768 21 15.3945 21 18V21H15.75V19.5H19.5V18C19.5 15.5147 17.4853 13.5 15 13.5C14.4029 13.5 13.833 13.6163 13.3116 13.8275C14.3568 14.9073 15 16.3785 15 18V21H3V18ZM9 11.25C8.31104 11.25 7.66548 11.0642 7.11068 10.74C5.9977 10.0896 5.25 8.88211 5.25 7.5C5.25 5.42893 6.92893 3.75 9 3.75C10.2267 3.75 11.3158 4.33901 12 5.24963C12.6842 4.33901 13.7733 3.75 15 3.75C17.0711 3.75 18.75 5.42893 18.75 7.5C18.75 8.88211 18.0023 10.0896 16.8893 10.74C16.3345 11.0642 15.689 11.25 15 11.25C14.311 11.25 13.6655 11.0642 13.1107 10.74C12.6776 10.4869 12.2999 10.1495 12 9.75036C11.7001 10.1496 11.3224 10.4869 10.8893 10.74C10.3345 11.0642 9.68896 11.25 9 11.25ZM13.5 18V19.5H4.5V18C4.5 15.5147 6.51472 13.5 9 13.5C11.4853 13.5 13.5 15.5147 13.5 18ZM11.25 7.5C11.25 8.74264 10.2426 9.75 9 9.75C7.75736 9.75 6.75 8.74264 6.75 7.5C6.75 6.25736 7.75736 5.25 9 5.25C10.2426 5.25 11.25 6.25736 11.25 7.5ZM15 5.25C13.7574 5.25 12.75 6.25736 12.75 7.5C12.75 8.74264 13.7574 9.75 15 9.75C16.2426 9.75 17.25 8.74264 17.25 7.5C17.25 6.25736 16.2426 5.25 15 5.25Z" fill="#ABABAB"/>
- </g>
- </svg>`,
- "Select Assignee",
- () => {
- // Check if the dropdown is already visible and remove it
- const existingDropdown = document.querySelector('.custom-dropdown');
- if (existingDropdown) {
- existingDropdown.remove();
- return;
- }
- // Create the custom dropdown for team members
- let dropdown = document.createElement('div');
- dropdown.className = 'custom-dropdown';
- dropdown.style.position = 'absolute';
- dropdown.style.background = 'white';
- dropdown.style.border = '1px solid #ddd';
- dropdown.style.padding = '0';
- dropdown.style.zIndex = '1000';
- dropdown.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.2)';
- dropdown.style.maxHeight = '300px';
- dropdown.style.overflowY = 'auto';
- // Add search input to filter the team members
- searchInputWrapper.classList.add('search-input-wrapper')
- searchInputWrapper.style.width = '100%';
- searchInputWrapper.style.padding = '10px';
- let searchInput = document.createElement('input');
- searchInput.type = 'text';
- searchInput.placeholder = 'Search team members';
- searchInput.style.width = '90%';
- searchInput.style.padding = '5px';
- searchInput.style.margin = '10px';
- searchInput.style.border = '1px solid #ddd';
- searchInput.style.borderRadius = '4px';
- searchInput.style.boxSizing = 'border-box';
- searchInput.id='searchInput'
- // searchInputWrapper.appendChild(searchInput)
- // Append search input to the dropdown
- dropdown.appendChild(searchInput);
- // Container for member list
- let memberList = document.createElement('div');
- dropdown.appendChild(memberList);
- // Populate dropdown with team members
- team_members.forEach(member => {
- const memberItem = document.createElement('div');
- memberItem.textContent = member.name;
- memberItem.style.padding = '5px 10px';
- memberItem.style.cursor = 'pointer';
- memberItem.style.backgroundColor = 'white';
- // Hover effect for the dropdown items
- memberItem.addEventListener('mouseover', () => {
- memberItem.style.backgroundColor = '#f1f1f1';
- });
- memberItem.addEventListener('mouseout', () => {
- memberItem.style.backgroundColor = 'white';
- });
- // When a member is selected
- memberItem.addEventListener('click', () => {
- const initials = getInitials(member.name);
- createInitialsCircle(initials, member.name); // Replace with initials circle
- selectedMember = member.name;
- selectedMemberData = {"id":member.id,"name":member.name};
- dropdown.remove(); // Remove the dropdown
- });
- memberList.appendChild(memberItem);
- });
- // Position the dropdown below the icon
- const rect = assignIcon.getBoundingClientRect();
- const zoomFactor = 1; // Get the zoom level
- dropdown.style.left = `${rect.left / zoomFactor}px`; // Adjust for zoom level
- dropdown.style.top = `${(rect.bottom + window.scrollY) / zoomFactor}px`; // Adjust for zoom level and page scroll
- document.body.appendChild(dropdown);
- const handleClickOutside = (event) => {
- if (!assignIcon.contains(event.target) && !(event.target.id == "searchInput")) {
- dropdown.remove();
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- // Search functionality
- searchInput.addEventListener('input', function () {
- const query = searchInput.value.toLowerCase();
- const items = memberList.querySelectorAll('div');
- items.forEach(item => {
- const text = item.textContent.toLowerCase();
- if (text.indexOf(query) === -1) {
- item.style.display = 'none';
- } else {
- item.style.display = 'block';
- }
- });
- });
- // Clear button functionality
- const clearButton = document.createElement('button');
- clearButton.textContent = 'Clear';
- clearButton.style.padding = '5px 10px';
- clearButton.style.margin = '10px';
- clearButton.style.cursor = 'pointer';
- clearButton.style.border = 'none';
- clearButton.style.borderRadius = '4px';
- clearButton.style.fontSize = '14px';
- clearButton.style.background = "lightgrey";
- clearButton.addEventListener('click', () => {
- selectedMember = 'none'; // Set selected member to "none"
- selectedMemberData = {};
- assignIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <path fill-rule="evenodd" clip-rule="evenodd" d="M3 18C3 15.3945 4.66081 13.1768 6.98156 12.348C7.61232 12.1227 8.29183 12 9 12C9.70817 12 10.3877 12.1227 11.0184 12.348C11.3611 12.4703 11.6893 12.623 12 12.8027C12.3107 12.623 12.6389 12.4703 12.9816 12.348C13.6123 12.1227 14.2918 12 15 12C15.7082 12 16.3877 12.1227 17.0184 12.348C19.3392 13.1768 21 15.3945 21 18V21H15.75V19.5H19.5V18C19.5 15.5147 17.4853 13.5 15 13.5C14.4029 13.5 13.833 13.6163 13.3116 13.8275C14.3568 14.9073 15 16.3785 15 18V21H3V18ZM9 11.25C8.31104 11.25 7.66548 11.0642 7.11068 10.74C5.9977 10.0896 5.25 8.88211 5.25 7.5C5.25 5.42893 6.92893 3.75 9 3.75C10.2267 3.75 11.3158 4.33901 12 5.24963C12.6842 4.33901 13.7733 3.75 15 3.75C17.0711 3.75 18.75 5.42893 18.75 7.5C18.75 8.88211 18.0023 10.0896 16.8893 10.74C16.3345 11.0642 15.689 11.25 15 11.25C14.311 11.25 13.6655 11.0642 13.1107 10.74C12.6776 10.4869 12.2999 10.1495 12 9.75036C11.7001 10.1496 11.3224 10.4869 10.8893 10.74C10.3345 11.0642 9.68896 11.25 9 11.25ZM13.5 18V19.5H4.5V18C4.5 15.5147 6.51472 13.5 9 13.5C11.4853 13.5 13.5 15.5147 13.5 18ZM11.25 7.5C11.25 8.74264 10.2426 9.75 9 9.75C7.75736 9.75 6.75 8.74264 6.75 7.5C6.75 6.25736 7.75736 5.25 9 5.25C10.2426 5.25 11.25 6.25736 11.25 7.5ZM15 5.25C13.7574 5.25 12.75 6.25736 12.75 7.5C12.75 8.74264 13.7574 9.75 15 9.75C16.2426 9.75 17.25 8.74264 17.25 7.5C17.25 6.25736 16.2426 5.25 15 5.25Z" fill="#ABABAB"/>
- </g>
- </svg>`; // Reset to the original SVG
- dropdown.remove();
- });
- dropdown.appendChild(clearButton);
- }
- );
- function getInitials(username) {
- const nameParts = username.trim().split(/\s+/);
- return nameParts.map(part => part[0].toUpperCase()).join('');
- }
- function createInitialsCircle(initials, username) {
- // Create the circle element for initials
- const circle = document.createElement('div');
- circle.className = 'initials-circle';
- circle.style.width = '25px';
- circle.style.height = '25px';
- circle.style.borderRadius = '50%';
- circle.style.display = 'flex';
- circle.style.alignItems = 'center';
- circle.style.justifyContent = 'center';
- circle.style.backgroundColor = '#7c4dff';
- circle.style.color = '#fff';
- circle.style.cursor = 'pointer';
- circle.style.fontSize = '14px';
- circle.textContent = initials;
- // Replace the assign icon with the circle containing the initials
- assignIcon.innerHTML = ''; // Clear previous icon
- assignIcon.appendChild(circle);
- assignIcon.title = username;
- // When the circle is clicked, open the dropdown again
- circle.addEventListener('click', () => {
- createIconButton(assignIcon.innerHTML, "Assign", () => {
- openDropdown();
- });
- });
- }
- // Create the date icon button (SVG for the calendar)
- const dateIcon = createIconButton(
- `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_iconCarrier">
- <path d="M19 2H5C3.9 2 3 2.9 3 4V20C3 21.1 3.9 22 5 22H19C20.1 22 21 21.1 21 20V4C21 2.9 20.1 2 19 2ZM19 20H5V8H19V20ZM5 4H7V6H5V4ZM19 4H17V6H19V4Z" fill="#ababab"/>
- </g>
- </svg>`,
- "Select Date",
- () => {
- // Check if the date picker is already visible and remove it
- const existingDropdown = document.querySelector('.date-picker-dropdown');
- if (existingDropdown) {
- existingDropdown.remove();
- return; // Don't create a new dropdown if one is already visible
- }
- // Create the date input field (it will appear as a dropdown)
- const dateInput = document.createElement('input');
- dateInput.type = 'date';
- dateInput.className = 'date-icon-input'; // Add class for styling
- // Create a dropdown container for the date input field
- const dropdown = document.createElement('div');
- dropdown.classList.add('date-picker-dropdown', 'input-style');
- dropdown.style.position = 'absolute';
- dropdown.style.zIndex = '9999';
- dropdown.style.width = "200px";
- const rect = dateIcon.getBoundingClientRect();
- const zoomFactor = 1;
- dropdown.style.left = `${rect.left / zoomFactor}px`;
- dropdown.style.top = `${(rect.bottom + window.scrollY) / zoomFactor}px`;
- dropdown.appendChild(dateInput);
- // Create a "Clear" button in the next row
- const clearButton = document.createElement('button');
- clearButton.textContent = 'Clear';
- clearButton.classList.add('clear-button');
- clearButton.style.marginTop = '10px';
- clearButton.style.padding = '5px 10px';
- clearButton.style.cursor = 'pointer';
- clearButton.style.backgroundColor = '#f0f0f0';
- clearButton.style.border = '1px solid #ddd';
- clearButton.style.borderRadius = '4px';
- clearButton.style.fontSize = '14px';
- // When the "Clear" button is clicked, reset the input and icon
- clearButton.addEventListener('click', () => {
- dateInput.value = ''; // Reset the date input
- dateIcon.style.width = "35px";
- dateIcon.style.minWidth = "35px";
- dateIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_iconCarrier">
- <path d="M19 2H5C3.9 2 3 2.9 3 4V20C3 21.1 3.9 22 5 22H19C20.1 22 21 21.1 21 20V4C21 2.9 20.1 2 19 2ZM19 20H5V8H19V20ZM5 4H7V6H5V4ZM19 4H17V6H19V4Z" fill="#ababab"/>
- </g>
- </svg>`; // Reset the date icon text
- dropdown.remove(); // Remove the dropdown
- });
- dropdown.appendChild(clearButton);
- // Append the dropdown to the body
- document.body.appendChild(dropdown);
- const handleClickOutside = (event) => {
- if (!dateIcon.contains(event.target) && !dateInput.contains(event.target)) {
- dropdown.remove();
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- // When a date is selected, update the icon HTML with the selected date
- dateInput.addEventListener('change', function () {
- selectedDate = new Date(this.value);
- const formattedDate = selectedDate.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: '2-digit' });
- // Update the icon's HTML with the selected date
- dateIcon.innerHTML = `${formattedDate}`;
- dateIcon.offsetWidth;
- dateIcon.style.width = "auto";
- dateIcon.style.minWidth = "100px"; // Adjust this as per your needs
- // Hide the dropdown after date selection
- dropdown.remove();
- });
- }
- );
- const saveButton = document.createElement('button');
- saveButton.className = "save-button";
- saveButton.textContent = "Save";
- saveButton.style.margin = "0px 10px";
- saveButton.title = "Save Task";
- function handleKeydown(event) {
- if (event.key === 'Enter' && rowIsOpen) {
- event.preventDefault();
- saveData();
- }
- }
- const saveData = () => {
- const data = {
- "priority": selectedPriority,
- "member": selectedMemberData, // format: {id: some id, name: some name}
- "date": selectedDate,
- "bounty": currentValue,
- "type": selectedOption,
- "name": taskInput.value,
- "parent" : taskId
- };
- selectedPriority = "";
- selectedDate = "";
- selectedOption = "";
- selectedMember = "";
- selectedMemberData = {};
- currentValue = "";
- taskInput.value = "";
- addSubtask(button, data,handleKeydown,newRow);
- };
- // Add event listener to save button
- saveButton.addEventListener('click', saveData);
- document.addEventListener('keydown', handleKeydown);
- const cancelButton = document.createElement('button');
- cancelButton.className = "cancel-button";
- cancelButton.textContent = "Cancel";
- cancelButton.title = "Cancel";
- cancelButton.addEventListener('click', () => {
- const rowToRemove = cancelButton.closest('tr');
- if (rowToRemove) {
- rowToRemove.remove();
- rowIsOpen = false;
- document.removeEventListener('keydown', handleKeydown);
- }
- });
- newCell.appendChild(taskInput);
- newRow.appendChild(newCell);
- iconBtnWrapper.append(taskIcon, assignIcon, priorityIcon, bountyIcon, dateIcon);
- newCell2.append(iconBtnWrapper);
- newCell3.append(saveButton, cancelButton);
- newRow.appendChild(newCell2);
- newRow.appendChild(newCell3);
- if (addTaskRow) {
- let nextRow = addTaskRow.nextElementSibling;
- while (nextRow && nextRow.classList.contains('subtask-row')) {
- nextRow = nextRow.nextElementSibling;
- }
- nextRow.insertAdjacentElement('beforebegin', newRow);
- enforceTableCellHeight(35);
- taskInput.focus();
- }
- }
- let rowIsOpen = false;
- function addTaskRow(button) {
- let addTaskTr = button.parentElement.parentElement;
- addTaskTr.classList.add('hidden')
- rowIsOpen = true;
- // Find the row containing the button
- const addTaskRow = button.closest('tr');
- // Create a new row element
- const newRow = document.createElement('tr');
- newRow.classList.add ("task-row","add-task-row");
- newRow.style.textAlign = "left";
- newRow.style.height = "40px";
- rowIsOpen = true;
- // Create a cell that spans multiple columns
- const newCell = document.createElement('td');
- newCell.style.padding = "6px 36px";
- newCell.style.width = "50%";
- newCell.style.borderLeft = "1px solid #CED4DA";
- newCell.style.borderRight = "none";
- newCell.colSpan = "1";
- newCell.className = 'add-task-td';
- const newCell2 = document.createElement('td');
- newCell2.style.borderRight = "none";
- newCell2.style.borderLeft = "none";
- // newCell2.style.width = "10%";
- newCell2.style.padding = "10px";
- // newCell2.style.display = "flex";
- // newCell2.style.flexDirection = "row";
- // newCell2.style.gap = "5px"; // Space between icons
- newCell2.colSpan = "3";
- const newCell3 = document.createElement('td');
- newCell3.style.borderLeft = "none";
- newCell3.style.width = "10%";
- newCell3.colSpan = "2";
- newCell3.style.gap = "10px"; // Space between icons
- const taskInput = document.createElement('input');
- taskInput.type = "text";
- taskInput.classList.add("task-input-name","task-input");
- taskInput.placeholder = "Enter Task Name";
- taskInput.style.width = "100%";
- const iconBtnWrapper = document.createElement("div");
- iconBtnWrapper.classList.add('icon-btn-wrapper')
- // Helper function to create icon buttons with tooltip and onclick event
- function createIconButton(iconHTML, tooltipText, onClickHandler) {
- const button = document.createElement('button');
- button.className = "icon-button";
- button.style.height = "30px";
- button.style.width = "30px";
- button.style.display = "flex";
- button.style.alignItems = "center";
- button.style.justifyContent = "center";
- button.style.padding = "2px";
- button.innerHTML = iconHTML;
- button.title = tooltipText; // Tooltip
- button.onclick = onClickHandler; // Assign onclick function
- button.style.border = "2px solid #ababab";
- return button;
- }
- let selectedPriority = "";
- let selectedDate = "";
- let selectedOption = "";
- let selectedMember = "";
- let selectedMemberData = {};
- let currentValue = "";
- // Your exact icons with tooltips and onclick handlers
- const priorityIcon = createIconButton(
- `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
- <g id="SVGRepo_iconCarrier">
- <path
- id="priority-icon-path-add"
- d="M12 4L20 20H4L12 4Z"
- fill = "none"
- stroke="#ababab"
- stroke-width="1.5"
- stroke-linecap="round"
- stroke-linejoin="round"
- />
- <path
- d="M12 8L12 12"
- id = "priority-icon-exclamation-add"
- stroke="#ababab"
- stroke-width="1.5"
- stroke-linecap="round"
- stroke-linejoin="round"
- />
- <path
- id = "priority-iconborder-add"
- d="M12 16.01L12.01 15.9989"
- stroke="#ababab"
- stroke-width="1.5"
- stroke-linecap="round"
- stroke-linejoin="round"
- />
- </g>
- </svg>
- `,
- "Assign Priority",
- () => {
- // Check if the dropdown is already visible and remove it
- const existingDropdown = document.querySelector('.custom-dropdown');
- if (existingDropdown) {
- existingDropdown.remove();
- return; // Don't create a new dropdown if one is already visible
- }
- // Create the custom dropdown for priority options
- let dropdown = document.createElement('div');
- dropdown.className = 'custom-dropdown';
- dropdown.style.position = 'absolute';
- dropdown.style.background = 'white';
- dropdown.style.border = '1px solid #ddd';
- dropdown.style.padding = '5px';
- dropdown.style.zIndex = '1000';
- dropdown.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.2)';
- dropdown.style.maxHeight = '200px';
- dropdown.style.overflowY = 'auto';
- dropdown.style.minWidth = '200px';
- // Define the priority options
- const priorities = {
- urgent: "Urgent",
- high: "High",
- normal: "Normal",
- low: "Low"
- };
- // Create the dropdown options
- Object.keys(priorities).forEach(priority => {
- const option = document.createElement('div');
- option.textContent = priorities[priority];
- option.style.padding = '5px 10px';
- option.style.cursor = 'pointer';
- option.style.backgroundColor = 'white';
- // Hover effect for the dropdown items
- option.addEventListener('mouseover', () => {
- option.style.backgroundColor = '#f1f1f1';
- });
- option.addEventListener('mouseout', () => {
- option.style.backgroundColor = 'white';
- });
- // When an option is selected, update the flag color
- option.addEventListener('click', () => {
- selectedPriority = priority;
- const priorityIconPath = document.getElementById('priority-icon-path-add');
- const priorityIconExc = document.getElementById('priority-iconborder-add');
- const priorityOutline = document.getElementById('priority-icon-exclamation-add');
- // Define color changes for different priorities
- const colors = {
- urgent: "red",
- high: "gold",
- normal: "transparent",
- low: "grey"
- };
- // Change the color of the flag icon based on selected priority
- if (selectedPriority !== "none") {
- priorityIconPath.setAttribute('fill', colors[selectedPriority]);
- priorityIconPath.setAttribute('stroke',colors[selectedPriority]);
- if (colors[selectedPriority] == "red" )
- {
- priorityIconExc.setAttribute('stroke', 'white');
- priorityOutline.setAttribute('stroke', 'white');
- }
- else if (colors[selectedPriority] == "transparent"){
- priorityOutline.setAttribute('stroke', 'grey');
- priorityIconPath.setAttribute('stroke', 'grey');
- }
- else{
- priorityIconExc.setAttribute('stroke', 'black');
- priorityOutline.setAttribute('stroke', 'black');
- }
- }
- // Remove the dropdown after selection
- dropdown.remove();
- });
- dropdown.appendChild(option);
- });
- // Create a "Clear" button as part of the dropdown
- const clearButton = document.createElement('button');
- clearButton.textContent = 'Clear';
- clearButton.style.padding = '5px 10px';
- clearButton.style.marginTop = '10px';
- clearButton.style.cursor = 'pointer';
- clearButton.style.border = 'none';
- clearButton.style.background = "lightgrey";
- // When the "Clear" button is clicked, reset to original state
- clearButton.addEventListener('click', () => {
- const priorityIconPath = document.getElementById('priority-icon-path');
- priorityIconPath.setAttribute('fill', 'none');
- priorityIconPath.setAttribute('stroke', '#ababab');
- // Remove the dropdown after clicking "Clear"
- dropdown.remove();
- });
- dropdown.appendChild(clearButton);
- // Position the dropdown below the icon
- const rect = priorityIcon.getBoundingClientRect();
- const zoomFactor = 1; // Optional zoom adjustment
- dropdown.style.left = `${rect.left / zoomFactor}px`; // Adjust for zoom level
- dropdown.style.top = `${(rect.bottom + window.scrollY) / zoomFactor}px`; // Adjust for zoom level and page scroll
- document.body.appendChild(dropdown);
- const handleClickOutside = (event) => {
- if (!priorityIcon.contains(event.target)) {
- dropdown.remove();
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- }
- );
- const taskIcon = createIconButton(
- `<svg width="30px" height="30px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#000000">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier"> <path color="#000000" d="M7.992 0A2.008 2.008 0 0 0 6 2H5c-.657 0-1.178.06-1.617.225-.439.164-.79.461-.998.838-.415.752-.37 1.673-.385 2.931v5.012c.015 1.258-.03 2.179.385 2.932.208.376.56.673.998.838.439.164.96.224 1.617.224h3.133c.021-.05.031-.102.059-.15l.482-.85H5c-.592 0-1.006-.063-1.265-.16-.26-.098-.372-.203-.473-.387C3.06 13.087 3.015 12.259 3 11V6c.015-1.259.06-2.087.262-2.453.101-.184.213-.29.473-.387C3.995 3.062 4.408 3 5 3v.999h6V3c.593 0 1.006.063 1.266.16.26.098.371.203.472.387.202.366.247 1.194.262 2.453v2.832c.31.115.582.323.752.62v.001l.248.438V5.994c-.015-1.258.031-2.179-.385-2.932a1.88 1.88 0 0 0-.998-.837C12.179 2.06 11.657 2 11 2H9.996a2.008 2.008 0 0 0-1.992-2zm.01 1c.559 0 1 .442 1 1a.99.99 0 0 1-1 1 .982.982 0 0 1-.922-.61A1.01 1.01 0 0 1 7.003 2c0-.558.441-1 1-1zM5 6v1h6.012V6zm0 2v1h6.012V8zm-.01 2v1h3v-1z" fill="gray" font-family="sans-serif" font-weight="400" overflow="visible" style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;isolation:auto;mix-blend-mode:normal;marker:none" white-space="normal"/> <path class="warning" color="#000000" d="M12.48 9.729a.443.443 0 0 0-.36.22l-3.061 5.397a.437.437 0 0 0 .379.654h6.125a.437.437 0 0 0 .379-.654l-3.059-5.397a.442.442 0 0 0-.402-.22zM12 11h1v.168c0 .348-.016.667-.047.957-.03.29-.069.581-.115.875h-.666a12.898 12.898 0 0 1-.125-.875 9.146 9.146 0 0 1-.047-.957zm.5 3a.5.5 0 1 1 0 1 .5.5 0 0 1 0-1z" fill="#ababab" fill-rule="evenodd" font-family="sans-serif" font-weight="400" overflow="visible" style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;isolation:auto;mix-blend-mode:normal" white-space="normal"/> </g>
- </svg>`,
- "Select Type", // Default text on the button
- () => {
- // Check if the dropdown is already visible and remove it
- const existingDropdown = document.querySelector('.custom-dropdown');
- if (existingDropdown) {
- existingDropdown.remove();
- return; // Don't create a new dropdown if one is already visible
- }
- // Create the dropdown container
- let dropdown = document.createElement('div');
- dropdown.className = 'custom-dropdown';
- dropdown.style.position = 'absolute';
- dropdown.style.background = 'white';
- dropdown.style.border = 'none';
- dropdown.style.padding = '4px 0';
- dropdown.style.zIndex = '1000';
- dropdown.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.2)';
- dropdown.style.maxHeight = '200px';
- dropdown.style.overflowY = 'auto';
- dropdown.style.minWidth = '200px';
- dropdown.style.borderRadius = '8px';
- // Define the options for Task/Checkpoint
- const options = [
- {
- title: "Task",
- icon: `<svg width="24px" height="24px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#000000">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier"> <path color="#000000" d="M7.992 0A2.008 2.008 0 0 0 6 2H5c-.657 0-1.178.06-1.617.225-.439.164-.79.461-.998.838-.415.752-.37 1.673-.385 2.931v5.012c.015 1.258-.03 2.179.385 2.932.208.376.56.673.998.838.439.164.96.224 1.617.224h3.133c.021-.05.031-.102.059-.15l.482-.85H5c-.592 0-1.006-.063-1.265-.16-.26-.098-.372-.203-.473-.387C3.06 13.087 3.015 12.259 3 11V6c.015-1.259.06-2.087.262-2.453.101-.184.213-.29.473-.387C3.995 3.062 4.408 3 5 3v.999h6V3c.593 0 1.006.063 1.266.16.26.098.371.203.472.387.202.366.247 1.194.262 2.453v2.832c.31.115.582.323.752.62v.001l.248.438V5.994c-.015-1.258.031-2.179-.385-2.932a1.88 1.88 0 0 0-.998-.837C12.179 2.06 11.657 2 11 2H9.996a2.008 2.008 0 0 0-1.992-2zm.01 1c.559 0 1 .442 1 1a.99.99 0 0 1-1 1 .982.982 0 0 1-.922-.61A1.01 1.01 0 0 1 7.003 2c0-.558.441-1 1-1zM5 6v1h6.012V6zm0 2v1h6.012V8zm-.01 2v1h3v-1z" fill="gray" font-family="sans-serif" font-weight="400" overflow="visible" style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;isolation:auto;mix-blend-mode:normal;marker:none" white-space="normal"/> <path class="warning" color="#000000" d="M12.48 9.729a.443.443 0 0 0-.36.22l-3.061 5.397a.437.437 0 0 0 .379.654h6.125a.437.437 0 0 0 .379-.654l-3.059-5.397a.442.442 0 0 0-.402-.22zM12 11h1v.168c0 .348-.016.667-.047.957-.03.29-.069.581-.115.875h-.666a12.898 12.898 0 0 1-.125-.875 9.146 9.146 0 0 1-.047-.957zm.5 3a.5.5 0 1 1 0 1 .5.5 0 0 1 0-1z" fill="#ababab" fill-rule="evenodd" font-family="sans-serif" font-weight="400" overflow="visible" style="line-height:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;isolation:auto;mix-blend-mode:normal" white-space="normal"/> </g>
- </svg>`
- },
- {
- title: "Checkpoint",
- icon: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16" fill="#000000">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier"> <path fill="#ababab" fill-rule="evenodd" d="M4,4 L9,4 C9.55228,4 10,3.55228 10,3 C10,2.44772 9.55228,2 9,2 L4,2 C2.89543,2 2,2.89543 2,4 L2,12 C2,13.1046 2.89543,14 4,14 L12,14 C13.1046,14 14,13.1046 14,12 L14,10 C14,9.44771 13.5523,9 13,9 C12.4477,9 12,9.44771 12,10 L12,12 L4,12 L4,4 Z M15.2071,2.29289 C14.8166,1.90237 14.1834,1.90237 13.7929,2.29289 L8.5,7.58579 L7.70711,6.79289 C7.31658,6.40237 6.68342,6.40237 6.29289,6.79289 C5.90237,7.18342 5.90237,7.81658 6.29289,8.20711 L7.79289,9.70711 C7.98043,9.89464 8.23478,10 8.5,10 C8.76522,10 9.01957,9.89464 9.20711,9.70711 L15.2071,3.70711 C15.5976,3.31658 15.5976,2.68342 15.2071,2.29289 Z"/> </g>
- </svg>`
- },
- ];
- // Create dropdown options
- options.forEach(option => {
- const item = document.createElement('div');
- item.style.cursor = 'pointer';
- item.style.padding = '8px 10px';
- item.style.backgroundColor = 'white';
- item.style.display ="flex";
- item.style.flexDirection ="row";
- item.style.gap = "8px"
- item.innerHTML = `${option.icon} ${option.title}`;
- // Add click handler to set the button text and close the dropdown
- item.addEventListener('click', () => {
- selectedOption = option.title;
- taskIcon.innerHTML = option.icon; // Update button text
- dropdown.remove(); // Close the dropdown
- });
- dropdown.appendChild(item);
- });
- // Append the dropdown to the button's position
- taskIcon.appendChild(dropdown);
- const rect = taskIcon.getBoundingClientRect();
- const zoomFactor = 1; // Optional zoom adjustment
- dropdown.style.left = `${rect.left / zoomFactor}px`; // Adjust for zoom level
- dropdown.style.top = `${(rect.bottom + window.scrollY) / zoomFactor}px`; // Adjust for zoom level and page scroll
- document.body.appendChild(dropdown);
- const handleClickOutside = (event) => {
- if (!taskIcon.contains(event.target)) {
- dropdown.remove();
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- }
- );
- const bountyIcon = createIconButton(
- `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512" fill="#000000">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <title>ionicons-v5-p</title>
- <circle cx="256" cy="160" r="128" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <path d="M143.65,227.82,48,400l86.86-.42a16,16,0,0,1,13.82,7.8L192,480l88.33-194.32" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <path d="M366.54,224,464,400l-86.86-.42a16,16,0,0,0-13.82,7.8L320,480,256,339.2" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <circle cx="256" cy="160" r="64" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- </g>
- </svg>`,
- "Assign Bounties",
- () => {
- // If the current element is an input field, save the value and revert to the text
- const container = document.createElement('div');
- container.classList.add('bounty-container');
- container.style.display = 'inline-flex';
- container.style.alignItems = 'center'; // Align items vertically centered
- container.style.gap = '5px'; // Small gap between icon and input
- container.style.width = '35px'; // Fixed width for the container
- // Create input field if clicking on the current text
- const inputField = document.createElement('input');
- inputField.type = 'text';
- inputField.className = 'bounty-input';
- inputField.value = currentValue; // Set the current value in the input
- inputField.style.width = '35px'; // Match the width
- inputField.style.padding = '5px';
- inputField.style.fontSize = '16px';
- inputField.style.textAlign = 'center';
- inputField.style.border = '1px solid #ababab';
- inputField.style.borderRadius = '4px';
- inputField.style.backgroundColor = 'transparent'; // Keep background transparent
- inputField.style.color = '#ababab'; // Match the SVG color
- // Append the input field into the container
- container.appendChild(inputField);
- // Replace the SVG or text with the container holding the input field
- bountyIcon.innerHTML = '';
- bountyIcon.appendChild(container);
- // Focus the input field when it's displayed
- inputField.focus();
- const value = inputField.value.trim(); // Get the trimmed value
- if (value) {
- currentValue = value; // Save the value entered by the user
- currentHtml = value;}
- // When the user finishes editing (on blur), save the value and update the display
- inputField.addEventListener('blur', function () {
- const value = inputField.value.trim(); // Get the trimmed value
- // If the input is not empty, save the new value
- if (value) {
- currentValue = value; // Save the value entered by the user
- currentHtml = value;
- }
- else {
- currentValue = "";
- currentHtml = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512" fill="#000000">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <title>ionicons-v5-p</title>
- <circle cx="256" cy="160" r="128" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <path d="M143.65,227.82,48,400l86.86-.42a16,16,0,0,1,13.82,7.8L192,480l88.33-194.32" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <path d="M366.54,224,464,400l-86.86-.42a16,16,0,0,0-13.82,7.8L320,480,256,339.2" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- <circle cx="256" cy="160" r="64" style="fill:none;stroke:#ababab;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/>
- </g>
- </svg>`
- }
- // Revert the icon to show the current value (either new or original)
- bountyIcon.innerHTML = currentHtml;
- bountyIcon.style.width = "35px";
- bountyIcon.style.minWidth = "35px";
- bountyIcon.style.color = '#ababab';
- });
- }
- );
- const assignIcon = createIconButton(
- `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <path fill-rule="evenodd" clip-rule="evenodd" d="M3 18C3 15.3945 4.66081 13.1768 6.98156 12.348C7.61232 12.1227 8.29183 12 9 12C9.70817 12 10.3877 12.1227 11.0184 12.348C11.3611 12.4703 11.6893 12.623 12 12.8027C12.3107 12.623 12.6389 12.4703 12.9816 12.348C13.6123 12.1227 14.2918 12 15 12C15.7082 12 16.3877 12.1227 17.0184 12.348C19.3392 13.1768 21 15.3945 21 18V21H15.75V19.5H19.5V18C19.5 15.5147 17.4853 13.5 15 13.5C14.4029 13.5 13.833 13.6163 13.3116 13.8275C14.3568 14.9073 15 16.3785 15 18V21H3V18ZM9 11.25C8.31104 11.25 7.66548 11.0642 7.11068 10.74C5.9977 10.0896 5.25 8.88211 5.25 7.5C5.25 5.42893 6.92893 3.75 9 3.75C10.2267 3.75 11.3158 4.33901 12 5.24963C12.6842 4.33901 13.7733 3.75 15 3.75C17.0711 3.75 18.75 5.42893 18.75 7.5C18.75 8.88211 18.0023 10.0896 16.8893 10.74C16.3345 11.0642 15.689 11.25 15 11.25C14.311 11.25 13.6655 11.0642 13.1107 10.74C12.6776 10.4869 12.2999 10.1495 12 9.75036C11.7001 10.1496 11.3224 10.4869 10.8893 10.74C10.3345 11.0642 9.68896 11.25 9 11.25ZM13.5 18V19.5H4.5V18C4.5 15.5147 6.51472 13.5 9 13.5C11.4853 13.5 13.5 15.5147 13.5 18ZM11.25 7.5C11.25 8.74264 10.2426 9.75 9 9.75C7.75736 9.75 6.75 8.74264 6.75 7.5C6.75 6.25736 7.75736 5.25 9 5.25C10.2426 5.25 11.25 6.25736 11.25 7.5ZM15 5.25C13.7574 5.25 12.75 6.25736 12.75 7.5C12.75 8.74264 13.7574 9.75 15 9.75C16.2426 9.75 17.25 8.74264 17.25 7.5C17.25 6.25736 16.2426 5.25 15 5.25Z" fill="#ABABAB"/>
- </g>
- </svg>`,
- "Select Assignee",
- () => {
- // Check if the dropdown is already visible and remove it
- const existingDropdown = document.querySelector('.custom-dropdown');
- if (existingDropdown) {
- existingDropdown.remove();
- return;
- }
- // Create the custom dropdown for team members
- let dropdown = document.createElement('div');
- dropdown.className = 'custom-dropdown';
- dropdown.style.position = 'absolute';
- dropdown.style.background = 'white';
- dropdown.style.border = '1px solid #ddd';
- dropdown.style.borderRadius = '4px';
- dropdown.style.padding = '0';
- dropdown.style.zIndex = '1000';
- dropdown.style.boxShadow = '0 8px 16px rgba(0, 0, 0, 0.2)';
- dropdown.style.maxHeight = '300px';
- dropdown.style.overflowY = 'auto';
- // Add search input to filter the team members
- let searchInputWrapper = document.createElement('div');
- searchInputWrapper.classList.add('search-input-wrapper')
- searchInputWrapper.style.width = '100%';
- searchInputWrapper.style.padding = '10px';
- let searchInput = document.createElement('input');
- searchInput.type = 'text';
- searchInput.placeholder = 'Search team members';
- searchInput.style.width = '90%';
- searchInput.style.padding = '5px';
- searchInput.style.margin = '10px';
- searchInput.style.border = '1px solid #ddd';
- searchInput.style.borderRadius = '4px';
- searchInput.style.boxSizing = 'border-box';
- searchInput.id='searchInput'
- // searchInputWrapper.appendChild(searchInput)
- // Append search input to the dropdown
- dropdown.appendChild(searchInput);
- // Container for member list
- let memberList = document.createElement('div');
- dropdown.appendChild(memberList);
- // Populate dropdown with team members
- team_members.forEach(member => {
- const memberItem = document.createElement('div');
- memberItem.textContent = member.name;
- memberItem.style.padding = '5px 10px';
- memberItem.style.cursor = 'pointer';
- memberItem.style.backgroundColor = 'white';
- // Hover effect for the dropdown items
- memberItem.addEventListener('mouseover', () => {
- memberItem.style.backgroundColor = '#f1f1f1';
- });
- memberItem.addEventListener('mouseout', () => {
- memberItem.style.backgroundColor = 'white';
- });
- // When a member is selected
- memberItem.addEventListener('click', () => {
- const initials = getInitials(member.name);
- createInitialsCircle(initials, member.name); // Replace with initials circle
- selectedMember = member.name;
- selectedMemberData = {"id":member.id,"name":member.name};
- dropdown.remove(); // Remove the dropdown
- });
- memberList.appendChild(memberItem);
- });
- // Position the dropdown below the icon
- const rect = assignIcon.getBoundingClientRect();
- const zoomFactor = 1; // Get the zoom level
- dropdown.style.left = `${rect.left / zoomFactor}px`; // Adjust for zoom level
- dropdown.style.top = `${(rect.bottom + window.scrollY) / zoomFactor}px`; // Adjust for zoom level and page scroll
- document.body.appendChild(dropdown);
- const handleClickOutside = (event) => {
- if (!assignIcon.contains(event.target) && !(event.target.id == "searchInput")) {
- dropdown.remove();
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- // Search functionality
- searchInput.addEventListener('input', function () {
- const query = searchInput.value.toLowerCase();
- const items = memberList.querySelectorAll('div');
- items.forEach(item => {
- const text = item.textContent.toLowerCase();
- if (text.indexOf(query) === -1) {
- item.style.display = 'none';
- } else {
- item.style.display = 'block';
- }
- });
- });
- // Clear button functionality
- const clearButton = document.createElement('button');
- clearButton.textContent = 'Clear';
- clearButton.style.padding = '5px 10px';
- clearButton.style.margin = '10px';
- clearButton.style.cursor = 'pointer';
- clearButton.style.border = 'none';
- clearButton.style.borderRadius = '4px';
- clearButton.style.fontSize = '14px';
- clearButton.style.background = "lightgrey";
- clearButton.addEventListener('click', () => {
- selectedMember = 'none'; // Set selected member to "none"
- selectedMemberData = {};
- assignIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <path fill-rule="evenodd" clip-rule="evenodd" d="M3 18C3 15.3945 4.66081 13.1768 6.98156 12.348C7.61232 12.1227 8.29183 12 9 12C9.70817 12 10.3877 12.1227 11.0184 12.348C11.3611 12.4703 11.6893 12.623 12 12.8027C12.3107 12.623 12.6389 12.4703 12.9816 12.348C13.6123 12.1227 14.2918 12 15 12C15.7082 12 16.3877 12.1227 17.0184 12.348C19.3392 13.1768 21 15.3945 21 18V21H15.75V19.5H19.5V18C19.5 15.5147 17.4853 13.5 15 13.5C14.4029 13.5 13.833 13.6163 13.3116 13.8275C14.3568 14.9073 15 16.3785 15 18V21H3V18ZM9 11.25C8.31104 11.25 7.66548 11.0642 7.11068 10.74C5.9977 10.0896 5.25 8.88211 5.25 7.5C5.25 5.42893 6.92893 3.75 9 3.75C10.2267 3.75 11.3158 4.33901 12 5.24963C12.6842 4.33901 13.7733 3.75 15 3.75C17.0711 3.75 18.75 5.42893 18.75 7.5C18.75 8.88211 18.0023 10.0896 16.8893 10.74C16.3345 11.0642 15.689 11.25 15 11.25C14.311 11.25 13.6655 11.0642 13.1107 10.74C12.6776 10.4869 12.2999 10.1495 12 9.75036C11.7001 10.1496 11.3224 10.4869 10.8893 10.74C10.3345 11.0642 9.68896 11.25 9 11.25ZM13.5 18V19.5H4.5V18C4.5 15.5147 6.51472 13.5 9 13.5C11.4853 13.5 13.5 15.5147 13.5 18ZM11.25 7.5C11.25 8.74264 10.2426 9.75 9 9.75C7.75736 9.75 6.75 8.74264 6.75 7.5C6.75 6.25736 7.75736 5.25 9 5.25C10.2426 5.25 11.25 6.25736 11.25 7.5ZM15 5.25C13.7574 5.25 12.75 6.25736 12.75 7.5C12.75 8.74264 13.7574 9.75 15 9.75C16.2426 9.75 17.25 8.74264 17.25 7.5C17.25 6.25736 16.2426 5.25 15 5.25Z" fill="#ABABAB"/>
- </g>
- </svg>`; // Reset to the original SVG
- dropdown.remove();
- });
- dropdown.appendChild(clearButton);
- }
- );
- function getInitials(username) {
- const nameParts = username.trim().split(/\s+/);
- return nameParts.map(part => part[0].toUpperCase()).join('');
- }
- function createInitialsCircle(initials, username) {
- // Create the circle element for initials
- const circle = document.createElement('div');
- circle.className = 'initials-circle';
- circle.style.width = '25px';
- circle.style.height = '25px';
- circle.style.borderRadius = '50%';
- circle.style.display = 'flex';
- circle.style.alignItems = 'center';
- circle.style.justifyContent = 'center';
- circle.style.backgroundColor = '#7c4dff';
- circle.style.color = '#fff';
- circle.style.cursor = 'pointer';
- circle.style.fontSize = '14px';
- circle.textContent = initials;
- // Replace the assign icon with the circle containing the initials
- assignIcon.innerHTML = ''; // Clear previous icon
- assignIcon.appendChild(circle);
- assignIcon.title = username;
- // When the circle is clicked, open the dropdown again
- circle.addEventListener('click', () => {
- createIconButton(assignIcon.innerHTML, "Assign", () => {
- openDropdown();
- });
- });
- }
- // Create the date icon button (SVG for the calendar)
- const dateIcon = createIconButton(
- `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_iconCarrier">
- <path d="M19 2H5C3.9 2 3 2.9 3 4V20C3 21.1 3.9 22 5 22H19C20.1 22 21 21.1 21 20V4C21 2.9 20.1 2 19 2ZM19 20H5V8H19V20ZM5 4H7V6H5V4ZM19 4H17V6H19V4Z" fill="#ababab"/>
- </g>
- </svg>`,
- "Select Date",
- () => {
- // Check if the date picker is already visible and remove it
- const existingDropdown = document.querySelector('.date-picker-dropdown');
- if (existingDropdown) {
- existingDropdown.remove();
- return; // Don't create a new dropdown if one is already visible
- }
- // Create the date input field (it will appear as a dropdown)
- const dateInput = document.createElement('input');
- dateInput.type = 'date';
- dateInput.className = 'date-icon-input'; // Add class for styling
- // Create a dropdown container for the date input field
- const dropdown = document.createElement('div');
- dropdown.classList.add('date-picker-dropdown', 'input-style');
- dropdown.style.position = 'absolute';
- dropdown.style.zIndex = '9999';
- dropdown.style.width = "200px";
- const rect = dateIcon.getBoundingClientRect();
- const zoomFactor = 1;
- dropdown.style.left = `${rect.left / zoomFactor}px`;
- dropdown.style.top = `${(rect.bottom + window.scrollY) / zoomFactor}px`;
- dropdown.appendChild(dateInput);
- // Create a "Clear" button in the next row
- const clearButton = document.createElement('button');
- clearButton.textContent = 'Clear';
- clearButton.classList.add('clear-button');
- clearButton.style.marginTop = '10px';
- clearButton.style.padding = '5px 10px';
- clearButton.style.cursor = 'pointer';
- clearButton.style.backgroundColor = '#f0f0f0';
- clearButton.style.border = '1px solid #ddd';
- clearButton.style.borderRadius = '4px';
- clearButton.style.fontSize = '14px';
- // When the "Clear" button is clicked, reset the input and icon
- clearButton.addEventListener('click', () => {
- dateInput.value = ''; // Reset the date input
- dateIcon.style.width = "35px";
- dateIcon.style.minWidth = "35px";
- dateIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_iconCarrier">
- <path d="M19 2H5C3.9 2 3 2.9 3 4V20C3 21.1 3.9 22 5 22H19C20.1 22 21 21.1 21 20V4C21 2.9 20.1 2 19 2ZM19 20H5V8H19V20ZM5 4H7V6H5V4ZM19 4H17V6H19V4Z" fill="#ababab"/>
- </g>
- </svg>`; // Reset the date icon text
- dropdown.remove(); // Remove the dropdown
- });
- dropdown.appendChild(clearButton);
- // Append the dropdown to the body
- document.body.appendChild(dropdown);
- const handleClickOutside = (event) => {
- if (!dateIcon.contains(event.target) && !dateInput.contains(event.target)) {
- dropdown.remove();
- document.removeEventListener('click', handleClickOutside);
- }
- };
- document.addEventListener('click', handleClickOutside);
- // When a date is selected, update the icon HTML with the selected date
- dateInput.addEventListener('change', function () {
- selectedDate = new Date(this.value);
- const formattedDate = selectedDate.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: '2-digit' });
- // Update the icon's HTML with the selected date
- dateIcon.innerHTML = `${formattedDate}`;
- dateIcon.offsetWidth;
- dateIcon.style.width = "auto";
- dateIcon.style.minWidth = "100px"; // Adjust this as per your needs
- // Hide the dropdown after date selection
- dropdown.remove();
- });
- }
- );
- const saveButton = document.createElement('button');
- saveButton.className = "save-button";
- saveButton.textContent = "Save";
- saveButton.style.margin = "0px 10px";
- saveButton.title = "Save Task";
- function handleKeydown(event) {
- if (event.key === 'Enter' && rowIsOpen) {
- event.preventDefault();
- saveData();
- }
- }
- const saveData = () => {
- const data = {
- "priority": selectedPriority,
- "member": selectedMemberData, // format: {id: some id, name: some name}
- "date": selectedDate,
- "bounty": currentValue,
- "type": selectedOption,
- "name": taskInput.value
- };
- selectedPriority = "";
- selectedDate = "";
- selectedOption = "";
- selectedMember = "";
- selectedMemberData = {};
- currentValue = "";
- taskInput.value = "";
- addTask(button, data,handleKeydown);
- addTaskTr.classList.remove('hidden')
- };
- // Add event listener to save button
- saveButton.addEventListener('click', saveData);
- document.addEventListener('keydown', handleKeydown);
- const cancelButton = document.createElement('button');
- cancelButton.className = "cancel-button";
- cancelButton.textContent = "Cancel";
- cancelButton.title = "Cancel";
- cancelButton.addEventListener('click', () => {
- const rowToRemove = cancelButton.closest('tr');
- if (rowToRemove) {
- rowToRemove.remove();
- rowIsOpen = false;
- document.removeEventListener('keydown', handleKeydown);
- }
- addTaskTr.classList.remove('hidden')
- });
- newCell.appendChild(taskInput);
- newRow.appendChild(newCell);
- iconBtnWrapper.append(taskIcon, assignIcon, priorityIcon, bountyIcon, dateIcon);
- newCell2.append(iconBtnWrapper);
- newCell3.append(saveButton, cancelButton);
- newRow.appendChild(newCell2);
- newRow.appendChild(newCell3);
- addTaskRow.insertAdjacentElement('beforebegin', newRow);
- taskInput.focus();
- enforceTableCellHeight(35);
- }
- const priorityMapping = {
- urgent: {
- icon: `<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_iconCarrier">
- <path
- id="priority-icon-path"
- d="M12 4L20 20H4L12 4Z"
- fill="red" stroke="red" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
- <path d="M12 8L12 12" id="priority-icon-exclamation" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
- <path id="priority-icon-border" d="M12 16.01L12.01 15.9989" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
- </g>
- </svg>`,
- color: "red",
- stroke: "red"
- },
- high: {
- icon: `<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_iconCarrier">
- <path
- id="priority-icon-path"
- d="M12 4L20 20H4L12 4Z"
- fill="gold" stroke="gold" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
- <path d="M12 8L12 12" id="priority-icon-exclamation" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
- <path id="priority-icon-border" d="M12 16.01L12.01 15.9989" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
- </g>
- </svg>`,
- color: "gold",
- stroke: "black"
- },
- normal: {
- icon: `<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_iconCarrier">
- <path
- id="priority-icon-path"
- d="M12 4L20 20H4L12 4Z"
- fill="transparent" stroke="grey" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
- <path d="M12 8L12 12" id="priority-icon-exclamation" stroke="grey" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
- <path id="priority-icon-border" d="M12 16.01L12.01 15.9989" stroke="grey" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
- </g>
- </svg>`,
- color: "blue",
- stroke: "white"
- },
- low: {
- icon: `<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_iconCarrier">
- <path
- id="priority-icon-path"
- d="M12 4L20 20H4L12 4Z"
- fill="grey" stroke="grey" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
- <path d="M12 8L12 12" id="priority-icon-exclamation" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
- <path id="priority-icon-border" d="M12 16.01L12.01 15.9989" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
- </g>
- </svg>`,
- color: "grey",
- stroke: "white"
- }
- };
- async function addTask(button,data,handleKeydown) {
- //gett the nearest table
- const milestoneSection = button.closest('table');
- const milestoneHeader = milestoneSection.querySelector('thead tr th:first-child');
- const milestoneText = milestoneHeader ? milestoneHeader.textContent.trim() : null;
- const milestoneSectionInput = button.closest('tr');
- // input for addtask
- const taskInput = data.name;
- const assigneeInput = data.member.id;
- const dateInput = data.date;
- const bountyInput = data.bounty;
- const checkpointCheckbox = data.type == "Checkpoint";
- const selectedAssignee = data.member.name;
- const priorityText = data.priority;
- let { label, color } = { label: `<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24" fill="none">
- <g id="SVGRepo_iconCarrier">
- <path d="M19 2H5C3.9 2 3 2.9 3 4V20C3 21.1 3.9 22 5 22H19C20.1 22 21 21.1 21 20V4C21 2.9 20.1 2 19 2ZM19 20H5V8H19V20ZM5 4H7V6H5V4ZM19 4H17V6H19V4Z" fill="#ababab"/>
- </g>
- </svg>`, color: "black" }; // Initialize with default values
- if (dateInput) {
- const formatted = `${dateInput.getFullYear()}-${String(dateInput.getMonth() + 1).padStart(2, '0')}-${String(dateInput.getDate()).padStart(2, '0')}`;
- ({ label, color } = getDateLabelAndColor(formatted));
- }
- // validate the task name input
- if (!taskInput.trim()) {
- alert("Please enter a valid task name.");
- return;
- }
- let formattedDate = null;
- if (dateInput){
- duedate = dateInput;
- formattedDate = `${duedate.getFullYear()}-${String(duedate.getMonth() + 1).padStart(2, '0')}-${String(duedate.getDate()).padStart(2, '0')}`;
- }
- else{
- formattedDate = null;
- duedate = null;
- }
- //BACKEND Call
- const taskData = {
- name: taskInput,
- assignee: assigneeInput,
- dueDate: formattedDate,
- bounty: bountyInput,
- isCheckpoint: checkpointCheckbox,
- priority : priorityText,
- project_ID: projectId,
- milestone: milestoneText
- };
- const taskId = await saveTaskToBackend(taskData);
- taskFilterContainer.style.display = "block";
- noTasksMessage.style.display = "none";
- // create a new tr for task
- const newTaskRow = document.createElement('tr');
- newTaskRow.style.textAlign = 'left';
- newTaskRow.style.height = '40px';
- newTaskRow.classList.add("task-row");
- newTaskRow.id = taskId;
- newTaskRow.setAttribute('draggable', 'true');
- newTaskRow.setAttribute('ondragstart', 'drag(event)');
- newTaskRow.setAttribute('ondragover', 'allowDrop(event)');
- newTaskRow.setAttribute('ondrop', 'drop(event)');
- newTaskRow.setAttribute('data-status', 'I'); // Add initial status
- newTaskRow.setAttribute('value', 'I'); // Add initial value
- // set the inner html for task
- let priorityIcon = "";
- if (priorityText){
- priorityIcon = priorityMapping[priorityText.toLowerCase()].icon;
- }
- else{
- priorityIcon = priorityMapping['normal'].icon;
- }
- if (checkpointCheckbox) {
- // create a single task for milestone check
- let flagColor = "#DD1D43"; // Default red for past due
- const today = new Date();
- today.setHours(0, 0, 0, 0);
- if (dateInput && new Date(dateInput) > today) {
- flagColor = "grey"; // Grey for future due date
- }
- newTaskRow.innerHTML = `
- <td class="task-row-elems">
- <div class="task-row-title" style="display: flex; flex-direction: row; align-items: center; gap: 4px;">
- <span class="flag-icon" style="display: inline-block; cursor:pointer; width: 20px; height: 20px; margin-left: 0px;">
- <svg onclick="updateFlag(this)" width="16" height="19" viewBox="0 0 16 19" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink: 0;">
- <path d="M0 19V0H16L14 5L16 10H2V19H0Z" fill="#DD1D43"/>
- </svg>
- </span>
- <span onclick="openTaskModal(${taskId})" id="task-name-${taskId}" class="task-name" style="padding-left: 10px;"> ${taskInput}</span>
- </div>
- <div style="display: flex; gap: 10px;">
- <button title="Rename Task" onclick="editTask(${taskId})" class="edit-btn">
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M7.37333 4.01333L7.98667 4.62667L1.94667 10.6667H1.33333V10.0533L7.37333 4.01333ZM9.77333 0C9.60667 0 9.43333 0.0666666 9.30667 0.193333L8.08667 1.41333L10.5867 3.91333L11.8067 2.69333C12.0667 2.43333 12.0667 2.01333 11.8067 1.75333L10.2467 0.193333C10.1133 0.06 9.94667 0 9.77333 0ZM7.37333 2.12667L0 9.5V12H2.5L9.87333 4.62667L7.37333 2.12667Z" fill="#939CA3"/>
- </svg>
- </button>
- <button title="Delete Task" onclick="deleteTask(this,${taskId})" class="edit-btn">
- <svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M3 3L13 13M3 13L13 3" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </button>
- </div>
- </td>
- <td style="border-left: 1px solid #CED4DA; padding-left: 10px;"></td>
- <td class="assignee-input" style="border-left: 1px solid #CED4DA; padding-left: 10px; display: flex; justify-content: center; align-items: center;">
- <button title="Assign Assignee" id="${taskId}" class="assign-btn" onclick="openAssigneeDropdown(${taskId},this)" style="border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- ${selectedAssignee ? `
- <div class="initials-circle" style="width: 25px; height: 25px; border-radius: 50%; display: flex; justify-content: center; align-items: center; background-color: #7c4dff; color: #fff; font-size: 14px; cursor: pointer;">
- ${getInitials(selectedAssignee)}
- </div>` : `
- <svg class="assignee-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" style="width: 24px; height: 24px; fill: #ABABAB;">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <path fill-rule="evenodd" clip-rule="evenodd" d="M3 18C3 15.3945 4.66081 13.1768 6.98156 12.348C7.61232 12.1227 8.29183 12 9 12C9.70817 12 10.3877 12.1227 11.0184 12.348C11.3611 12.4703 11.6893 12.623 12 12.8027C12.3107 12.623 12.6389 12.4703 12.9816 12.348C13.6123 12.1227 14.2918 12 15 12C15.7082 12 16.3877 12.1227 17.0184 12.348C19.3392 13.1768 21 15.3945 21 18V21H15.75V19.5H19.5V18C19.5 15.5147 17.4853 13.5 15 13.5C14.4029 13.5 13.833 13.6163 13.3116 13.8275C14.3568 14.9073 15 16.3785 15 18V21H3V18ZM9 11.25C8.31104 11.25 7.66548 11.0642 7.11068 10.74C5.9977 10.0896 5.25 8.88211 5.25 7.5C5.25 5.42893 6.92893 3.75 9 3.75C10.2267 3.75 11.3158 4.33901 12 5.24963C12.6842 4.33901 13.7733 3.75 15 3.75C17.0711 3.75 18.75 5.42893 18.75 7.5C18.75 8.88211 18.0023 10.0896 16.8893 10.74C16.3345 11.0642 15.689 11.25 15 11.25C14.311 11.25 13.6655 11.0642 13.1107 10.74C12.6776 10.4869 12.2999 10.1495 12 9.75036C11.7001 10.1496 11.3224 10.4869 10.8893 10.74C10.3345 11.0642 9.68896 11.25 9 11.25ZM13.5 18V19.5H4.5V18C4.5 15.5147 6.51472 13.5 9 13.5C11.4853 13.5 13.5 15.5147 13.5 18ZM11.25 7.5C11.25 8.74264 10.2426 9.75 9 9.75C7.75736 9.75 6.75 8.74264 6.75 7.5C6.75 6.25736 7.75736 5.25 9 5.25C10.2426 5.25 11.25 6.25736 11.25 7.5ZM15 5.25C13.7574 5.25 12.75 6.25736 12.75 7.5C12.75 8.74264 13.7574 9.75 15 9.75C16.2426 9.75 17.25 8.74264 17.25 7.5C17.25 6.25736 16.2426 5.25 15 5.25Z" />
- </g>
- </svg>`
- }
- </button>
- </td>
- <td class="date-input" value="${duedate}" style=" justify-items:center; align-items: center;border-left: 1px solid #CED4DA; padding-left: 10px; color: ${color};">
- <button title="Change Date" class="assign-btn"
- {% if is_user_spm or request.user == project_detail.manager or hat_type == 'projectManager' %}
- onclick="openDueDateDropdown(${taskId},this)"
- {% endif %}
- style="border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- ${label}
- </button>
- </td>
- <td class="priority-input" value="${priorityText}" title="${priorityText}" style="border-left: 1px solid #CED4DA; padding-left: 10px; text-align: center; vertical-align: middle;justify-content: center; display:flex;">
- <button title="Change Priority" class="assign-btn"
- onclick="openPriorityDropdown(${taskId},this)"
- style="border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- ${priorityIcon || '-' }
- </button>
- </td>
- <td class="bountie-input" style="padding-left: 10px; justify-content: center; justify-items:center;">
- <button title="Change Bounties" id="${taskId}" value="${bountyInput || '1'}" class="assign-btn bounty-btn"
- {% if is_user_spm or request.user == project_detail.manager or hat_type == 'projectManager' %}
- onclick="openBountyDropdown(${taskId},this)"
- {% endif %}
- style="justify-content: center;width:35px; border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- <div>${bountyInput || '-'}</div>
- </button>
- </td>
- `;
- } else {
- newTaskRow.innerHTML = `
- <td class="task-row-elems">
- <div class="" style="width: 100%; display: flex; flex-direction: row; align-items: center; gap: 4px; justify-content: space-between;">
- <div class="task-row-title"><!-- Always add the checkbox for new tasks since they start with 0 subtasks -->
- <label class="circular-checkbox">
- <input
- class="taskCheckbox"
- type="checkbox"
- onchange="updateTaskStatus(${taskId}, this)"
- >
- </label>
- <svg id="togglesvg" class="arrow-svg" onclick="toggleSubtask(this)" style="display:none; cursor: pointer;" width="8" height="20" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M1 9L5 5L1 1" stroke="#6D747A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- <span id="togglespan" style="padding-left:8px;"></span>
- <span onclick="openTaskModal(${taskId})" id="task-name-${taskId}"class="task-name" style="padding-left:10px;"> ${taskInput}</span>
- </div>
- <div style="display: flex; gap: 10px;">
- <button title="Add Subtask" onclick="addSubtaskRow(this, ${taskId})" class="edit-btn">
- <svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M8 3V13M3 8H13" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </button>
- <!-- Edit Icon to Rename Task -->
- <button title="Rename Task" onclick="editTask(${taskId})" class="edit-btn">
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M7.37333 4.01333L7.98667 4.62667L1.94667 10.6667H1.33333V10.0533L7.37333 4.01333ZM9.77333 0C9.60667 0 9.43333 0.0666666 9.30667 0.193333L8.08667 1.41333L10.5867 3.91333L11.8067 2.69333C12.0667 2.43333 12.0667 2.01333 11.8067 1.75333L10.2467 0.193333C10.1133 0.06 9.94667 0 9.77333 0ZM7.37333 2.12667L0 9.5V12H2.5L9.87333 4.62667L7.37333 2.12667Z" fill="#939CA3"/>
- </svg>
- </button>
- <button title="Delete Task" onclick="deleteTask(this,${taskId})" class="edit-btn">
- <svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M3 3L13 13M3 13L13 3" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </button>
- </div>
- <span class="subtask-count"> (0) </span>
- </div>
- </td>
- <td style="padding: 10px;">
- <div style="display:none;" class="no-subtasks-message"> No subtasks </div>
- <div class="status-bar-container" style="position: relative;">
- <div class="status-bar" style="width: 100%; height: 10px; background-color: lightgray;">
- <div class="status-bar-completed" style="height: 100%; background-color: green;"></div>
- </div>
- <div class="progress-tooltip">
- 0 out of 0 tasks remaining
- </div>
- </div> </td>
- <td class="assignee-input" style="border-left: 1px solid #CED4DA; padding-left: 10px; display: flex; justify-content: center; align-items: center;">
- <button title="Assign Assignee" id="${taskId}" class="assign-btn" onclick="openAssigneeDropdown(${taskId},this)" style="border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- ${selectedAssignee ? `
- <div class="initials-circle" style="width: 25px; height: 25px; border-radius: 50%; display: flex; justify-content: center; align-items: center; background-color: #7c4dff; color: #fff; font-size: 14px; cursor: pointer;">
- ${getInitials(selectedAssignee)}
- </div>` : `
- <svg class="assignee-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" style="width: 24px; height: 24px; fill: #ABABAB;">
- <g id="SVGRepo_bgCarrier" stroke-width="0"/>
- <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
- <g id="SVGRepo_iconCarrier">
- <path fill-rule="evenodd" clip-rule="evenodd" d="M3 18C3 15.3945 4.66081 13.1768 6.98156 12.348C7.61232 12.1227 8.29183 12 9 12C9.70817 12 10.3877 12.1227 11.0184 12.348C11.3611 12.4703 11.6893 12.623 12 12.8027C12.3107 12.623 12.6389 12.4703 12.9816 12.348C13.6123 12.1227 14.2918 12 15 12C15.7082 12 16.3877 12.1227 17.0184 12.348C19.3392 13.1768 21 15.3945 21 18V21H15.75V19.5H19.5V18C19.5 15.5147 17.4853 13.5 15 13.5C14.4029 13.5 13.833 13.6163 13.3116 13.8275C14.3568 14.9073 15 16.3785 15 18V21H3V18ZM9 11.25C8.31104 11.25 7.66548 11.0642 7.11068 10.74C5.9977 10.0896 5.25 8.88211 5.25 7.5C5.25 5.42893 6.92893 3.75 9 3.75C10.2267 3.75 11.3158 4.33901 12 5.24963C12.6842 4.33901 13.7733 3.75 15 3.75C17.0711 3.75 18.75 5.42893 18.75 7.5C18.75 8.88211 18.0023 10.0896 16.8893 10.74C16.3345 11.0642 15.689 11.25 15 11.25C14.311 11.25 13.6655 11.0642 13.1107 10.74C12.6776 10.4869 12.2999 10.1495 12 9.75036C11.7001 10.1496 11.3224 10.4869 10.8893 10.74C10.3345 11.0642 9.68896 11.25 9 11.25ZM13.5 18V19.5H4.5V18C4.5 15.5147 6.51472 13.5 9 13.5C11.4853 13.5 13.5 15.5147 13.5 18ZM11.25 7.5C11.25 8.74264 10.2426 9.75 9 9.75C7.75736 9.75 6.75 8.74264 6.75 7.5C6.75 6.25736 7.75736 5.25 9 5.25C10.2426 5.25 11.25 6.25736 11.25 7.5ZM15 5.25C13.7574 5.25 12.75 6.25736 12.75 7.5C12.75 8.74264 13.7574 9.75 15 9.75C16.2426 9.75 17.25 8.74264 17.25 7.5C17.25 6.25736 16.2426 5.25 15 5.25Z" />
- </g>
- </svg>`
- }
- </button>
- </td>
- <td class="date-input" value="${duedate}" style=" justify-items:center; align-items: center;border-left: 1px solid #CED4DA; padding-left: 10px; color: ${color};">
- <button title="Change Date" class="assign-btn"
- {% if is_user_spm or request.user == project_detail.manager or hat_type == 'projectManager' %}
- onclick="openDueDateDropdown(${taskId},this)"
- {% endif %}
- style="border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- ${label}
- </button>
- </td>
- <td class="priority-input" value="${priorityText}" title="${priorityText}" style="border-left: 1px solid #CED4DA; padding-left: 10px; text-align: center; vertical-align: middle;justify-content: center; display:flex;">
- <button title="Change Priority" class="assign-btn"
- onclick="openPriorityDropdown(${taskId},this)" style="border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- ${priorityIcon || '-' }
- </button>
- </td>
- <td class="bountie-input" style="padding-left: 10px; justify-content: center; justify-items:center;">
- <button title="Change Bounties" id="${taskId}" value="${bountyInput || '1'}" class="assign-btn bounty-btn"
- {% if is_user_spm or request.user == project_detail.manager or hat_type == 'projectManager' %}
- onclick="openBountyDropdown(${taskId},this)"
- {% endif %}style="justify-content: center;width:35px; border: none; background: none; cursor: pointer; display: flex; align-items: center;">
- <div>${bountyInput || '-'}</div>
- </button>
- </td>
- `;
- }
- // append the new task before the bitton task row
- const tableBody = milestoneSection.querySelector('tbody');
- const buttonRow = milestoneSection.querySelector('.add-task-row');
- tableBody.insertBefore(newTaskRow, buttonRow);
- buttonRow.remove();
- rowIsOpen = false;
- document.removeEventListener('keydown', handleKeydown);
- updateProgress(newTaskRow);
- enforceTableCellHeight(35);
- }
- function updateTaskStatus(taskId, checkbox) {
- checkbox.checked = !checkbox.checked; // Revert the automatic change
- const newStatus = !checkbox.checked ? 'C' : 'I';
- const taskRow = checkbox.closest('tr');
- // Store references to related elements
- const parentTaskId = taskRow.getAttribute('data-task-id');
- const isSubtask = taskRow.classList.contains('subtask-row');
- let parentRow = null;
- if (isSubtask) {
- parentRow = document.getElementById(parentTaskId);
- }
- fetch(`/project/update-subtask-status/${taskId}/`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ status: newStatus })
- })
- .then(response => {
- if (!response.ok) {
- throw new Error('Failed to update task status.');
- }
- return response.json();
- })
- .then(data => {
- checkbox.checked = !checkbox.checked;
- // Update task's status attributes
- taskRow.setAttribute('data-status', newStatus);
- taskRow.setAttribute('value', newStatus);
- if (filtered === "pending") {
- if (checkbox.checked) {
- taskRow.setAttribute('hidden', 'true');
- // If it's a subtask, handle parent task visibility
- if (isSubtask && parentRow) {
- let allSubtasksComplete = true;
- const siblingSubtasks = document.querySelectorAll(`.subtask-row[data-task-id="${parentTaskId}"]`);
- siblingSubtasks.forEach(subtask => {
- if (subtask.getAttribute('value') !== 'C') {
- allSubtasksComplete = false;
- }
- });
- if (allSubtasksComplete) {
- parentRow.setAttribute('hidden', 'true');
- }
- }
- }
- } else if (filtered === "all") {
- // In "all" view, never hide the task
- taskRow.removeAttribute('hidden');
- // If it's a parent task, ensure subtasks visibility is maintained
- if (!isSubtask) {
- const subtasks = document.querySelectorAll(`.subtask-row[data-task-id="${taskId}"]`);
- subtasks.forEach(subtask => {
- if (taskRow.dataset.open === "true") {
- subtask.removeAttribute('hidden');
- }
- });
- }
- }
- // Update progress indicators
- const progressBar = taskRow.querySelector('.status-bar-completed');
- if (progressBar) {
- progressBar.style.width = checkbox.checked ? '100%' : '0%';
- }
- const tooltip = taskRow.querySelector('.progress-tooltip');
- if (tooltip) {
- tooltip.textContent = checkbox.checked ? 'Task completed' : 'Task pending';
- }
- // Update parent task progress if this is a subtask
- if (isSubtask && parentRow) {
- updateProgress(parentRow);
- }
- })
- .catch(error => {
- console.error('Error updating task status:', error);
- alert('Failed to update task status. Please try again.');
- });
- }
- function editTask(subtaskId) {
- const taskNameSpan = document.querySelector(`#task-name-${subtaskId}`);
- // Create input field with current task name
- const inputField = document.createElement('input');
- inputField.type = 'text';
- inputField.value = taskNameSpan.textContent;
- // Replace the span with the input field
- taskNameSpan.replaceWith(inputField);
- // Focus on the input field
- inputField.focus();
- // Function to save the task name (send API call to backend)
- function saveTaskName() {
- const newTaskName = inputField.value.trim();
- if (newTaskName) {
- // Prepare data to send to the backend
- const data = {
- taskId: subtaskId,
- name: newTaskName
- };
- // Make an API call to the backend to save the new task name
- fetch(`/project/rename/${subtaskId}/`, {
- method: 'POST', // or 'PUT', depending on your backend API
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(data)
- })
- .then(response => response.json())
- .then(updatedTask => {
- // If the request was successful, replace the input with the updated task name
- const updatedTaskNameSpan = document.createElement('span');
- updatedTaskNameSpan.classList.add('task-name');
- updatedTaskNameSpan.id = `task-name-${subtaskId}`;
- updatedTaskNameSpan.textContent = updatedTask.name; // Use updated name from backend
- inputField.replaceWith(updatedTaskNameSpan);
- })
- .catch(error => {
- console.error('Error renaming task:', error);
- // Optionally, handle errors (e.g., revert the input back to the original task name)
- inputField.replaceWith(taskNameSpan);
- });
- } else {
- // If the input field is empty, revert to the old value (optional)
- const updatedTaskNameSpan = document.createElement('span');
- updatedTaskNameSpan.classList.add('task-name');
- updatedTaskNameSpan.id = `task-name-${subtaskId}`;
- updatedTaskNameSpan.textContent = taskNameSpan.textContent; // Or previous task name
- inputField.replaceWith(updatedTaskNameSpan);
- }
- }
- // Save the task name on Enter key press
- inputField.addEventListener('keydown', function(event) {
- if (event.key === 'Enter') {
- saveTaskName();
- }
- });
- // Save the task name when the input field loses focus (blur event)
- inputField.addEventListener('blur', saveTaskName);
- }
- // Helper function to get CSRF token (if applicable)
- function getCSRFToken() {
- const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
- return csrfToken;
- }
- function updateFlag(svg) {
- const header = svg.closest('tr');
- const taskId = header.id;
- const flagPath = svg.querySelector('path');
- const currentColor = flagPath.getAttribute('fill');
- let status = "C";
- if (currentColor === '#DD1D43' || currentColor === 'grey') {
- flagPath.setAttribute('fill', 'green');
- status = "C";
- } else {
- // Currently complete, changing to incomplete
- // Check due date to determine color
- const dueDateCell = header.querySelector('.date-input');
- const dueDateValue = dueDateCell.getAttribute('value');
- const today = new Date();
- today.setHours(0, 0, 0, 0); // Set to beginning of day for comparison
- // More robust date parsing
- if (dueDateValue) {
- const dueDate = new Date(dueDateValue);
- if (!isNaN(dueDate.getTime()) && dueDate > today) {
- flagPath.setAttribute('fill', 'grey'); // Future due date
- } else {
- flagPath.setAttribute('fill', '#DD1D43'); // Past due date
- }
- } else {
- flagPath.setAttribute('fill', '#DD1D43'); // No due date, default to red
- }
- status = "I";
- }
- fetch(`/project/update-subtask-status/${taskId}/`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ status: status }) // Send the new status
- })
- .then(response => {
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
- return response.json(); // Return the response as JSON
- })
- .then(data => {
- console.log('Success:', data); // Handle the success response if needed
- })
- .catch((error) => {
- console.error('Error updating task status:', error);
- });
- }
- /* helper funcs */
- function openClickUpModal() {
- event.preventDefault();
- document.getElementById('clickupModal').style.display = 'block';
- }
- function closeClickupModal(event) {
- event.preventDefault();
- const form = document.getElementById('clickupForm')
- form.reset();
- document.getElementById('clickupModal').style.display = 'none';
- }
- function selectPMFolder(event) {
- event.preventDefault(); // Prevent form submission
- // Retrieve the selected project ID
- const selectedProjectInput = document.querySelector('input[name="existing-project"]:checked');
- if (!selectedProjectInput) {
- alert("Please select a project to sync.");
- return; // Exit the function if no project is selected
- }
- const selectedProjectName = selectedProjectInput.nextSibling.nodeValue.trim(); // This retrieves the project name
- // Create the ClickUp mapping object
- const clickupMapping = {
- project_id: projectId,
- project_name: selectedProjectName
- };
- // Call your function to create the mapping (replace with actual implementation)
- createClickUpMapping(clickupMapping);
- // Optionally, close the modal after selection
- closeClickupModal(event);
- }
- function createClickUpMapping(mapping) {
- fetch('/project/create_clickup_mapping/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(mapping),
- })
- .then(response => response.json())
- .then(data => {
- console.log('Success:', data);
- })
- .catch((error) => {
- console.error('Error:', error);
- });
- }
- //function for dd/mm/yyyy date format
- function formatDateToDDMMYYYY(dateString) {
- const [year, month, day] = dateString.split('-');
- return `${day}/${month}/${year}`;
- }
- function formatDateToDDMMYYYY_day(dateString) {
- if (dateString != ''){
- const [month, day,year] = dateString.split('/');
- return `${day}/${month}/${year}`;
- }
- return "Not Set"
- }
- function formatDateToYYYYMMDD(dateString) {
- const [day, month, year] = dateString.split('/');
- return `${year}-${month}-${day}`;
- }
- //function for edit project key detials
- function editMode() {
- document.getElementById('view-mode').style.display = 'none';
- document.getElementById('edit-mode').style.display = 'flex';
- }
- //function for cancel edit project key detials
- function cancelEdit() {
- document.getElementById('edit-mode').style.display = 'none';
- document.getElementById('view-mode').style.display = 'flex';
- }
- ////function for save project key detials
- function saveChanges() {
- const clientNames = Array.from(document.querySelectorAll('input[name="client-name"]'))
- .map(input => input.value.trim())
- .filter(value => value);
- const clientEmails = Array.from(document.querySelectorAll('input[name="client-email"]'))
- .map(input => input.value.trim())
- .filter(value => value);
- const clientContact = Array.from(document.querySelectorAll('input[name="client-mobile"]'))
- .map(input => input.value.trim())
- .filter(value => value);
- const projectName = document.getElementById('project-name').value;
- const startDate = document.getElementById('start-date').value;
- const endDate = document.getElementById('tentative-end-date').value;
- const companyName = document.getElementById('company-name').value;
- //const clientName = document.getElementById('client-name').value;
- //const email = document.getElementById('email').value;
- // const mobile = document.getElementById('mobile').value;
- const address = document.getElementById('address').value;
- const gstin = document.getElementById('gstin').value;
- const pan = document.getElementById('pan').value;
- const proposal_doc = document.getElementById('proposal_doc').value;
- // const milestone = document.getElementById('milestone-container').value;
- let data = {
- project_id: projectId,
- project_name: projectName,
- start_date: startDate,
- end_date: endDate,
- company_name: companyName,
- client_name: clientNames,
- email: clientEmails,
- mobile: clientContact,
- address: address,
- gstin: gstin,
- pan: pan,
- proposal_doc: proposal_doc,
- milestone: milestoneStatuses
- }
- // Send data to backend
- fetch('/project/update_details/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(data)
- })
- // .then(console.log(milestoneStatuses))
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- cancelEdit();
- window.location.reload();
- alert('Project details updated successfully!');
- // Call to tasks/update/ after successful update
- } else {
- alert('Failed to update project details. ' + data.message);
- }
- });
- }
- //function for apllying color dynamically for due date
- function applyDueDateColors() {
- // select all cells with the class due-date-cell
- const dueDateCells = document.querySelectorAll('.due-date-cell');
- // iterate over each cell and apply the apropriate class based on the text content
- dueDateCells.forEach(cell => {
- const text = cell.textContent.trim();
- if (text === 'Tomorrow') {
- cell.classList.add('due-date-tomorrow');
- } else if (text.includes('days ago')) {
- cell.classList.add('due-date-days-ago');
- } else if (text === 'No due date') {
- cell.classList.add('due-date-no-date');
- }
- });
- }
- // function to apply style internship days left and dynamically add popup
- function applyInternshipEndStyles(daysLeftData) {
- daysLeftData.forEach(({ rowId, daysLeft }) => {
- const row = document.getElementById(rowId);
- if (row) {
- const teamMemberNameElement = row.querySelector('.team-member-name');
- if (daysLeft <= 30) {
- teamMemberNameElement.style.backgroundColor = '#FFBEC0';
- teamMemberNameElement.style.color = '#A90420';
- teamMemberNameElement.style.cursor = 'pointer';
- let popupElement = teamMemberNameElement.querySelector('.team-member-alert-internship-end-popup');
- if (!popupElement) {
- popupElement = document.createElement('div');
- popupElement.className = 'team-member-alert-internship-end-popup';
- teamMemberNameElement.appendChild(popupElement);
- }
- popupElement.textContent = `${daysLeft} days remaining`;
- }
- }
- });
- }
- function populateSelectOptions() {
- const selectElements = document.querySelectorAll('.searchable-select');
- selectElements.forEach(select => {
- select.innerHTML = '<option value="">Select Name</option>';
- teamMembers.forEach(member => {
- const option = document.createElement('option');
- option.value = member.id;
- option.textContent = member.username;
- select.appendChild(option);
- });
- $(select).select2();
- });
- }
- function updateMemberDetails(selectElement) {
- const selectedId = parseInt(selectElement.value, 10);
- const row = selectElement.closest('tr');
- const roleColumn = row.querySelector('#role-column');
- const availabilityColumn = row.querySelector('#availability-column');
- const contactColumn = row.querySelector('#contact-column');
- const member = replacementMembers.find(member => member.id === selectedId);
- if (member) {
- availabilityColumn.textContent = member.availability;
- contactColumn.innerHTML = `
- <div>Email: ${member.email_address}</div>
- <div>Mob: ${member.phone_number}</div>
- `;
- } else {
- roleColumn.textContent = 'Role';
- availabilityColumn.textContent = 'Availability';
- contactColumn.innerHTML = `
- <div>Email: </div>
- <div>Mob: </div>
- `;
- }
- }
- document.querySelectorAll('.display-milestone').forEach(milestoneElement => {
- const milestoneId = milestoneElement.id;
- const status = milestoneStatuses[milestoneId].status || 'none';
- updateMilestoneColor(milestoneElement, status);
- });
- async function fetchDaysLeftData(projectId) {
- try {
- const response = await fetch(`/project/days_left_data/${projectId}/`);
- const data = await response.json();
- if (data.errors && data.errors.length > 0) {
- console.log('Errors occurred while fetching days left:', data.errors);
- }
- daysLeftData = data.days_left_data;
- applyInternshipEndStyles(daysLeftData);
- } catch (error) {
- console.error('Error fetching days left data:', error);
- }
- }
- async function updateAlertData(projectId) {
- try {
- // Fetch the alert data from the backend
- const response = await fetch(`/project/alert_data/${projectId}/`);
- const data = await response.json();
- if (data.errors && data.errors.length > 0) {
- console.log('Errors occurred while fetching alerts:', data.errors);
- }
- alert_data = data.alerts;
- alert_data.forEach(alert => {
- const memberRow = document.getElementById(`${alert.rowId}`);
- const alertCell = memberRow.querySelector(".team-member-alert");
- // Build the alert message based on the alert data
- let alertMessage = '';
- if (alert.githubPushed.length > 0) {
- alertMessage +=
- ` <span class="team-member-alert-no-github-push" id="git_person">
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M16 3.66667V7.66667C16 8.21933 15.552 8.66667 15 8.66667C14.448 8.66667 14 8.21933 14 7.66667V3.66667C14 2.748 13.2527 2 12.3333 2H3.66667C2.748 2 2 2.748 2 3.66667V12.3333C2 13.252 2.748 14 3.66667 14C4.21933 14 4.66667 14.4473 4.66667 15C4.66667 15.5527 4.21933 16 3.66667 16C1.64467 16 0 14.3553 0 12.3333V3.66667C0 1.64467 1.64467 0 3.66667 0H12.3333C14.3547 0 16 1.64467 16 3.66667ZM15.824 13.9907C16.0567 14.4147 16.058 14.9133 15.832 15.3313C15.606 15.7493 15.2013 16 14.748 16H7.252C6.79933 16 6.394 15.75 6.16867 15.3313C5.94267 14.9127 5.94467 14.414 6.17267 13.9967L9.924 7.32067C10.148 6.91333 10.5513 6.66667 11.0007 6.66667C11.45 6.66667 11.8533 6.91333 12.08 7.32733L15.824 13.9907ZM11.9993 13.6667C11.9993 13.1147 11.5513 12.6667 10.9993 12.6667C10.4473 12.6667 9.99933 13.1147 9.99933 13.6667C9.99933 14.2187 10.4473 14.6667 10.9993 14.6667C11.5513 14.6667 11.9993 14.2187 11.9993 13.6667ZM11.9993 9.66667C11.9993 9.114 11.5513 8.66667 10.9993 8.66667C10.4473 8.66667 9.99933 9.114 9.99933 9.66667C9.99933 10.2193 10.4473 10.6667 10.9993 10.6667C11.5513 10.6667 11.9993 10.2193 11.9993 9.66667Z" fill="#DD1D43"/>
- </svg>
- <div class="team-member-alert-no-github-push-popup">No GitHub pushes: </br> ${alert.githubPushed.join(', ')}</div>
- </span>`;
- }
- if (alert.isAbsentToday) {
- alertMessage +=
- `<span class="team-member-alert-absent">
- <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="15" viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
- <g>
- <circle style="fill:#CC3333;" cx="63.93" cy="64" r="60"/>
- <circle style="fill:#F44336;" cx="60.03" cy="63.1" r="56.1"/>
- <path style="fill:#FF8A80;" d="M23.93,29.7c4.5-7.1,14.1-13,24.1-14.8c2.5-0.4,5-0.6,7.1,0.2c1.6,0.6,2.9,2.1,2,3.8
- c-0.7,1.4-2.6,2-4.1,2.5c-9.38,3.1-17.47,9.21-23,17.4c-2,3-5,11.3-8.7,9.2C17.43,45.7,18.23,38.5,23.93,29.7z"/>
- </g>
- </svg>
- <div class="team-member-alert-absent-popup">Absent</div>
- </span>
- `;
- }
- if (alert.upcomingLeaves) {
- alertMessage +=
- `<span class="team-member-alert-leave">
- <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="15" viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
- <g>
- <circle style="fill:#F2A600;" cx="63.93" cy="64" r="60"/>
- <circle style="fill:#FFCC32;" cx="60.03" cy="63.1" r="56.1"/>
- <path style="fill:#FFF170;" d="M23.93,29.7c4.5-7.1,14.1-13,24.1-14.8c2.5-0.4,5-0.6,7.1,0.2c1.6,0.6,2.9,2.1,2,3.8
- c-0.7,1.4-2.6,2-4.1,2.5c-9.38,3.1-17.47,9.21-23,17.4c-2,3-5,11.3-8.7,9.2C17.43,45.7,18.23,38.5,23.93,29.7z"/>
- </g>
- </svg>
- <div class="team-member-alert-leave-popup">${ alert.totalDays } upcoming leaves <br> ${ alert.leaveDates }</div>
- </span>`
- }
- // Update the alert cell with the constructed message
- alertCell.innerHTML = alertMessage;
- });
- } catch (error) {
- console.error('Failed to fetch alert data:', error);
- }
- }
- applyDueDateColors();
- function enforceTableCellHeight(height) {
- const rows = document.querySelectorAll(".task-table tr");
- const cells = document.querySelectorAll(".task-table th, .task-table td");
- const rowAdd = document.querySelectorAll('.add-task-rows');
- const AddCell = document.querySelectorAll('.add-task-rows td');
- // Apply height to all rows
- rows.forEach(row => {
- row.style.height = `${height}px`;
- row.style.paddingTop = "0px";
- row.style.paddingBottom = "0px";
- row.style.justifyItems = "center";
- row.style.justifyContent= "center";
- });
- rowAdd.forEach(row => {
- row.style.height = `35px`;
- row.style.paddingTop = "2px";
- row.style.paddingBottom = "2px";
- });
- cells.forEach(cell => {
- cell.style.height = `${height}px`;
- cell.style.lineHeight = `${height}px`; // Centers text vertically
- cell.style.verticalAlign = "middle"; // Ensures vertical alignment
- cell.style.margin = "0px";
- cell.style.paddingTop = "0px";
- cell.style.paddingBottom = "0px";
- });
- AddCell.forEach(cell => {
- cell.style.height = `35px`;
- cell.style.lineHeight = `35px`;
- cell.style.verticalAlign = "middle";
- cell.style.justifyContent = "center";
- cell.style.paddingTop = "8px";
- cell.style.paddingBottom = "8px";
- });
- }
- function openCreateDocumentForm() {
- document.getElementById("create-document-modal").style.display = "flex";
- }
- function closeCreateDocumentForm() {
- document.getElementById("create-document-modal").style.display = "none";
- }
- //submit created documnet
- async function submitCreateDocumentForm() {
- const title = document.getElementById('createDocumentName').value;
- const link = document.getElementById('createDocumentLink').value;
- const type = document.getElementById('documentType').value;
- const permissions = document.getElementById('documentPermissions').value;
- if (!title || !link || !type || !permissions) {
- alert('Please fill in all fields.');
- return;
- }
- const data = {
- title,
- link,
- type,
- permissions,
- project_id: projectId
- };
- alert('Document created successfully');
- try {
- const response = await fetch('/project/create-document/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': '{{ csrf_token }}',
- },
- body: JSON.stringify(data)
- });
- const result = await response.json();
- alert('Document created successfully again');
- if (result.status === 'success') {
- closeCreateDocumentForm();
- saveNewLinks_2_with_parameter(title, link);
- alert("Document created successfully");
- } else {
- alert('Error: ' + (result.message || 'Unknown error'));
- }
- } catch (error) {
- console.error('Error:', error);
- alert('Error creating document: ' + error.message);
- }
- }
- // Close modal when clicking outside of it
- window.onclick = function(event) {
- const modal = document.getElementById("create-document-modal");
- if (event.target === modal) {
- modal.style.display = "none";
- }
- }
- // Function to trigger the file input click
- function triggerFileUpload() {
- print('inside trigger');
- document.getElementById('fileInput').click();
- }
- // Function to handle the file upload process
- async function handleFileUpload(event) {
- const fileInput = event.target;
- const file = fileInput.files[0]; // Get the first file selected
- if (!file) {
- console.log('No file selected.');
- return;
- }
- // Log the uploading status
- console.log('Uploading file...');
- // Create a FormData object to send the file data
- const formData = new FormData();
- formData.append('file', file); // Append the file to FormData
- formData.append('projectId', projectId); // Add project_document_id if needed
- // Use Fetch API to send the file to the server
- await fetch('/project/upload-project-docs/', {
- method: 'POST',
- body: formData,
- headers: {
- 'X-CSRFToken': '{{ csrf_token }}', // CSRF token for security
- }
- })
- .then(response => response.json())
- .then(data => {
- if (data.success) {
- console.log('message',data.message,'file_url',data.file_url);
- alert('file upload successful');
- saveNewLinks_2_with_parameter(data.file_name,data.file_url);
- } else {
- console.log('message',data.message,'error',data.error);
- }
- })
- .catch(error => {
- console.error('Fetch Error:', error);
- });
- }
- async function get_title() {
- const url = document.getElementById('md-url').value;
- if (!url) {
- alert('No URL provided. Please insert a valid URL.');
- return;
- }
- const formData = {
- url: url,
- projectId: projectId, // Make sure `projectId` is defined somewhere in your code
- };
- await fetch('/project/get-file-name/', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': '{{ csrf_token }}', // Ensure CSRF token is correctly passed
- },
- body: JSON.stringify(formData)
- })
- .then(response => response.json())
- .then(data => {
- console.log("Data received from server:", data);
- if (data.status === 'success') {
- console.log('Success!');
- alert("FIle Name Found");
- if(!data.title){
- console.error("File name error");
- }
- saveNewLinks_2_with_parameter(data.title,url);
- } else {
- console.log('Error response:', data);
- alert('Error: ' + (data.message || 'Unknown error'));
- }
- })
- .catch(error => {
- console.log(error);
- console.error('Error:', error);
- alert('An error occurred while Getting the name of the document: ' + error.message);
- });
- }
- function saveNewLinks_2_with_parameter(linkName,linkUrl) {
- console.log("saveNewLinks_2_with_parameter called");
- const documentList = document.getElementById('document-list');
- if (linkName && linkUrl) {
- // Create a new link element
- console.log("valid linkname and linkurl");
- const newLinkDiv = document.createElement('div');
- newLinkDiv.style.display = 'flex';
- newLinkDiv.style.justifyContent = 'space-between';
- const newLink = document.createElement('a');
- newLink.href = linkUrl;
- newLink.style.textDecoration = 'underline';
- newLink.style.color = '#2F6CE5';
- newLink.target = '_blank';
- newLink.textContent = linkName;
- const timeAgo = document.createElement('div');
- timeAgo.style.color = '#939CA3';
- timeAgo.textContent = 'Just now';
- // Append new elements to the parent div
- newLinkDiv.appendChild(newLink);
- newLinkDiv.appendChild(timeAgo);
- // Append the new link div to the document list
- documentList.appendChild(newLinkDiv);
- }
- document.getElementById('md-url').value="";
- window.location.reload();
- };
- document.addEventListener('DOMContentLoaded', () => {
- sortNotifications('github-miss','repo');
- try {
- enforceTableCellHeight(35);
- fetchDaysLeftData(projectId);
- updateAlertData(projectId);
- const emailIcons = document.querySelectorAll('.email-icon');
- emailIcons.forEach(icon => {
- icon.addEventListener('click', function () {
- const templateId = this.getAttribute('data-template');
- const modal = document.getElementById(templateId);
- if (modal) {
- modal.style.display = 'block';
- }
- });
- });
- populateSelectOptions();
- const milestoneNames = document.querySelectorAll('.milestone-name');
- milestoneNames.forEach(name => {
- const start = name.getAttribute('data-start');
- const end = name.getAttribute('data-end');
- // Format the dates
- const formattedStart = formatDateToDDMMYYYY_day(start);
- const formattedEnd = formatDateToDDMMYYYY_day(end);
- // Update the title attribute for hover display
- name.setAttribute('title', `Start Date: ${formattedStart}\nEnd Date: ${formattedEnd}`);
- });
- const taskRows = document.querySelectorAll('.task-row');
- taskRows.forEach(row => {
- updateProgress(row);
- });
- const ownerSelect = document.getElementById('owner');
- const orgSelect = document.getElementById('organization');
- // Event listener for the owner dropdown
- ownerSelect.addEventListener('change', function () {
- const selectedOwner = ownerSelect.value;
- // Clear the organization dropdown
- orgSelect.innerHTML = '<option value="">Select Organization (if applicable)</option>';
- if (selectedOwner) {
- fetch(`/project/get_user_organizations/?owner=${selectedOwner}`) // Adjust the URL as needed
- .then(response => response.json())
- .then(data => {
- if (data.organizations) {
- data.organizations.forEach(org => {
- orgSelect.innerHTML += `<option value="${org.login}">${org.login}</option>`;
- });
- }
- })
- .catch(error => console.error('Error fetching organizations:', error));
- }
- });
- } catch (error) {
- console.error('Error:', error);
- }
- (function () {
- // toggle task Element
- function toggleTaskElement() {
- const taskElement = document.querySelectorAll('.task-dashboard-content');
- const icons = document.querySelectorAll('.up-icon');
- icons.forEach((icon, index) => {
- icon.addEventListener('click', function () {
- const taskEle = taskElement[index];
- if (taskEle.style.display === 'flex' || taskEle.style.display === '') {
- taskEle.style.display = 'none';
- icon.innerHTML = `<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M1 1L7 7L13 1" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- } else {
- taskEle.style.display = 'flex';
- icon.innerHTML = `<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M13 7L7 1L1 7" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- }
- });
- });
- }
- //toggle team element
- function toggleTeamElement() {
- const teamElement = document.querySelectorAll('.team-member-content');
- const icons = document.querySelectorAll('.up-team-member-icon');
- icons.forEach((icon, index) => {
- icon.addEventListener('click', function () {
- const teamEle = teamElement[index];
- if (teamEle.style.display === 'block') {
- teamEle.style.display = 'none';
- icon.innerHTML = `<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M1 1L7 7L13 1" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- } else {
- teamEle.style.display = 'block';
- icon.innerHTML = `<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M13 7L7 1L1 7" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- }
- });
- });
- }
- // toggle budget elment
- function toggleBudgetElement() {
- const budgetElement = document.querySelectorAll('.dashboard-budget-group');
- const icons = document.querySelectorAll('.up-budget-icon');
- icons.forEach((icon, index) => {
- icon.addEventListener('click', function () {
- const budgetEle = budgetElement[index];
- if (budgetEle.style.display === 'block') {
- budgetEle.style.display = 'none';
- icon.innerHTML = `<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M1 1L7 7L13 1" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- } else {
- budgetEle.style.display = 'block';
- icon.innerHTML = `<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M13 7L7 1L1 7" stroke="#6D747A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- }
- });
- });
- }
- //
- toggleTaskElement();
- toggleTeamElement();
- toggleBudgetElement();
- })();
- // Function to fetch repositories for each member
- members.forEach(member => {
- fetch(`/project/get_user_repo/${member.id}/${projectId}/`) // Adjust the API endpoint as needed
- .then(response => response.json())
- .then(data => {
- const tooltip = document.getElementById(`tooltip-${member.id}`);
- const repoIcon = document.getElementById(`repo-icon-${member.id}`);
- if (data.length > 0) {
- // Show the icon and populate the tooltip with repository names
- repoIcon.style.display = 'inline-block';
- const repoNames = data.map(repo => repo.name).join(', ');
- tooltip.innerHTML = repoNames;
- } else {
- repoIcon.style.display = 'none'; // Hide the icon if no repos
- }
- })
- .catch(error => console.error('Error fetching repositories:', error));
- });
- const categories = [
- { id: 'tasks-category', triggers: ['task_overdue', 'task_due_today'] },
- { id: 'github-miss-category', triggers: ['git_person'] },
- { id: 'notice-period-category', triggers: ['person_notice'] },
- { id: 'informational-category', triggers: [] }
- ];
- // Loop through each category and check if it has relevant notifications
- categories.forEach(category => {
- const categoryElement = document.getElementById(category.id);
- if (categoryElement) {
- const notifContainers = categoryElement.querySelectorAll('.notif-container');
- let shouldExpand = false;
- notifContainers.forEach(container => {
- const trigger = container.dataset.trigger;
- if (category.triggers.length === 0 || category.triggers.includes(trigger)) {
- shouldExpand = true;
- }
- });
- if (shouldExpand) {
- categoryElement.classList.add('expanded'); // Add 'expanded' class to the category
- }
- }
- });
- const tasks = {{ tasks_json |safe }};
- const milestone_statuses = {{ milestone_statuses|safe }};
- const noMilestonesMessage = document.getElementById("noMilestonesMessage");
- const noTasksMessage = document.getElementById("noTasksMessage");
- const taskFilterContainer = document.getElementById("taskFilterContainer");
- let milestonesInProgress = false;
- let tasksExist = false;
- // Loop through milestones to check for "on-progress" status
- for (const [key, value] of Object.entries(milestone_statuses)) {
- if (value.status === "on-progress") {
- milestonesInProgress = true;
- const milestoneDiv = document.createElement("div");
- milestoneDiv.innerHTML = `<h4>Milestone: ${key}</h4>`;
- const milestoneTasks = tasks.filter(task => task.milestone == key);
- if (milestoneTasks.length > 0) {
- tasksExist = true;
- milestoneTasks.forEach(task => {
- const taskDiv = document.createElement("div");
- taskDiv.textContent = `Task: ${task.name}`;
- milestoneDiv.appendChild(taskDiv);
- });
- }
- }
- }
- // Show messages based on conditions
- if (!milestonesInProgress) {
- taskFilterContainer.style.display = "none";
- noMilestonesMessage.style.display = "block";
- } else if (!tasksExist) {
- taskFilterContainer.style.display = "none";
- noTasksMessage.style.display = "block";
- }
- });
- function openDelayModal() {
- document.getElementById("delayModal").style.display = "flex";
- updateSecondDropdown();
- updateDelayText();
- }
- document.getElementById("delayModal").addEventListener("click", function(e) {
- if (e.target === this) {
- this.style.display = "none";
- }
- });
- document.getElementById("daysInput").addEventListener("input", updateDelayText);
- document.getElementById("typeSelect").addEventListener("change", updateDelayText);
- function updateDelayText() {
- const type = document.getElementById("typeSelect").value;
- const days = document.getElementById("daysInput").value;
- let label = type.charAt(0).toUpperCase() + type.slice(1);
- document.getElementById("delayText").innerText = `The ${label} is delayed by ${days} days.`;
- }
- async function updateSecondDropdown() {
- const typeSelect = document.getElementById("typeSelect").value;
- const secondSelect = document.getElementById("secondSelect");
- secondSelect.innerHTML = "";
- if (typeSelect === "project") {
- secondSelect.disabled = true;
- secondSelect.innerHTML = `<option value="${projectId}">β</option>`;
- return;
- }
- secondSelect.disabled = false;
- secondSelect.innerHTML = `<option>Loading...</option>`;
- try {
- const res = await fetch(`/project/api/get-items/?type=${typeSelect}&project_id=${projectId}`);
- if (!res.ok) throw new Error("API request failed");
- const items = await res.json();
- secondSelect.innerHTML = "";
- if (!Array.isArray(items) || items.length === 0) {
- secondSelect.innerHTML = `<option>No items found</option>`;
- return;
- }
- items.forEach(item => {
- const opt = document.createElement("option");
- opt.value = item.id;
- opt.text = item.name;
- secondSelect.add(opt);
- });
- } catch (err) {
- console.error(err);
- secondSelect.innerHTML = `<option>Error loading data</option>`;
- }
- }
- async function submitDelay() {
- const delay_type = document.getElementById("typeSelect").value;
- let item_id = document.getElementById("secondSelect").value;
- const days = parseInt(document.getElementById("daysInput").value, 10);
- const reason = document.getElementById("reasonInput").value;
- if (!delay_type || !item_id || isNaN(days) || !reason.trim()) {
- alert("Please fill in all fields.");
- return;
- }
- if (delay_type !== "project") {
- item_id = parseInt(item_id, 10);
- if (isNaN(item_id)) {
- alert("Please select a valid item.");
- return;
- }
- }
- console.log({
- delay_type,
- item_id,
- days,
- reason
- });
- try {
- const res = await fetch("/project/api/add-delay/", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "X-CSRFToken": "{{ csrf_token }}"
- },
- credentials: "include",
- body: JSON.stringify({ delay_type, item_id, days, reason })
- });
- let data;
- try {
- data = await res.json();
- } catch (e) {
- // If JSON parsing fails, show generic error
- alert("Server error: Unable to process response");
- return;
- }
- // Check if request was successful
- if (res.ok) {
- // Success case
- alert(data.message);
- document.getElementById("delayModal").style.display = "none";
- } else {
- // Error case - display the specific error message from Django
- const errorMessage = data.error || data.message || "Failed to add delay.";
- alert(errorMessage);
- }
- } catch (err) {
- console.error(err);
- alert("Network error: Unable to connect to server");
- }
- }
- // window.onload=()=>{
- // document.querySelector('.sort-date').click()
- // }
- </script>
- </body>
Advertisement
Add Comment
Please, Sign In to add comment