Updates
This commit is contained in:
parent
28040f44f4
commit
2394248d53
13 changed files with 571 additions and 538 deletions
|
|
@ -28,7 +28,7 @@
|
|||
<!-- Search / Filter -->
|
||||
<div class="filter-bar">
|
||||
<div class="field" style="margin-bottom: 0; flex: 1">
|
||||
<input v-model="searchQuery" placeholder="Search members..." />
|
||||
<input v-model="searchQuery" placeholder="Search members..." >
|
||||
</div>
|
||||
<div class="field" style="margin-bottom: 0">
|
||||
<select v-model="circleFilter" aria-label="Filter by circle">
|
||||
|
|
@ -38,6 +38,15 @@
|
|||
<option value="practitioner">Practitioner</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom: 0">
|
||||
<select v-model="statusFilter" aria-label="Filter by status">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="pending_payment">Pending Payment</option>
|
||||
<option value="suspended">Suspended</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Members Table -->
|
||||
|
|
@ -61,17 +70,18 @@
|
|||
:checked="allVisibleSelected"
|
||||
:indeterminate="!allVisibleSelected && someVisibleSelected"
|
||||
@change="toggleSelectAll"
|
||||
/>
|
||||
>
|
||||
<span class="check-mark" />
|
||||
</label>
|
||||
</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Circle</th>
|
||||
<th>Tier</th>
|
||||
<th class="sortable" @click="toggleSort('name')">Name <span class="sort-ind">{{ sortIndicator('name') }}</span></th>
|
||||
<th class="sortable" @click="toggleSort('email')">Email <span class="sort-ind">{{ sortIndicator('email') }}</span></th>
|
||||
<th class="sortable" @click="toggleSort('circle')">Circle <span class="sort-ind">{{ sortIndicator('circle') }}</span></th>
|
||||
<th class="sortable" @click="toggleSort('contributionTier')">Tier <span class="sort-ind">{{ sortIndicator('contributionTier') }}</span></th>
|
||||
<th class="sortable" @click="toggleSort('status')">Status <span class="sort-ind">{{ sortIndicator('status') }}</span></th>
|
||||
<th>Invite</th>
|
||||
<th>Slack</th>
|
||||
<th>Joined</th>
|
||||
<th class="sortable" @click="toggleSort('createdAt')">Joined <span class="sort-ind">{{ sortIndicator('createdAt') }}</span></th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -89,7 +99,7 @@
|
|||
type="checkbox"
|
||||
:checked="selectedMemberIds.includes(member._id)"
|
||||
@change="toggleSelect(member._id)"
|
||||
/>
|
||||
>
|
||||
<span class="check-mark" />
|
||||
</label>
|
||||
</td>
|
||||
|
|
@ -103,6 +113,9 @@
|
|||
}}</span>
|
||||
</td>
|
||||
<td class="col-mono">${{ member.contributionTier }}/mo</td>
|
||||
<td>
|
||||
<span class="badge status" :class="`status-${member.status || 'pending_payment'}`">{{ statusLabel(member.status) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
:class="member.inviteEmailSent ? 'status-ok' : 'status-dim'"
|
||||
|
|
@ -122,10 +135,10 @@
|
|||
<NuxtLink :to="`/admin/members/${member._id}`" class="link-btn" @click.stop
|
||||
>View</NuxtLink
|
||||
>
|
||||
<button @click.stop="sendSlackInvite(member)" class="link-btn">
|
||||
<button class="link-btn" @click.stop="sendSlackInvite(member)">
|
||||
Slack
|
||||
</button>
|
||||
<button @click.stop="editMember(member)" class="link-btn">Edit</button>
|
||||
<button class="link-btn" @click.stop="editMember(member)">Edit</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
@ -150,10 +163,10 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="createMember" class="modal-body">
|
||||
<form class="modal-body" @submit.prevent="createMember">
|
||||
<div class="field">
|
||||
<label>Name</label>
|
||||
<input v-model="newMember.name" placeholder="Full name" required />
|
||||
<input v-model="newMember.name" placeholder="Full name" required >
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Email</label>
|
||||
|
|
@ -162,7 +175,7 @@
|
|||
type="email"
|
||||
placeholder="email@example.com"
|
||||
required
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Circle</label>
|
||||
|
|
@ -220,9 +233,9 @@
|
|||
ref="csvFileInput"
|
||||
type="file"
|
||||
accept=".csv"
|
||||
@change="handleCsvFile"
|
||||
class="file-input"
|
||||
/>
|
||||
@change="handleCsvFile"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="csvParseError" class="error-box">
|
||||
|
|
@ -243,7 +256,7 @@
|
|||
>
|
||||
{{ csvRows.length - csvValidRows.length }} with errors.
|
||||
</span>
|
||||
<button @click="resetCsvImport" class="link-btn">
|
||||
<button class="link-btn" @click="resetCsvImport">
|
||||
Choose different file
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -303,14 +316,14 @@
|
|||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button @click="closeImportModal" class="btn">
|
||||
<button class="btn" @click="closeImportModal">
|
||||
{{ importResults ? "Done" : "Cancel" }}
|
||||
</button>
|
||||
<button
|
||||
v-if="csvValidRows.length && !importResults"
|
||||
:disabled="importing"
|
||||
@click="submitImport"
|
||||
class="btn btn-primary"
|
||||
@click="submitImport"
|
||||
>
|
||||
{{
|
||||
importing
|
||||
|
|
@ -336,14 +349,14 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitEditMember" class="modal-body">
|
||||
<form class="modal-body" @submit.prevent="submitEditMember">
|
||||
<div class="field">
|
||||
<label>Name</label>
|
||||
<input v-model="editingMember.name" required />
|
||||
<input v-model="editingMember.name" required >
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Email</label>
|
||||
<input v-model="editingMember.email" type="email" required />
|
||||
<input v-model="editingMember.email" type="email" required >
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Circle</label>
|
||||
|
|
@ -407,7 +420,7 @@
|
|||
|
||||
<div class="field">
|
||||
<label>Email Template</label>
|
||||
<textarea v-model="inviteTemplate" rows="12"></textarea>
|
||||
<textarea v-model="inviteTemplate" rows="12"/>
|
||||
<p class="help-text" style="margin-top: 4px">
|
||||
Tokens: <code>{name}</code>, <code>{loginLink}</code>,
|
||||
<code>{circle}</code>
|
||||
|
|
@ -439,14 +452,14 @@
|
|||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button @click="showInviteModal = false" class="btn">
|
||||
<button class="btn" @click="showInviteModal = false">
|
||||
{{ inviteResults ? "Done" : "Cancel" }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!inviteResults"
|
||||
:disabled="sendingInvites"
|
||||
@click="submitInvites"
|
||||
class="btn btn-primary"
|
||||
@click="submitInvites"
|
||||
>
|
||||
{{
|
||||
sendingInvites
|
||||
|
|
@ -477,6 +490,30 @@ const {
|
|||
|
||||
const searchQuery = ref("");
|
||||
const circleFilter = ref("");
|
||||
const statusFilter = ref("");
|
||||
const sortKey = ref("createdAt");
|
||||
const sortDir = ref("desc");
|
||||
|
||||
const STATUS_LABELS = {
|
||||
active: "Active",
|
||||
pending_payment: "Pending",
|
||||
suspended: "Suspended",
|
||||
cancelled: "Cancelled",
|
||||
};
|
||||
const statusLabel = (s) => STATUS_LABELS[s] || "Pending";
|
||||
|
||||
const toggleSort = (key) => {
|
||||
if (sortKey.value === key) {
|
||||
sortDir.value = sortDir.value === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
sortKey.value = key;
|
||||
sortDir.value = key === "createdAt" ? "desc" : "asc";
|
||||
}
|
||||
};
|
||||
const sortIndicator = (key) => {
|
||||
if (sortKey.value !== key) return "";
|
||||
return sortDir.value === "asc" ? "▲" : "▼";
|
||||
};
|
||||
const showCreateModal = ref(false);
|
||||
const creating = ref(false);
|
||||
|
||||
|
|
@ -519,7 +556,7 @@ const newMember = reactive({
|
|||
const filteredMembers = computed(() => {
|
||||
if (!members.value) return [];
|
||||
|
||||
return members.value.filter((member) => {
|
||||
const filtered = members.value.filter((member) => {
|
||||
const matchesSearch =
|
||||
!searchQuery.value ||
|
||||
member.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
|
|
@ -528,7 +565,33 @@ const filteredMembers = computed(() => {
|
|||
const matchesCircle =
|
||||
!circleFilter.value || member.circle === circleFilter.value;
|
||||
|
||||
return matchesSearch && matchesCircle;
|
||||
const matchesStatus =
|
||||
!statusFilter.value || (member.status || "pending_payment") === statusFilter.value;
|
||||
|
||||
return matchesSearch && matchesCircle && matchesStatus;
|
||||
});
|
||||
|
||||
const key = sortKey.value;
|
||||
const dir = sortDir.value === "asc" ? 1 : -1;
|
||||
return [...filtered].sort((a, b) => {
|
||||
let av = a[key];
|
||||
let bv = b[key];
|
||||
if (key === "contributionTier") {
|
||||
av = Number(av) || 0;
|
||||
bv = Number(bv) || 0;
|
||||
} else if (key === "createdAt") {
|
||||
av = av ? new Date(av).getTime() : 0;
|
||||
bv = bv ? new Date(bv).getTime() : 0;
|
||||
} else if (key === "status") {
|
||||
av = a.status || "pending_payment";
|
||||
bv = b.status || "pending_payment";
|
||||
} else {
|
||||
av = (av || "").toString().toLowerCase();
|
||||
bv = (bv || "").toString().toLowerCase();
|
||||
}
|
||||
if (av < bv) return -1 * dir;
|
||||
if (av > bv) return 1 * dir;
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1055,6 +1118,44 @@ tbody td {
|
|||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* ---- SORTABLE HEADERS ---- */
|
||||
th.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
th.sortable:hover {
|
||||
color: var(--candle);
|
||||
}
|
||||
.sort-ind {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
font-size: 9px;
|
||||
color: var(--candle);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* ---- MEMBER STATUS BADGES ---- */
|
||||
.badge.status {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.badge.status-active {
|
||||
color: var(--green, #3a6b3a);
|
||||
border-color: rgba(58, 107, 58, 0.45);
|
||||
}
|
||||
.badge.status-pending_payment {
|
||||
color: var(--text-dim);
|
||||
border-color: var(--border);
|
||||
}
|
||||
.badge.status-suspended {
|
||||
color: var(--ember);
|
||||
border-color: rgba(138, 68, 32, 0.45);
|
||||
}
|
||||
.badge.status-cancelled {
|
||||
color: var(--text-faint);
|
||||
border-color: var(--border);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ---- MODALS ---- */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue