feat: Enhance GroupDetailPage with chore assignments and history
All checks were successful
Deploy to Production, build images and push to Gitea Registry / build_and_push (pull_request) Successful in 1m23s

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.
This commit is contained in:
mohamad 2025-06-08 02:03:38 +02:00
parent 402489c928
commit 88c9516308
2 changed files with 932 additions and 288 deletions

View File

@ -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 {

View File

@ -1,11 +1,13 @@
<template>
<main class="container page-padding">
<div class="group-detail-container">
<div v-if="loading" class="text-center">
<VSpinner :label="t('groupDetailPage.loadingLabel')" />
</div>
<VAlert v-else-if="error" type="error" :message="error" class="mb-3">
<template #actions>
<VButton variant="danger" size="sm" @click="fetchGroupDetails">{{ t('groupDetailPage.retryButton') }}</VButton>
<VButton variant="danger" size="sm" @click="fetchGroupDetails">{{ t('groupDetailPage.retryButton') }}
</VButton>
</template>
</VAlert>
<div v-else-if="group">
@ -37,7 +39,7 @@
</div>
<button ref="addMemberButtonRef" @click="toggleInviteUI" class="add-member-btn"
:aria-label="t('groupDetailPage.invites.title')">
{{ t('groupDetailPage.invites.addMemberButtonLabel') }}
+
</button>
<!-- Invite Members Popup -->
@ -80,21 +82,74 @@
<div class="mt-4 neo-section">
<div class="flex justify-between items-center w-full mb-2">
<VHeading :level="3" class="neo-section-header">{{ t('groupDetailPage.chores.title') }}</VHeading>
<VButton @click="showGenerateScheduleModal = true">{{ t('groupDetailPage.chores.generateScheduleButton') }}
<VButton @click="showGenerateScheduleModal = true">{{ t('groupDetailPage.chores.generateScheduleButton')
}}
</VButton>
</div>
<VList v-if="upcomingChores.length > 0">
<VListItem v-for="chore in upcomingChores" :key="chore.id" @click="openChoreDetailModal(chore)"
class="flex justify-between items-center cursor-pointer">
<div class="neo-chore-info">
<span class="neo-chore-name">{{ chore.name }}</span>
<span class="neo-chore-due">{{ t('groupDetailPage.chores.duePrefix') }} {{
formatDate(chore.next_due_date)
}}</span>
<div v-if="upcomingChores.length > 0" class="enhanced-chores-list">
<div v-for="chore in upcomingChores" :key="chore.id" class="enhanced-chore-item"
:class="`status-${getDueDateStatus(chore)} ${getChoreStatusInfo(chore).isCompleted ? 'completed' : ''}`"
@click="openChoreDetailModal(chore)">
<div class="chore-main-content">
<div class="chore-icon-container">
<div class="chore-status-indicator" :class="{
'overdue': getDueDateStatus(chore) === 'overdue',
'due-today': getDueDateStatus(chore) === 'due-today',
'completed': getChoreStatusInfo(chore).isCompleted
}">
{{ getChoreStatusInfo(chore).isCompleted ? '✅' :
getDueDateStatus(chore) === 'overdue' ? '⚠️' :
getDueDateStatus(chore) === 'due-today' ? '📅' : '📋' }}
</div>
</div>
<div class="chore-text-content">
<div class="chore-header">
<span class="neo-chore-name" :class="{ completed: getChoreStatusInfo(chore).isCompleted }">
{{ chore.name }}
</span>
<div class="chore-badges">
<VBadge :text="formatFrequency(chore.frequency)"
:variant="getFrequencyBadgeVariant(chore.frequency)" />
<VBadge v-if="getDueDateStatus(chore) === 'overdue'" text="Overdue" variant="danger" />
<VBadge v-if="getDueDateStatus(chore) === 'due-today'" text="Due Today" variant="warning" />
<VBadge v-if="getChoreStatusInfo(chore).isCompleted" text="Completed" variant="success" />
</div>
</div>
<div class="chore-details">
<div class="chore-due-info">
<span class="due-label">Due:</span>
<span class="due-date" :class="getDueDateStatus(chore)">
{{ formatDate(chore.next_due_date) }}
<span v-if="getDueDateStatus(chore) === 'due-today'" class="today-indicator">(Today)</span>
<span v-if="getDueDateStatus(chore) === 'overdue'" class="overdue-indicator">
({{ formatDistanceToNow(new Date(chore.next_due_date), { addSuffix: true }) }})
</span>
</span>
</div>
<div class="chore-assignment-info">
<span class="assignment-label">Assigned to:</span>
<span class="assigned-user">{{ getChoreStatusInfo(chore).assignedUserName }}</span>
</div>
<div v-if="chore.description" class="chore-description">
{{ chore.description }}
</div>
<div
v-if="getChoreStatusInfo(chore).isCompleted && getChoreStatusInfo(chore).currentAssignment?.completed_at"
class="completion-info">
Completed {{ formatDistanceToNow(new
Date(getChoreStatusInfo(chore).currentAssignment!.completed_at!),
{ addSuffix: true }) }}
</div>
</div>
</div>
</div>
<div class="chore-actions">
<VButton size="sm" variant="neutral" @click.stop="openChoreDetailModal(chore)" title="View Details">
👁
</VButton>
</div>
</div>
</div>
<VBadge :text="formatFrequency(chore.frequency)" :variant="getFrequencyBadgeVariant(chore.frequency)" />
</VListItem>
</VList>
<div v-else class="text-center py-4">
<VIcon name="cleaning_services" size="lg" class="opacity-50 mb-2" />
<p>{{ t('groupDetailPage.chores.emptyState') }}</p>
@ -161,7 +216,8 @@
<div class="neo-splits-list">
<div v-for="split in expense.splits" :key="split.id" class="neo-split-item">
<div class="split-col split-user">
<strong>{{ split.user?.name || split.user?.email || t('groupDetailPage.expenses.fallbackUserName',
<strong>{{ split.user?.name || split.user?.email ||
t('groupDetailPage.expenses.fallbackUserName',
{
userId: split.user_id
}) }}</strong>
@ -197,7 +253,8 @@
formatCurrency(activity.amount_paid) }}
{{
t('groupDetailPage.expenses.byUser') }} {{ activity.payer?.name ||
t('groupDetailPage.expenses.activityByUserFallback', { userId: activity.paid_by_user_id }) }} {{
t('groupDetailPage.expenses.activityByUserFallback', { userId: activity.paid_by_user_id }) }}
{{
t('groupDetailPage.expenses.onDate') }} {{ new
Date(activity.paid_at).toLocaleDateString() }}
</li>
@ -249,35 +306,147 @@
</template>
</VModal>
<!-- Chore Detail Modal -->
<!-- Enhanced Chore Detail Modal -->
<VModal v-model="showChoreDetailModal" :title="selectedChore?.name" size="lg">
<div v-if="selectedChore">
<!-- ... chore details ... -->
<VHeading :level="4">{{ t('groupDetailPage.choreDetailModal.assignmentsTitle') }}</VHeading>
<div v-for="assignment in selectedChore.assignments" :key="assignment.id" class="assignment-row">
<template v-if="editingAssignment?.id === assignment.id">
<!-- Inline Editing UI -->
<VSelect v-if="group && group.members" :options="group.members.map(m => ({ value: m.id, label: m.email }))"
v-model="editingAssignment.assigned_to_user_id" />
<VInput type="date" :model-value="editingAssignment.due_date ?? ''"
@update:model-value="val => editingAssignment && (editingAssignment.due_date = val)" />
<VButton @click="saveAssignmentEdit(assignment.id)" size="sm">{{ t('shared.save') }}</VButton>
<VButton @click="cancelAssignmentEdit" variant="neutral" size="sm">{{ t('shared.cancel') }}</VButton>
</template>
<template v-else>
<span>{{ assignment.assigned_user?.email }} - Due: {{ formatDate(assignment.due_date) }}</span>
<VButton v-if="!assignment.is_complete" @click="startAssignmentEdit(assignment)" size="sm"
variant="neutral">{{ t('shared.edit') }}</VButton>
</template>
<template #default>
<div v-if="selectedChore" class="chore-detail-content">
<!-- Chore Overview -->
<div class="chore-overview-section">
<div class="chore-status-summary">
<div class="status-badges">
<VBadge :text="formatFrequency(selectedChore.frequency)"
:variant="getFrequencyBadgeVariant(selectedChore.frequency)" />
<VBadge v-if="getDueDateStatus(selectedChore) === 'overdue'" text="Overdue" variant="danger" />
<VBadge v-if="getDueDateStatus(selectedChore) === 'due-today'" text="Due Today" variant="warning" />
<VBadge v-if="getChoreStatusInfo(selectedChore).isCompleted" text="Completed" variant="success" />
</div>
<div class="chore-meta-info">
<div class="meta-item">
<span class="label">Created by:</span>
<span class="value">{{ selectedChore.creator?.name || selectedChore.creator?.email || 'Unknown'
}}</span>
</div>
<div class="meta-item">
<span class="label">Created:</span>
<span class="value">{{ format(new Date(selectedChore.created_at), 'MMM d, yyyy') }}</span>
</div>
<div class="meta-item">
<span class="label">Next due:</span>
<span class="value" :class="getDueDateStatus(selectedChore)">
{{ formatDate(selectedChore.next_due_date) }}
<span v-if="getDueDateStatus(selectedChore) === 'due-today'">(Today)</span>
</span>
</div>
<div v-if="selectedChore.custom_interval_days" class="meta-item">
<span class="label">Custom interval:</span>
<span class="value">Every {{ selectedChore.custom_interval_days }} days</span>
</div>
</div>
</div>
<div v-if="selectedChore.description" class="chore-description-full">
<VHeading :level="5">Description</VHeading>
<p>{{ selectedChore.description }}</p>
</div>
</div>
<VHeading :level="4">{{ t('groupDetailPage.choreDetailModal.choreHistoryTitle') }}</VHeading>
<!-- Chore History Display -->
<ul v-if="selectedChore.history && selectedChore.history.length > 0">
<li v-for="entry in selectedChore.history" :key="entry.id">{{ formatHistoryEntry(entry) }}</li>
</ul>
<p v-else>{{ t('groupDetailPage.choreDetailModal.noHistory') }}</p>
<!-- Current Assignments -->
<div class="assignments-section">
<VHeading :level="4">{{ t('groupDetailPage.choreDetailModal.assignmentsTitle') }}</VHeading>
<div v-if="loadingAssignments" class="loading-assignments">
<VSpinner size="sm" />
<span>Loading assignments...</span>
</div>
<div v-else-if="selectedChoreAssignments.length > 0" class="assignments-list">
<div v-for="assignment in selectedChoreAssignments" :key="assignment.id" class="assignment-card">
<template v-if="editingAssignment?.id === assignment.id">
<!-- Inline Editing UI -->
<div class="editing-assignment">
<VFormField label="Assigned to:">
<VSelect v-if="group?.members"
:options="group.members.map(m => ({ value: m.id, label: m.email }))"
:model-value="editingAssignment.assigned_to_user_id || 0"
@update:model-value="val => editingAssignment && (editingAssignment.assigned_to_user_id = val)" />
</VFormField>
<VFormField label="Due date:">
<VInput type="date" :model-value="editingAssignment.due_date ?? ''"
@update:model-value="val => editingAssignment && (editingAssignment.due_date = val)" />
</VFormField>
<div class="editing-actions">
<VButton @click="saveAssignmentEdit(assignment.id)" size="sm">{{ t('shared.save') }}</VButton>
<VButton @click="cancelAssignmentEdit" variant="neutral" size="sm">{{ t('shared.cancel') }}
</VButton>
</div>
</div>
</template>
<template v-else>
<div class="assignment-info">
<div class="assignment-header">
<div class="assigned-user-info">
<span class="user-name">{{ assignment.assigned_user?.name || assignment.assigned_user?.email
|| 'Unknown User' }}</span>
<VBadge v-if="assignment.is_complete" text="Completed" variant="success" />
<VBadge v-else-if="isAssignmentOverdue(assignment)" text="Overdue" variant="danger" />
</div>
<div class="assignment-actions">
<VButton v-if="!assignment.is_complete" @click="startAssignmentEdit(assignment)" size="sm"
variant="neutral">
{{ t('shared.edit') }}
</VButton>
</div>
</div>
<div class="assignment-details">
<div class="detail-item">
<span class="label">Due:</span>
<span class="value">{{ formatDate(assignment.due_date) }}</span>
</div>
<div v-if="assignment.is_complete && assignment.completed_at" class="detail-item">
<span class="label">Completed:</span>
<span class="value">
{{ formatDate(assignment.completed_at) }}
({{ formatDistanceToNow(new Date(assignment.completed_at), { addSuffix: true }) }})
</span>
</div>
</div>
</div>
</template>
</div>
</div>
<p v-else class="no-assignments">{{ t('groupDetailPage.choreDetailModal.noAssignments') }}</p>
</div>
<!-- Assignment History -->
<div
v-if="selectedChore.assignments && selectedChore.assignments.some(a => a.history && a.history.length > 0)"
class="assignment-history-section">
<VHeading :level="4">Assignment History</VHeading>
<div class="history-timeline">
<div v-for="assignment in selectedChore.assignments" :key="`history-${assignment.id}`">
<div v-if="assignment.history && assignment.history.length > 0">
<h6 class="assignment-history-header">{{ assignment.assigned_user?.email || 'Unknown User' }}</h6>
<div v-for="historyEntry in assignment.history" :key="historyEntry.id" class="history-entry">
<div class="history-timestamp">{{ format(new Date(historyEntry.timestamp), 'MMM d, yyyy HH:mm') }}
</div>
<div class="history-event">{{ formatHistoryEntry(historyEntry) }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Chore History -->
<div class="chore-history-section">
<VHeading :level="4">{{ t('groupDetailPage.choreDetailModal.choreHistoryTitle') }}</VHeading>
<div v-if="selectedChore.history && selectedChore.history.length > 0" class="history-timeline">
<div v-for="entry in selectedChore.history" :key="entry.id" class="history-entry">
<div class="history-timestamp">{{ format(new Date(entry.timestamp), 'MMM d, yyyy HH:mm') }}</div>
<div class="history-event">{{ formatHistoryEntry(entry) }}</div>
<div v-if="entry.changed_by_user" class="history-user">by {{ entry.changed_by_user.email }}</div>
</div>
</div>
<p v-else class="no-history">{{ t('groupDetailPage.choreDetailModal.noHistory') }}</p>
</div>
</div>
</template>
</VModal>
<!-- Generate Schedule Modal -->
@ -295,6 +464,7 @@
t('groupDetailPage.generateScheduleModal.generateButton') }}</VButton>
</template>
</VModal>
</div>
</main>
</template>
@ -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<ChoreHistory[]>([]);
const groupHistoryLoading = ref(false);
const loadingAssignments = ref(false);
const selectedChoreAssignments = ref<ChoreAssignment[]>([]);
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<ChoreFrequency, string> = {
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) {
try {
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;
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;
}
</style>