diff --git a/src/my-time.css b/src/my-time.css index e2c56eb..492d7b2 100644 --- a/src/my-time.css +++ b/src/my-time.css @@ -1,5 +1,6 @@ -html, body { height: 100vh; overflow: auto; } +html, body { height: 100vh; overflow: hidden; } body { padding: 20px; display: flex; flex-direction: column; } +.page-scroll { flex: 1; min-height: 0; overflow-y: auto; } .page-header { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; flex-shrink: 0; } .page-header h1 { margin: 0; font-weight: 600; } @@ -96,8 +97,11 @@ body { padding: 20px; display: flex; flex-direction: column; } .quicklog input[type="date"] { width: 140px; } .quicklog button { padding: 5px 12px; font-size: 13px; } -.delete-btn { background-color: var(--error-border) !important; padding: 4px 10px; font-size: 12px; } -.delete-btn:hover { opacity: 0.85; } +/* week navigation header */ +.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } +.section-header .section-title { margin: 0; } +.week-nav { display: flex; gap: 4px; } +.week-nav-btn { padding: 2px 10px; font-size: 16px; line-height: 1; } .empty { padding: 32px; text-align: center; color: var(--text-secondary); } .loading { padding: 40px; text-align: center; color: var(--text-secondary); } diff --git a/src/my-time.html b/src/my-time.html index 20350fe..3b2bbca 100644 --- a/src/my-time.html +++ b/src/my-time.html @@ -30,8 +30,15 @@

My Time

+
+ +
+
Loading…
-
This Week
+
+
This Week
+
+ + +
+
@@ -94,14 +107,22 @@

Untracked This Week

+ + + diff --git a/src/notification-settings.html b/src/notification-settings.html index 73efe21..28d4c9b 100644 --- a/src/notification-settings.html +++ b/src/notification-settings.html @@ -87,9 +87,16 @@

User Notification Configuration

