Merge pull request 'ph5' (#63) from ph5 into prod

Reviewed-on: #63
This commit is contained in:
mo 2025-06-08 12:29:29 +02:00
commit 2d70850840
8 changed files with 105 additions and 44 deletions

View File

@ -13,6 +13,7 @@ from app.schemas.invite import InviteCodePublic
from app.schemas.message import Message # For simple responses
from app.schemas.list import ListPublic, ListDetail
from app.schemas.chore import ChoreHistoryPublic, ChoreAssignmentPublic
from app.schemas.user import UserPublic
from app.crud import group as crud_group
from app.crud import invite as crud_invite
from app.crud import list as crud_list
@ -92,6 +93,33 @@ async def read_group(
return group
@router.get(
"/{group_id}/members",
response_model=List[UserPublic],
summary="Get Group Members",
tags=["Groups"]
)
async def read_group_members(
group_id: int,
db: AsyncSession = Depends(get_session), # Use read-only session for GET
current_user: UserModel = Depends(current_active_user),
):
"""Retrieves all members of a specific group, if the user is part of it."""
logger.info(f"User {current_user.email} requesting members for group ID: {group_id}")
# Check if user is a member first
is_member = await crud_group.is_user_member(db=db, group_id=group_id, user_id=current_user.id)
if not is_member:
logger.warning(f"Access denied: User {current_user.email} not member of group {group_id}")
raise GroupMembershipError(group_id, "view group members")
group = await crud_group.get_group_by_id(db=db, group_id=group_id)
if not group:
logger.error(f"Group {group_id} requested by member {current_user.email} not found (data inconsistency?)")
raise GroupNotFoundError(group_id)
# Extract and return just the user information from member associations
return [member_assoc.user for member_assoc in group.member_associations]
@router.post(
"/{group_id}/invites",

View File

@ -81,7 +81,8 @@
body {
font-family: 'Patrick Hand', cursive;
background-color: var(--light);
background-image: var(--paper-texture);
// background-image: var(--paper-texture);
// background-image: url('@/assets/11.webp');
// padding: 2rem 1rem;s
color: var(--dark);
font-size: 1.1rem;

View File

@ -97,6 +97,8 @@
"addChore": "+",
"edit": "Edit",
"delete": "Delete",
"editChore": "Edit Chore",
"createChore": "Create Chore",
"empty": {
"title": "No Chores Yet",
"message": "Get started by adding your first chore!",
@ -170,6 +172,23 @@
"loadingLabel": "Loading group details...",
"retryButton": "Retry",
"groupNotFound": "Group not found or an error occurred.",
"lists": {
"title": "Group Lists"
},
"generateScheduleModal": {
"title": "Generate Schedule"
},
"activityLog": {
"title": "Activity Log",
"emptyState": "No activity to show yet."
},
"chores": {
"title": "Group Chores",
"manageButton": "Manage Chores",
"duePrefix": "Due:",
"emptyState": "No chores scheduled. Click \"Manage Chores\" to create some!",
"generateScheduleButton": "Generate Schedule"
},
"members": {
"title": "Group Members",
"defaultRole": "Member",
@ -201,12 +220,6 @@
"console": {
"noActiveInvite": "No active invite code found for this group."
},
"chores": {
"title": "Group Chores",
"manageButton": "Manage Chores",
"duePrefix": "Due:",
"emptyState": "No chores scheduled. Click \"Manage Chores\" to create some!"
},
"expenses": {
"title": "Group Expenses",
"manageButton": "Manage Expenses",
@ -445,6 +458,8 @@
"addExpenseButton": "Add Expense",
"loading": "Loading expenses...",
"emptyState": "No expenses recorded for this list yet.",
"emptyStateTitle": "No Expenses",
"emptyStateMessage": "Add your first expense to get started.",
"paidBy": "Paid by:",
"onDate": "on",
"owes": "owes",

View File

@ -94,6 +94,11 @@
},
"choresPage": {
"title": "Taken",
"addChore": "+",
"edit": "Bewerken",
"delete": "Verwijderen",
"editChore": "Taak bewerken",
"createChore": "Nieuwe taak",
"tabs": {
"overdue": "Achterstallig",
"today": "Vandaag",
@ -339,6 +344,16 @@
"partiallyPaid": "Gedeeltelijk betaald",
"unpaid": "Onbetaald",
"unknown": "Onbekende status"
},
"lists": {
"title": "Groepslijsten"
},
"generateScheduleModal": {
"title": "Schema genereren"
},
"activityLog": {
"title": "Activiteitenlogboek",
"emptyState": "Nog geen activiteiten om weer te geven."
}
},
"accountPage": {

View File

@ -718,6 +718,19 @@ const getDueDateStatus = (chore: ChoreWithCompletion) => {
overflow: hidden;
}
/* Status-based styling */
.schedule-group:has(.status-overdue) .neo-item-list-container {
box-shadow: 6px 6px 0 #c72d2d;
}
.schedule-group:has(.status-due-today) .neo-item-list-container {
box-shadow: 6px 6px 0 #b37814;
}
.status-completed {
opacity: 0.7;
}
/* Neo-style list items from ListDetailPage */
.neo-item-list {
list-style: none;
@ -940,19 +953,6 @@ const getDueDateStatus = (chore: ChoreWithCompletion) => {
font-style: italic;
}
/* Status-based styling */
.status-overdue {
border-left: 4px solid #ef4444;
}
.status-due-today {
border-left: 4px solid #f59e0b;
}
.status-completed {
opacity: 0.7;
}
/* Modal styles */
.detail-modal .modal-container,
.history-modal .modal-container {

View File

@ -83,7 +83,7 @@
<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>
</div>
<div v-if="upcomingChores.length > 0" class="enhanced-chores-list">
@ -299,10 +299,10 @@
<template #footer>
<VButton variant="neutral" @click="closeSettleShareModal">{{
t('groupDetailPage.settleShareModal.cancelButton')
}}</VButton>
}}</VButton>
<VButton variant="primary" @click="handleConfirmSettle" :disabled="isSettlementLoading">{{
t('groupDetailPage.settleShareModal.confirmButton')
}}</VButton>
}}</VButton>
</template>
</VModal>
@ -324,7 +324,7 @@
<div class="meta-item">
<span class="label">Created by:</span>
<span class="value">{{ selectedChore.creator?.name || selectedChore.creator?.email || 'Unknown'
}}</span>
}}</span>
</div>
<div class="meta-item">
<span class="label">Created:</span>

View File

@ -316,22 +316,23 @@
<VAlert v-else-if="costSummaryError" type="error" :message="costSummaryError" />
<div v-else-if="listCostSummary">
<div class="mb-3 cost-overview">
<p><strong>{{ $t('listDetailPage.costSummaryModal.totalCostLabel') }}</strong> {{
<p><strong>{{ $t('listDetailPage.modals.costSummary.totalCostLabel') }}</strong> {{
formatCurrency(listCostSummary.total_list_cost) }}</p>
<p><strong>{{ $t('listDetailPage.costSummaryModal.equalShareLabel') }}</strong> {{
<p><strong>{{ $t('listDetailPage.modals.costSummary.equalShareLabel') }}</strong> {{
formatCurrency(listCostSummary.equal_share_per_user) }}</p>
<p><strong>{{ $t('listDetailPage.costSummaryModal.participantsLabel') }}</strong> {{
<p><strong>{{ $t('listDetailPage.modals.costSummary.participantsLabel') }}</strong> {{
listCostSummary.num_participating_users }}</p>
</div>
<h4>{{ $t('listDetailPage.costSummaryModal.userBalancesHeader') }}</h4>
<h4>{{ $t('listDetailPage.modals.costSummary.userBalancesHeader') }}</h4>
<div class="table-container mt-2">
<table class="table">
<thead>
<tr>
<th>{{ $t('listDetailPage.costSummaryModal.tableHeaders.user') }}</th>
<th class="text-right">{{ $t('listDetailPage.costSummaryModal.tableHeaders.itemsAddedValue') }}</th>
<th class="text-right">{{ $t('listDetailPage.costSummaryModal.tableHeaders.amountDue') }}</th>
<th class="text-right">{{ $t('listDetailPage.costSummaryModal.tableHeaders.balance') }}</th>
<th>{{ $t('listDetailPage.modals.costSummary.tableHeaders.user') }}</th>
<th class="text-right">{{ $t('listDetailPage.modals.costSummary.tableHeaders.itemsAddedValue') }}
</th>
<th class="text-right">{{ $t('listDetailPage.modals.costSummary.tableHeaders.amountDue') }}</th>
<th class="text-right">{{ $t('listDetailPage.modals.costSummary.tableHeaders.balance') }}</th>
</tr>
</thead>
<tbody>
@ -348,7 +349,7 @@
</table>
</div>
</div>
<p v-else>{{ $t('listDetailPage.costSummaryModal.emptyState') }}</p>
<p v-else>{{ $t('listDetailPage.modals.costSummary.emptyState') }}</p>
</template>
<template #footer>
<VButton variant="primary" @click="showCostSummaryDialog = false">{{ $t('listDetailPage.buttons.close') }}
@ -357,7 +358,7 @@
</VModal>
<!-- Settle Share Modal -->
<VModal v-model="showSettleModal" :title="$t('listDetailPage.settleShareModal.title')"
<VModal v-model="showSettleModal" :title="$t('listDetailPage.modals.settleShare.title')"
@update:modelValue="!$event && closeSettleShareModal()" size="md">
<template #default>
<div v-if="isSettlementLoading" class="text-center">
@ -365,11 +366,11 @@
</div>
<VAlert v-else-if="settleAmountError" type="error" :message="settleAmountError" />
<div v-else>
<p>{{ $t('listDetailPage.settleShareModal.settleAmountFor', {
<p>{{ $t('listDetailPage.modals.settleShare.settleAmountFor', {
userName: selectedSplitForSettlement?.user?.name
|| selectedSplitForSettlement?.user?.email || `User ID: ${selectedSplitForSettlement?.user_id}`
}) }}</p>
<VFormField :label="$t('listDetailPage.settleShareModal.amountLabel')"
<VFormField :label="$t('listDetailPage.modals.settleShare.amountLabel')"
:error-message="settleAmountError || undefined">
<VInput type="number" v-model="settleAmount" id="settleAmount" required />
</VFormField>
@ -377,9 +378,10 @@
</template>
<template #footer>
<VButton variant="neutral" @click="closeSettleShareModal">{{
$t('listDetailPage.settleShareModal.cancelButton')
$t('listDetailPage.modals.settleShare.cancelButton')
}}</VButton>
<VButton variant="primary" @click="handleConfirmSettle">{{ $t('listDetailPage.settleShareModal.confirmButton')
<VButton variant="primary" @click="handleConfirmSettle">{{
$t('listDetailPage.modals.settleShare.confirmButton')
}}</VButton>
</template>
</VModal>
@ -1215,12 +1217,12 @@ const closeSettleShareModal = () => {
const validateSettleAmount = (): boolean => {
settleAmountError.value = null;
if (!settleAmount.value.trim()) {
settleAmountError.value = t('listDetailPage.settleShareModal.errors.enterAmount');
settleAmountError.value = t('listDetailPage.modals.settleShare.errors.enterAmount');
return false;
}
const amount = new Decimal(settleAmount.value);
if (amount.isNaN() || amount.isNegative() || amount.isZero()) {
settleAmountError.value = t('listDetailPage.settleShareModal.errors.positiveAmount');
settleAmountError.value = t('listDetailPage.modals.settleShare.errors.positiveAmount');
return false;
}
if (selectedSplitForSettlement.value) {
@ -1228,11 +1230,11 @@ const validateSettleAmount = (): boolean => {
const owed = new Decimal(selectedSplitForSettlement.value.owed_amount);
const remaining = owed.minus(alreadyPaid);
if (amount.greaterThan(remaining.plus(new Decimal('0.001')))) {
settleAmountError.value = t('listDetailPage.settleShareModal.errors.exceedsRemaining', { amount: formatCurrency(remaining.toFixed(2)) });
settleAmountError.value = t('listDetailPage.modals.settleShare.errors.exceedsRemaining', { amount: formatCurrency(remaining.toFixed(2)) });
return false;
}
} else {
settleAmountError.value = t('listDetailPage.settleShareModal.errors.noSplitSelected');
settleAmountError.value = t('listDetailPage.modals.settleShare.errors.noSplitSelected');
return false;
}
return true;

View File

@ -29,8 +29,8 @@
<div class="neo-lists-grid">
<div v-for="list in lists" :key="list.id" class="neo-list-card"
:class="{ 'touch-active': touchActiveListId === list.id }" @click="navigateToList(list.id)"
@touchstart="handleTouchStart(list.id)" @touchend="handleTouchEnd" @touchcancel="handleTouchEnd"
:data-list-id="list.id">
@touchstart.passive="handleTouchStart(list.id)" @touchend.passive="handleTouchEnd"
@touchcancel.passive="handleTouchEnd" :data-list-id="list.id">
<div class="neo-list-header">{{ list.name }}</div>
<div class="neo-list-desc">{{ list.description || t('listsPage.noDescription') }}</div>
<ul class="neo-item-list">