ph5 #63
@ -13,6 +13,7 @@ from app.schemas.invite import InviteCodePublic
|
|||||||
from app.schemas.message import Message # For simple responses
|
from app.schemas.message import Message # For simple responses
|
||||||
from app.schemas.list import ListPublic, ListDetail
|
from app.schemas.list import ListPublic, ListDetail
|
||||||
from app.schemas.chore import ChoreHistoryPublic, ChoreAssignmentPublic
|
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 group as crud_group
|
||||||
from app.crud import invite as crud_invite
|
from app.crud import invite as crud_invite
|
||||||
from app.crud import list as crud_list
|
from app.crud import list as crud_list
|
||||||
@ -92,6 +93,33 @@ async def read_group(
|
|||||||
|
|
||||||
return 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(
|
@router.post(
|
||||||
"/{group_id}/invites",
|
"/{group_id}/invites",
|
||||||
|
@ -81,7 +81,8 @@
|
|||||||
body {
|
body {
|
||||||
font-family: 'Patrick Hand', cursive;
|
font-family: 'Patrick Hand', cursive;
|
||||||
background-color: var(--light);
|
background-color: var(--light);
|
||||||
background-image: var(--paper-texture);
|
// background-image: var(--paper-texture);
|
||||||
|
// background-image: url('@/assets/11.webp');
|
||||||
// padding: 2rem 1rem;s
|
// padding: 2rem 1rem;s
|
||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
@ -97,6 +97,8 @@
|
|||||||
"addChore": "+",
|
"addChore": "+",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"editChore": "Edit Chore",
|
||||||
|
"createChore": "Create Chore",
|
||||||
"empty": {
|
"empty": {
|
||||||
"title": "No Chores Yet",
|
"title": "No Chores Yet",
|
||||||
"message": "Get started by adding your first chore!",
|
"message": "Get started by adding your first chore!",
|
||||||
@ -170,6 +172,23 @@
|
|||||||
"loadingLabel": "Loading group details...",
|
"loadingLabel": "Loading group details...",
|
||||||
"retryButton": "Retry",
|
"retryButton": "Retry",
|
||||||
"groupNotFound": "Group not found or an error occurred.",
|
"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": {
|
"members": {
|
||||||
"title": "Group Members",
|
"title": "Group Members",
|
||||||
"defaultRole": "Member",
|
"defaultRole": "Member",
|
||||||
@ -201,12 +220,6 @@
|
|||||||
"console": {
|
"console": {
|
||||||
"noActiveInvite": "No active invite code found for this group."
|
"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": {
|
"expenses": {
|
||||||
"title": "Group Expenses",
|
"title": "Group Expenses",
|
||||||
"manageButton": "Manage Expenses",
|
"manageButton": "Manage Expenses",
|
||||||
@ -445,6 +458,8 @@
|
|||||||
"addExpenseButton": "Add Expense",
|
"addExpenseButton": "Add Expense",
|
||||||
"loading": "Loading expenses...",
|
"loading": "Loading expenses...",
|
||||||
"emptyState": "No expenses recorded for this list yet.",
|
"emptyState": "No expenses recorded for this list yet.",
|
||||||
|
"emptyStateTitle": "No Expenses",
|
||||||
|
"emptyStateMessage": "Add your first expense to get started.",
|
||||||
"paidBy": "Paid by:",
|
"paidBy": "Paid by:",
|
||||||
"onDate": "on",
|
"onDate": "on",
|
||||||
"owes": "owes",
|
"owes": "owes",
|
||||||
|
@ -94,6 +94,11 @@
|
|||||||
},
|
},
|
||||||
"choresPage": {
|
"choresPage": {
|
||||||
"title": "Taken",
|
"title": "Taken",
|
||||||
|
"addChore": "+",
|
||||||
|
"edit": "Bewerken",
|
||||||
|
"delete": "Verwijderen",
|
||||||
|
"editChore": "Taak bewerken",
|
||||||
|
"createChore": "Nieuwe taak",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"overdue": "Achterstallig",
|
"overdue": "Achterstallig",
|
||||||
"today": "Vandaag",
|
"today": "Vandaag",
|
||||||
@ -339,6 +344,16 @@
|
|||||||
"partiallyPaid": "Gedeeltelijk betaald",
|
"partiallyPaid": "Gedeeltelijk betaald",
|
||||||
"unpaid": "Onbetaald",
|
"unpaid": "Onbetaald",
|
||||||
"unknown": "Onbekende status"
|
"unknown": "Onbekende status"
|
||||||
|
},
|
||||||
|
"lists": {
|
||||||
|
"title": "Groepslijsten"
|
||||||
|
},
|
||||||
|
"generateScheduleModal": {
|
||||||
|
"title": "Schema genereren"
|
||||||
|
},
|
||||||
|
"activityLog": {
|
||||||
|
"title": "Activiteitenlogboek",
|
||||||
|
"emptyState": "Nog geen activiteiten om weer te geven."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"accountPage": {
|
"accountPage": {
|
||||||
|
@ -718,6 +718,19 @@ const getDueDateStatus = (chore: ChoreWithCompletion) => {
|
|||||||
overflow: hidden;
|
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-style list items from ListDetailPage */
|
||||||
.neo-item-list {
|
.neo-item-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@ -940,19 +953,6 @@ const getDueDateStatus = (chore: ChoreWithCompletion) => {
|
|||||||
font-style: italic;
|
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 */
|
/* Modal styles */
|
||||||
.detail-modal .modal-container,
|
.detail-modal .modal-container,
|
||||||
.history-modal .modal-container {
|
.history-modal .modal-container {
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
<VHeading :level="3" class="neo-section-header">{{ t('groupDetailPage.chores.title') }}</VHeading>
|
<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>
|
</VButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="upcomingChores.length > 0" class="enhanced-chores-list">
|
<div v-if="upcomingChores.length > 0" class="enhanced-chores-list">
|
||||||
@ -299,10 +299,10 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<VButton variant="neutral" @click="closeSettleShareModal">{{
|
<VButton variant="neutral" @click="closeSettleShareModal">{{
|
||||||
t('groupDetailPage.settleShareModal.cancelButton')
|
t('groupDetailPage.settleShareModal.cancelButton')
|
||||||
}}</VButton>
|
}}</VButton>
|
||||||
<VButton variant="primary" @click="handleConfirmSettle" :disabled="isSettlementLoading">{{
|
<VButton variant="primary" @click="handleConfirmSettle" :disabled="isSettlementLoading">{{
|
||||||
t('groupDetailPage.settleShareModal.confirmButton')
|
t('groupDetailPage.settleShareModal.confirmButton')
|
||||||
}}</VButton>
|
}}</VButton>
|
||||||
</template>
|
</template>
|
||||||
</VModal>
|
</VModal>
|
||||||
|
|
||||||
@ -324,7 +324,7 @@
|
|||||||
<div class="meta-item">
|
<div class="meta-item">
|
||||||
<span class="label">Created by:</span>
|
<span class="label">Created by:</span>
|
||||||
<span class="value">{{ selectedChore.creator?.name || selectedChore.creator?.email || 'Unknown'
|
<span class="value">{{ selectedChore.creator?.name || selectedChore.creator?.email || 'Unknown'
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-item">
|
<div class="meta-item">
|
||||||
<span class="label">Created:</span>
|
<span class="label">Created:</span>
|
||||||
|
@ -316,22 +316,23 @@
|
|||||||
<VAlert v-else-if="costSummaryError" type="error" :message="costSummaryError" />
|
<VAlert v-else-if="costSummaryError" type="error" :message="costSummaryError" />
|
||||||
<div v-else-if="listCostSummary">
|
<div v-else-if="listCostSummary">
|
||||||
<div class="mb-3 cost-overview">
|
<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>
|
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>
|
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>
|
listCostSummary.num_participating_users }}</p>
|
||||||
</div>
|
</div>
|
||||||
<h4>{{ $t('listDetailPage.costSummaryModal.userBalancesHeader') }}</h4>
|
<h4>{{ $t('listDetailPage.modals.costSummary.userBalancesHeader') }}</h4>
|
||||||
<div class="table-container mt-2">
|
<div class="table-container mt-2">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ $t('listDetailPage.costSummaryModal.tableHeaders.user') }}</th>
|
<th>{{ $t('listDetailPage.modals.costSummary.tableHeaders.user') }}</th>
|
||||||
<th class="text-right">{{ $t('listDetailPage.costSummaryModal.tableHeaders.itemsAddedValue') }}</th>
|
<th class="text-right">{{ $t('listDetailPage.modals.costSummary.tableHeaders.itemsAddedValue') }}
|
||||||
<th class="text-right">{{ $t('listDetailPage.costSummaryModal.tableHeaders.amountDue') }}</th>
|
</th>
|
||||||
<th class="text-right">{{ $t('listDetailPage.costSummaryModal.tableHeaders.balance') }}</th>
|
<th class="text-right">{{ $t('listDetailPage.modals.costSummary.tableHeaders.amountDue') }}</th>
|
||||||
|
<th class="text-right">{{ $t('listDetailPage.modals.costSummary.tableHeaders.balance') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -348,7 +349,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-else>{{ $t('listDetailPage.costSummaryModal.emptyState') }}</p>
|
<p v-else>{{ $t('listDetailPage.modals.costSummary.emptyState') }}</p>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VButton variant="primary" @click="showCostSummaryDialog = false">{{ $t('listDetailPage.buttons.close') }}
|
<VButton variant="primary" @click="showCostSummaryDialog = false">{{ $t('listDetailPage.buttons.close') }}
|
||||||
@ -357,7 +358,7 @@
|
|||||||
</VModal>
|
</VModal>
|
||||||
|
|
||||||
<!-- Settle Share Modal -->
|
<!-- 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">
|
@update:modelValue="!$event && closeSettleShareModal()" size="md">
|
||||||
<template #default>
|
<template #default>
|
||||||
<div v-if="isSettlementLoading" class="text-center">
|
<div v-if="isSettlementLoading" class="text-center">
|
||||||
@ -365,11 +366,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<VAlert v-else-if="settleAmountError" type="error" :message="settleAmountError" />
|
<VAlert v-else-if="settleAmountError" type="error" :message="settleAmountError" />
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p>{{ $t('listDetailPage.settleShareModal.settleAmountFor', {
|
<p>{{ $t('listDetailPage.modals.settleShare.settleAmountFor', {
|
||||||
userName: selectedSplitForSettlement?.user?.name
|
userName: selectedSplitForSettlement?.user?.name
|
||||||
|| selectedSplitForSettlement?.user?.email || `User ID: ${selectedSplitForSettlement?.user_id}`
|
|| selectedSplitForSettlement?.user?.email || `User ID: ${selectedSplitForSettlement?.user_id}`
|
||||||
}) }}</p>
|
}) }}</p>
|
||||||
<VFormField :label="$t('listDetailPage.settleShareModal.amountLabel')"
|
<VFormField :label="$t('listDetailPage.modals.settleShare.amountLabel')"
|
||||||
:error-message="settleAmountError || undefined">
|
:error-message="settleAmountError || undefined">
|
||||||
<VInput type="number" v-model="settleAmount" id="settleAmount" required />
|
<VInput type="number" v-model="settleAmount" id="settleAmount" required />
|
||||||
</VFormField>
|
</VFormField>
|
||||||
@ -377,9 +378,10 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VButton variant="neutral" @click="closeSettleShareModal">{{
|
<VButton variant="neutral" @click="closeSettleShareModal">{{
|
||||||
$t('listDetailPage.settleShareModal.cancelButton')
|
$t('listDetailPage.modals.settleShare.cancelButton')
|
||||||
}}</VButton>
|
}}</VButton>
|
||||||
<VButton variant="primary" @click="handleConfirmSettle">{{ $t('listDetailPage.settleShareModal.confirmButton')
|
<VButton variant="primary" @click="handleConfirmSettle">{{
|
||||||
|
$t('listDetailPage.modals.settleShare.confirmButton')
|
||||||
}}</VButton>
|
}}</VButton>
|
||||||
</template>
|
</template>
|
||||||
</VModal>
|
</VModal>
|
||||||
@ -1215,12 +1217,12 @@ const closeSettleShareModal = () => {
|
|||||||
const validateSettleAmount = (): boolean => {
|
const validateSettleAmount = (): boolean => {
|
||||||
settleAmountError.value = null;
|
settleAmountError.value = null;
|
||||||
if (!settleAmount.value.trim()) {
|
if (!settleAmount.value.trim()) {
|
||||||
settleAmountError.value = t('listDetailPage.settleShareModal.errors.enterAmount');
|
settleAmountError.value = t('listDetailPage.modals.settleShare.errors.enterAmount');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const amount = new Decimal(settleAmount.value);
|
const amount = new Decimal(settleAmount.value);
|
||||||
if (amount.isNaN() || amount.isNegative() || amount.isZero()) {
|
if (amount.isNaN() || amount.isNegative() || amount.isZero()) {
|
||||||
settleAmountError.value = t('listDetailPage.settleShareModal.errors.positiveAmount');
|
settleAmountError.value = t('listDetailPage.modals.settleShare.errors.positiveAmount');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (selectedSplitForSettlement.value) {
|
if (selectedSplitForSettlement.value) {
|
||||||
@ -1228,11 +1230,11 @@ const validateSettleAmount = (): boolean => {
|
|||||||
const owed = new Decimal(selectedSplitForSettlement.value.owed_amount);
|
const owed = new Decimal(selectedSplitForSettlement.value.owed_amount);
|
||||||
const remaining = owed.minus(alreadyPaid);
|
const remaining = owed.minus(alreadyPaid);
|
||||||
if (amount.greaterThan(remaining.plus(new Decimal('0.001')))) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
settleAmountError.value = t('listDetailPage.settleShareModal.errors.noSplitSelected');
|
settleAmountError.value = t('listDetailPage.modals.settleShare.errors.noSplitSelected');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -29,8 +29,8 @@
|
|||||||
<div class="neo-lists-grid">
|
<div class="neo-lists-grid">
|
||||||
<div v-for="list in lists" :key="list.id" class="neo-list-card"
|
<div v-for="list in lists" :key="list.id" class="neo-list-card"
|
||||||
:class="{ 'touch-active': touchActiveListId === list.id }" @click="navigateToList(list.id)"
|
:class="{ 'touch-active': touchActiveListId === list.id }" @click="navigateToList(list.id)"
|
||||||
@touchstart="handleTouchStart(list.id)" @touchend="handleTouchEnd" @touchcancel="handleTouchEnd"
|
@touchstart.passive="handleTouchStart(list.id)" @touchend.passive="handleTouchEnd"
|
||||||
:data-list-id="list.id">
|
@touchcancel.passive="handleTouchEnd" :data-list-id="list.id">
|
||||||
<div class="neo-list-header">{{ list.name }}</div>
|
<div class="neo-list-header">{{ list.name }}</div>
|
||||||
<div class="neo-list-desc">{{ list.description || t('listsPage.noDescription') }}</div>
|
<div class="neo-list-desc">{{ list.description || t('listsPage.noDescription') }}</div>
|
||||||
<ul class="neo-item-list">
|
<ul class="neo-item-list">
|
||||||
|
Loading…
Reference in New Issue
Block a user