function applyDevOpsTheme() { function applyIfExplicit(t) { if (!t) return false; - // A non-empty theme that isn't a dark variant (e.g. "Default") means light. - document.documentElement.classList.toggle('dark', String(t).toLowerCase().indexOf('dark') !== -1); - return true; + var theme = String(t).toLowerCase(); + if (theme.indexOf('dark') !== -1) { + document.documentElement.classList.add('dark'); + return true; + } + if (theme === 'default' || theme === 'light') { + document.documentElement.classList.remove('dark'); + return true; + } + return false; } var t = ''; try { if (typeof VSS.getPageContext === 'function') t = (VSS.getPageContext().globalization.theme || '').toLowerCase(); } catch(e) {} diff --git a/src/theme.css b/src/theme.css index f39d98d..77bcaf0 100644 --- a/src/theme.css +++ b/src/theme.css @@ -241,3 +241,190 @@ tr:hover td { .status { font-size: 13px; } .status.success { color: var(--success-text); } .status.error { color: var(--error-text); } + +/* ================================================================ + Icon action buttons — shared across all pages + ================================================================ */ +.icon-btn { + background: transparent !important; + border: none; + padding: 0; + border-radius: 3px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-secondary) !important; + width: 28px; + height: 28px; + cursor: pointer; +} +.icon-btn:hover { background: var(--bg-row-hover) !important; } +.edit-btn:hover { color: var(--accent) !important; } +.delete-btn:hover { color: #d13438 !important; } +.actions-cell { white-space: nowrap; padding: 8px 12px !important; } + +/* ================================================================ + Edit entry modal — shared across all pages + ================================================================ */ +.edit-modal-backdrop { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; +} +.edit-modal-content { + background: var(--bg-card); + padding: 24px; + border-radius: 4px; + min-width: 300px; + max-width: 440px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + gap: 12px; +} +.edit-modal-content h3 { margin: 0; } +.edit-modal-content label { font-weight: 600; color: var(--text-primary) !important; font-size: 13px; } +.edit-modal-content input, +.edit-modal-content textarea { width: 100%; box-sizing: border-box; } +.edit-modal-actions { display: flex; gap: 8px; } + +/* Table links */ +td a { color: var(--accent); text-decoration: none; } +td a:hover { text-decoration: underline; } + +/* ================================================================ + Add-entry button (filter bar, far right) + ================================================================ */ +.filters-add { margin-left: auto; display: flex; align-items: flex-end; } + +.add-entry-btn { + display: inline-flex !important; + align-items: center; + gap: 6px; + background: transparent !important; + color: var(--text-secondary) !important; + border: 1px solid var(--border-input) !important; + padding: 5px 12px !important; + border-radius: 3px; + font-size: 13px; + white-space: nowrap; + cursor: pointer; +} +.add-entry-btn:hover { + color: var(--accent) !important; + border-color: var(--accent) !important; + background: var(--bg-accent-light) !important; +} + +/* ================================================================ + Add-entry modal: work item picker + ================================================================ */ +.tc-add-modal { max-width: 520px; width: 100%; } + +.tc-add-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} +.tc-add-header h3 { margin: 0; } + +.tc-modal-close { + background: transparent !important; + border: none; + color: var(--text-secondary) !important; + font-size: 18px; + line-height: 1; + padding: 2px 6px; + cursor: pointer; + border-radius: 3px; +} +.tc-modal-close:hover { background: var(--bg-row-hover) !important; color: var(--text-primary) !important; } + +.tc-search-input { width: 100%; box-sizing: border-box; } + +.tc-search-status { + font-size: 12px; + color: var(--text-secondary); + min-height: 18px; + margin: 4px 0; +} + +.tc-item-list { + border: 1px solid var(--border); + border-radius: 2px; + max-height: 180px; + overflow-y: auto; +} + +.tc-item-group { + padding: 5px 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + color: var(--text-secondary); + background: var(--bg-header); + border-bottom: 1px solid var(--border); +} + +.tc-item-row { + display: flex !important; + align-items: center; + gap: 8px; + width: 100%; + text-align: left; + background: transparent !important; + color: var(--text-primary) !important; + border: none; + border-bottom: 1px solid var(--border); + border-radius: 0 !important; + padding: 8px 12px !important; + cursor: pointer; + font-size: 13px; +} +.tc-item-row:last-child { border-bottom: none; } +.tc-item-row:hover { background: var(--bg-row-hover) !important; } + +.tc-item-type { + display: inline-block; + padding: 2px 7px; + border-radius: 10px; + font-size: 11px; + font-weight: 600; + background: var(--bg-accent-light); + color: var(--accent); + flex-shrink: 0; + white-space: nowrap; +} + +.tc-item-title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.tc-item-empty { padding: 20px 12px; color: var(--text-secondary); font-size: 13px; text-align: center; } + +/* Selected work item chip — a clickable button that reopens the search */ +.tc-selected-chip { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + text-align: left; + background: var(--bg-header) !important; + color: var(--text-primary) !important; + border: 1px solid var(--border) !important; + border-radius: 4px; + padding: 8px 12px !important; + margin-bottom: 4px; + font-size: 13px; + cursor: pointer; +} +.tc-selected-chip:hover { + border-color: var(--accent) !important; + background: var(--bg-accent-light) !important; +} +/* Pencil icon: faintly visible at rest, full opacity on hover */ +.tc-chip-pencil { margin-left: auto; flex-shrink: 0; opacity: 0.25; } +.tc-selected-chip:hover .tc-chip-pencil { opacity: 0.8; } diff --git a/src/time-core.js b/src/time-core.js index 004ef88..3f68e7f 100644 --- a/src/time-core.js +++ b/src/time-core.js @@ -1,4 +1,4 @@ -/* ================================================================ +/* ================================================================ time-core.js — shared storage + entry logic Loaded as a plain + @@ -55,11 +56,45 @@

Time Entries

+ + diff --git a/src/time-report.css b/src/time-report.css index a75b767..ca817aa 100644 --- a/src/time-report.css +++ b/src/time-report.css @@ -100,3 +100,4 @@ td { padding: 12px 16px; color: var(--text-primary); } color: var(--accent); font-weight: 600; } + diff --git a/src/time-report.html b/src/time-report.html index 5d8317b..58c85ae 100644 --- a/src/time-report.html +++ b/src/time-report.html @@ -7,6 +7,7 @@ +

Time Reports

@@ -56,6 +57,12 @@

Time Reports

+
+ +
@@ -147,6 +154,47 @@

Work Items

let filteredEntries = []; let currentView = "entries"; var pendingFilters = null; + let currentUser = null; + var collectionUri = ""; + var currentProjectName = ""; + var TC = window.TimeCore; + var EDIT_ICON = TC.EDIT_ICON; + var DELETE_ICON = TC.DELETE_ICON; + var escapeHtml = TC.escapeHtml; + var editingEntryId = null; + var editingEntryOriginalDate = null; + var _witClient = null; + + function getWitClient() { + if (_witClient) return Promise.resolve(_witClient); + return new Promise(function(resolve) { + VSS.require(['VSS/Service', 'TFS/WorkItemTracking/RestClient'], function(VSS_Service, WIT_Client) { + _witClient = VSS_Service.getCollectionClient(WIT_Client.WorkItemTrackingHttpClient); + resolve(_witClient); + }); + }); + } + + function openAddEntry() { + var seen = {}, recent = []; + // Build recent list from current user's entries, newest first. + allEntries.filter(function(e) { + return currentUser && String(e.userId) === String(currentUser.id); + }).slice().sort(function(a, b) { + return b.date > a.date ? -1 : b.date < a.date ? 1 : 0; + }).forEach(function(e) { + var k = String(e.workItemId); + if (!seen[k]) { seen[k] = true; recent.push({ id: e.workItemId, title: e.workItemTitle || '(untitled)', type: null }); } + }); + TC.openAddEntryModal({ + dataService: window.dataService, + witClientGetter: getWitClient, + currentUser: currentUser, + projectName: currentProjectName, + recentItems: recent.slice(0, 20), + onSaved: function() { loadAllEntries(); } + }); + } function parseHashString(hash) { if (!hash) return null; @@ -184,14 +232,6 @@

Work Items

}); } - // Helper function to get storage key for a given date - function getStorageKeyForDate(dateString) { - var date = new Date(dateString); - var year = date.getFullYear(); - var month = String(date.getMonth() + 1).padStart(2, '0'); - return STORAGE_KEY_PREFIX + year + "_" + month; - } - // Get all months between two dates function getMonthsBetween(startDate, endDate) { var months = []; @@ -233,6 +273,12 @@

