From 88c951630891e7ac7c330c64880980f9184459a7 Mon Sep 17 00:00:00 2001 From: mohamad Date: Sun, 8 Jun 2025 02:03:38 +0200 Subject: [PATCH] feat: Enhance GroupDetailPage with chore assignments and history This update introduces significant improvements to the GroupDetailPage, including: - Added detailed modals for chore assignments and history. - Implemented loading states for assignments and chore history. - Enhanced chore display with status indicators for overdue and due-today. - Improved UI with new styles for chore items and assignment details. These changes enhance user experience by providing more context and information about group chores and their assignments. --- fe/src/assets/valerie-ui.scss | 14 +- fe/src/pages/GroupDetailPage.vue | 1206 +++++++++++++++++++++++------- 2 files changed, 932 insertions(+), 288 deletions(-) diff --git a/fe/src/assets/valerie-ui.scss b/fe/src/assets/valerie-ui.scss index f3d916b..95455cf 100644 --- a/fe/src/assets/valerie-ui.scss +++ b/fe/src/assets/valerie-ui.scss @@ -917,11 +917,13 @@ select.form-input { .modal-backdrop { position: fixed; inset: 0; - background-color: rgba(57, 62, 70, 0.7); + background-color: rgba(57, 62, 70, 0.9); + /* Increased opacity for better visibility */ display: flex; align-items: center; justify-content: center; - z-index: 1000; + z-index: 9999; + /* Increased z-index to ensure it's above other elements */ opacity: 0; visibility: hidden; transition: @@ -941,16 +943,18 @@ select.form-input { background-color: var(--light); border: var(--border); width: 90%; - max-width: 550px; + max-width: 850px; box-shadow: var(--shadow-lg); position: relative; - overflow-y: scroll; - /* Can cause tooltip clipping */ + overflow-y: auto; + /* Changed from scroll to auto */ transform: scale(0.95) translateY(-20px); transition: transform var(--transition-speed) var(--transition-ease-out); max-height: 90vh; display: flex; flex-direction: column; + z-index: 10000; + /* Ensure modal content is above backdrop */ } .modal-container::before { diff --git a/fe/src/pages/GroupDetailPage.vue b/fe/src/pages/GroupDetailPage.vue index 8d4aab6..7d4eb8f 100644 --- a/fe/src/pages/GroupDetailPage.vue +++ b/fe/src/pages/GroupDetailPage.vue @@ -1,300 +1,470 @@ @@ -308,7 +478,7 @@ import ListsPage from './ListsPage.vue'; // Import ListsPage import { useNotificationStore } from '@/stores/notifications'; import { choreService } from '../services/choreService' import type { Chore, ChoreFrequency, ChoreAssignment, ChoreHistory, ChoreAssignmentHistory } from '../types/chore' -import { format } from 'date-fns' +import { format, formatDistanceToNow, parseISO, startOfDay, isEqual, isToday as isTodayDate } from 'date-fns' import type { Expense, ExpenseSplit, SettlementActivityCreate } from '@/types/expense'; import { ExpenseOverallStatusEnum, ExpenseSplitStatusEnum } from '@/types/expense'; import { useAuthStore } from '@/stores/auth'; @@ -326,6 +496,7 @@ import VInput from '@/components/valerie/VInput.vue'; import VFormField from '@/components/valerie/VFormField.vue'; import VIcon from '@/components/valerie/VIcon.vue'; import VModal from '@/components/valerie/VModal.vue'; +import VSelect from '@/components/valerie/VSelect.vue'; import { onClickOutside } from '@vueuse/core' import { groupService } from '../services/groupService'; // New service @@ -425,6 +596,9 @@ const generatingSchedule = ref(false); const groupChoreHistory = ref([]); const groupHistoryLoading = ref(false); +const loadingAssignments = ref(false); +const selectedChoreAssignments = ref([]); + const getApiErrorMessage = (err: unknown, fallbackMessageKey: string): string => { if (err && typeof err === 'object') { if ('response' in err && err.response && typeof err.response === 'object' && 'data' in err.response && err.response.data) { @@ -618,6 +792,30 @@ const formatDate = (date: string) => { return format(new Date(date), 'MMM d, yyyy') } +const getDueDateStatus = (chore: Chore) => { + const today = startOfDay(new Date()); + const dueDate = startOfDay(new Date(chore.next_due_date)); + + if (dueDate < today) return 'overdue'; + if (isEqual(dueDate, today)) return 'due-today'; + return 'upcoming'; +} + +const getChoreStatusInfo = (chore: Chore) => { + const currentAssignment = chore.assignments && chore.assignments.length > 0 ? chore.assignments[0] : null; + const isCompleted = currentAssignment?.is_complete ?? false; + const assignedUser = currentAssignment?.assigned_user; + const dueDateStatus = getDueDateStatus(chore); + + return { + currentAssignment, + isCompleted, + assignedUser, + dueDateStatus, + assignedUserName: assignedUser?.name || assignedUser?.email || 'Unassigned' + }; +} + const formatFrequency = (frequency: ChoreFrequency) => { const options: Record = { one_time: t('choresPage.frequencyOptions.oneTime'), // Reusing existing keys @@ -833,13 +1031,35 @@ const toggleInviteUI = () => { const openChoreDetailModal = async (chore: Chore) => { selectedChore.value = chore; showChoreDetailModal.value = true; + + // Load assignments for this chore + loadingAssignments.value = true; + try { + selectedChoreAssignments.value = await choreService.getChoreAssignments(chore.id); + } catch (error) { + console.error('Failed to load chore assignments:', error); + notificationStore.addNotification({ + message: 'Failed to load chore assignments.', + type: 'error' + }); + } finally { + loadingAssignments.value = false; + } + // Optionally lazy load history if not already loaded with the chore if (!chore.history || chore.history.length === 0) { - const history = await choreService.getChoreHistory(chore.id); - const choreInList = upcomingChores.value.find(c => c.id === chore.id); - if (choreInList) { - choreInList.history = history; - selectedChore.value = choreInList; + try { + const history = await choreService.getChoreHistory(chore.id); + selectedChore.value = { + ...selectedChore.value, + history: history + }; + } catch (error) { + console.error('Failed to load chore history:', error); + notificationStore.addNotification({ + message: 'Failed to load chore history.', + type: 'error' + }); } } }; @@ -900,17 +1120,62 @@ const loadGroupChoreHistory = async () => { const formatHistoryEntry = (entry: ChoreHistory | ChoreAssignmentHistory): string => { const user = entry.changed_by_user?.email || 'System'; - const time = new Date(entry.timestamp).toLocaleString(); + const eventType = entry.event_type.toLowerCase().replace(/_/g, ' '); + + let action = ''; + switch (entry.event_type) { + case 'created': + action = 'created this chore'; + break; + case 'updated': + action = 'updated this chore'; + break; + case 'completed': + action = 'completed the assignment'; + break; + case 'reopened': + action = 'reopened the assignment'; + break; + case 'assigned': + action = 'was assigned to this chore'; + break; + case 'unassigned': + action = 'was unassigned from this chore'; + break; + case 'reassigned': + action = 'was reassigned this chore'; + break; + case 'due_date_changed': + action = 'changed the due date'; + break; + case 'deleted': + action = 'deleted this chore'; + break; + default: + action = eventType; + } + let details = ''; if (entry.event_data) { - details = Object.entries(entry.event_data).map(([key, value]) => { + const changes = Object.entries(entry.event_data).map(([key, value]) => { if (typeof value === 'object' && value !== null && 'old' in value && 'new' in value) { - return `${key} changed from '${value.old}' to '${value.new}'`; + const fieldName = key.replace(/_/g, ' '); + return `${fieldName}: "${value.old}" → "${value.new}"`; } return `${key}: ${JSON.stringify(value)}`; - }).join(', '); + }); + if (changes.length > 0) { + details = ` (${changes.join(', ')})`; + } } - return `${user} ${entry.event_type} on ${time}. Details: ${details}`; + + return `${user} ${action}${details}`; +}; + +const isAssignmentOverdue = (assignment: ChoreAssignment): boolean => { + const dueDate = new Date(assignment.due_date); + const today = startOfDay(new Date()); + return dueDate < today; }; onMounted(() => { @@ -1530,4 +1795,379 @@ onMounted(() => { .neo-settlement-activities li { margin-top: 0.2em; } + +/* Enhanced Chores List Styles */ +.enhanced-chores-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.enhanced-chore-item { + background: #fafafa; + border: 2px solid #111; + border-radius: 12px; + padding: 1rem; + cursor: pointer; + transition: all 0.2s ease; + position: relative; +} + +.enhanced-chore-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.enhanced-chore-item.status-overdue { + border-left: 6px solid #ef4444; +} + +.enhanced-chore-item.status-due-today { + border-left: 6px solid #f59e0b; +} + +.enhanced-chore-item.completed { + opacity: 0.8; + background: #f0f9ff; +} + +.chore-main-content { + display: flex; + align-items: flex-start; + gap: 1rem; +} + +.chore-icon-container { + flex-shrink: 0; +} + +.chore-status-indicator { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + background: #e5e7eb; +} + +.chore-status-indicator.overdue { + background: #fee2e2; + color: #dc2626; +} + +.chore-status-indicator.due-today { + background: #fef3c7; + color: #d97706; +} + +.chore-status-indicator.completed { + background: #d1fae5; + color: #059669; +} + +.chore-text-content { + flex: 1; + min-width: 0; +} + +.chore-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.5rem; + flex-wrap: wrap; + gap: 0.5rem; +} + +.neo-chore-name { + font-weight: 600; + font-size: 1.1rem; + color: #111; +} + +.neo-chore-name.completed { + text-decoration: line-through; + opacity: 0.7; +} + +.chore-badges { + display: flex; + gap: 0.25rem; + flex-wrap: wrap; +} + +.chore-details { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.9rem; + color: #666; +} + +.chore-due-info, +.chore-assignment-info { + display: flex; + gap: 0.5rem; +} + +.due-label, +.assignment-label { + font-weight: 600; + color: #374151; +} + +.due-date.overdue { + color: #dc2626; + font-weight: 600; +} + +.due-date.due-today { + color: #d97706; + font-weight: 600; +} + +.today-indicator, +.overdue-indicator { + font-size: 0.8rem; + font-weight: 500; +} + +.chore-description { + margin-top: 0.25rem; + font-style: italic; + color: #6b7280; +} + +.completion-info { + margin-top: 0.25rem; + color: #059669; + font-weight: 500; + font-size: 0.85rem; +} + +.chore-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Chore Detail Modal Styles */ +.chore-detail-content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.chore-overview-section { + border-bottom: 1px solid #e5e7eb; + padding-bottom: 1rem; +} + +.chore-status-summary { + margin-bottom: 1rem; +} + +.status-badges { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; + flex-wrap: wrap; +} + +.chore-meta-info { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.75rem; +} + +.meta-item { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.meta-item .label { + font-weight: 600; + color: #374151; + font-size: 0.9rem; +} + +.meta-item .value { + color: #111; +} + +.meta-item .value.overdue { + color: #dc2626; + font-weight: 600; +} + +.chore-description-full { + margin-top: 1rem; +} + +.chore-description-full p { + color: #374151; + line-height: 1.6; +} + +.assignments-section, +.assignment-history-section, +.chore-history-section { + border-bottom: 1px solid #e5e7eb; + padding-bottom: 1rem; +} + +.assignments-section:last-child, +.assignment-history-section:last-child, +.chore-history-section:last-child { + border-bottom: none; +} + +.assignments-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.assignment-card { + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 1rem; +} + +.editing-assignment { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.editing-actions { + display: flex; + gap: 0.5rem; +} + +.assignment-info { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.assignment-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.assigned-user-info { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.user-name { + font-weight: 600; + color: #111; +} + +.assignment-actions { + display: flex; + gap: 0.5rem; +} + +.assignment-details { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.detail-item { + display: flex; + gap: 0.5rem; +} + +.detail-item .label { + font-weight: 600; + color: #374151; + min-width: 80px; +} + +.detail-item .value { + color: #111; +} + +.no-assignments, +.no-history { + color: #6b7280; + font-style: italic; + text-align: center; + padding: 1rem; +} + +.history-timeline { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.assignment-history-header { + font-weight: 600; + color: #374151; + margin-bottom: 0.5rem; + border-bottom: 1px solid #e5e7eb; + padding-bottom: 0.25rem; +} + +.history-entry { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.75rem; + background: #f9fafb; + border-radius: 6px; + border-left: 3px solid #d1d5db; +} + +.history-timestamp { + font-size: 0.8rem; + color: #6b7280; + font-weight: 500; +} + +.history-event { + color: #374151; +} + +.history-user { + font-size: 0.85rem; + color: #6b7280; + font-style: italic; +} + +@media (max-width: 768px) { + .chore-header { + flex-direction: column; + align-items: flex-start; + } + + .chore-meta-info { + grid-template-columns: 1fr; + } + + .assignment-header { + flex-direction: column; + gap: 0.5rem; + } +} + +.loading-assignments { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + color: #6b7280; + font-style: italic; +}