ph5 #63
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user