Work Items

VSS.getService(VSS.ServiceIds.ExtensionData).then(function(dataService) { console.log("[TimeReport] Got extension data service"); window.dataService = dataService; + try { + var ctx = VSS.getWebContext(); + currentUser = ctx.user; + collectionUri = ((ctx.collection || {}).uri || "").replace(/\/$/, ""); + currentProjectName = (ctx.project || {}).name || ""; + } catch(e) {} try { dataService.setValue('user-theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light', { scopeType: 'User' }); } catch(e) {} // Set default date range (last 30 days) @@ -556,7 +602,7 @@

Work Items

var thead = document.getElementById("tableHead"); var tbody = document.getElementById("tableBody"); - thead.innerHTML = "DateUserWork ItemParentEpicTagsHoursDescription"; + thead.innerHTML = "DateUserWork ItemParentEpicTagsHoursDescription"; var sorted = [...filteredEntries].sort((a, b) => new Date(b.date) - new Date(a.date)); @@ -571,15 +617,27 @@

Work Items

parentDisplay = escapeHtml(e.parentTitle) + " (" + escapeHtml(e.parentType) + ")"; } + var canModify = currentUser && e.userId === currentUser.id; + var actionBtns = canModify + ? '' + + '' + : ''; + + var wiUrl = getWorkItemUrl(e); + var wiIdLabel = wiUrl + ? '#' + e.workItemId + '' + : '#' + e.workItemId; + return "" + "" + e.date + "" + "" + escapeHtml(e.userName) + "" + - "" + escapeHtml(e.workItemTitle) + " (#" + e.workItemId + ")" + + "" + escapeHtml(e.workItemTitle) + " (" + wiIdLabel + ")" + "" + parentDisplay + "" + "" + (e.epicTitle ? escapeHtml(e.epicTitle) : "-") + "" + "" + tags + "" + "" + e.hours + "h" + "" + (e.description ? escapeHtml(e.description) : "-") + "" + + "" + actionBtns + "" + ""; }).join(""); } @@ -1099,12 +1157,87 @@

Work Items

link.click(); } - function escapeHtml(text) { - if (!text) return ""; - var div = document.createElement("div"); - div.textContent = text; - return div.innerHTML; + function getWorkItemUrl(entry) { + var base = collectionUri.replace(/\/$/, ""); + var proj = entry.teamProject || currentProjectName || ""; + if (!base || !proj) return null; + return base + "/" + encodeURIComponent(proj) + "/_workitems/edit/" + entry.workItemId; + } + + function deleteEntry(entryId) { + if (!confirm("Delete this time entry?")) return; + var entry = allEntries.find(function(e) { return e.id === entryId; }); + if (!entry) { alert("Entry not found"); return; } + if (!currentUser || entry.userId !== currentUser.id) { alert("You can only delete your own entries"); return; } + TC.deleteEntryById(window.dataService, entryId, entry.date).then(function() { + loadAllEntries(); + }, function() { + alert("Failed to delete entry"); + }); + } + + function editEntry(entryId) { + var entry = allEntries.find(function(e) { return e.id === entryId; }); + if (!entry) { alert("Entry not found"); return; } + if (!currentUser || entry.userId !== currentUser.id) { alert("You can only edit your own entries"); return; } + editingEntryId = entryId; + editingEntryOriginalDate = entry.date; + document.getElementById("editHours").value = entry.hours; + document.getElementById("editDate").value = entry.date; + document.getElementById("editDescription").value = entry.description || ""; + document.getElementById("editModal").style.display = "flex"; + } + + function closeEditModal() { + document.getElementById("editModal").style.display = "none"; + editingEntryId = null; + editingEntryOriginalDate = null; + } + + function saveEditEntry() { + var hours = parseFloat(document.getElementById("editHours").value); + var date = document.getElementById("editDate").value; + var description = document.getElementById("editDescription").value; + if (!hours || hours <= 0 || hours > 24) { + alert("Please enter valid hours (0.25 - 24)"); + return; + } + if (!date) { + alert("Please select a date"); + return; + } + document.getElementById("editSaveBtn").disabled = true; + TC.updateEntry(window.dataService, editingEntryId, editingEntryOriginalDate, { hours: hours, date: date, description: description }) + .then(function() { + closeEditModal(); + loadAllEntries(); + }, function() { + alert("Failed to update entry"); + document.getElementById("editSaveBtn").disabled = false; + }); } + + diff --git a/vss-extension.dev.json b/vss-extension.dev.json index 7c3aa93..a72eb57 100644 --- a/vss-extension.dev.json +++ b/vss-extension.dev.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/vss-extension", "manifestVersion": 1, "id": "timetracker-extension-dev", - "version": "1.5.5", + "version": "1.5.14", "name": "Time Tracker (DEV)", "description": "[DEVELOPMENT] Simple time tracking for user stories with reporting by user, epic, tag and date range", "publisher": "miguelnicolas", diff --git a/vss-extension.json b/vss-extension.json index bef6f79..06208b2 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/vss-extension", "manifestVersion": 1, "id": "timetracker-extension", - "version": "1.6.1", + "version": "1.6.2", "name": "Time Tracker", "description": "Simple time tracking for user stories with reporting by user, epic, tag and date range", "publisher": "miguelnicolas",