Skip to content

Commit 91f2ef4

Browse files
committed
Community Records and Sorting Entitlement Requests
1 parent 0fe6e28 commit 91f2ef4

3 files changed

Lines changed: 179 additions & 89 deletions

File tree

src/routes/(protected)/dynamic-entities/personal/[entityName]/+page.server.ts

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ export const load: PageServerLoad = async ({ params, locals }) => {
5757
throw error(404, `Personal dynamic entity "${entityName}" not found`);
5858
}
5959

60-
// Fetch records for all applicable scopes in parallel
60+
logger.info(`Entity fields for ${entityName}: ${JSON.stringify(Object.keys(entity))}`);
61+
logger.info(`has_public_access: ${entity.has_public_access}, has_community_access: ${entity.has_community_access}, hasPublicAccess: ${entity.hasPublicAccess}, hasCommunityAccess: ${entity.hasCommunityAccess}`);
62+
63+
// Fetch records for all scopes in parallel
64+
// Always attempt all scopes - the API will return errors for unsupported ones
6165
const fetchPromises: Record<string, Promise<any>> = {};
6266

6367
// My records: always fetch
@@ -68,31 +72,25 @@ export const load: PageServerLoad = async ({ params, locals }) => {
6872
return { _error: err instanceof Error ? err.message : "Failed to fetch my records" };
6973
});
7074

71-
// Community records: only if has_community_access
72-
if (entity.has_community_access) {
73-
fetchPromises.community = obp_requests
74-
.get(`/obp/dynamic-entity/community/${entityName}`, accessToken)
75-
.catch((err) => {
76-
logger.warn("Could not fetch community records:", err);
77-
return { _error: err instanceof Error ? err.message : "Failed to fetch community records" };
78-
});
79-
}
75+
// Community records: always attempt
76+
fetchPromises.community = obp_requests
77+
.get(`/obp/dynamic-entity/community/${entityName}`, accessToken)
78+
.catch((err) => {
79+
logger.warn("Could not fetch community records:", err);
80+
return { _error: err instanceof Error ? err.message : "Failed to fetch community records" };
81+
});
8082

81-
// Public records: only if has_public_access
82-
// Note: public endpoints don't require auth, but we still proxy through
83-
// our server for consistency. Sending a token is harmless.
84-
if (entity.has_public_access) {
85-
fetchPromises.public = obp_requests
86-
.get(`/obp/dynamic-entity/public/${entityName}`, accessToken)
87-
.then((res) => {
88-
logger.info(`Public response keys for ${entityName}: ${JSON.stringify(Object.keys(res))}`);
89-
return res;
90-
})
91-
.catch((err) => {
92-
logger.warn("Could not fetch public records:", err);
93-
return { _error: err instanceof Error ? err.message : "Failed to fetch public records" };
94-
});
95-
}
83+
// Public records: always attempt
84+
fetchPromises.public = obp_requests
85+
.get(`/obp/dynamic-entity/public/${entityName}`, accessToken)
86+
.then((res) => {
87+
logger.info(`Public response keys for ${entityName}: ${JSON.stringify(Object.keys(res))}`);
88+
return res;
89+
})
90+
.catch((err) => {
91+
logger.warn("Could not fetch public records:", err);
92+
return { _error: err instanceof Error ? err.message : "Failed to fetch public records" };
93+
});
9694

9795
// Await all in parallel
9896
const results: Record<string, any> = {};
@@ -109,7 +107,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
109107

110108
let communityRecords: any[] = [];
111109
let communityError: string | null = null;
112-
if (entity.has_community_access && results.community) {
110+
if (results.community) {
113111
if (results.community._error) {
114112
communityError = results.community._error;
115113
} else {
@@ -119,7 +117,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
119117

120118
let publicRecords: any[] = [];
121119
let publicError: string | null = null;
122-
if (entity.has_public_access && results.public) {
120+
if (results.public) {
123121
if (results.public._error) {
124122
publicError = results.public._error;
125123
} else {

src/routes/(protected)/dynamic-entities/personal/[entityName]/+page.svelte

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -536,43 +536,39 @@
536536
</span>
537537
</button>
538538

539-
{#if entity.has_community_access}
540-
<button
541-
type="button"
542-
onclick={() => (activeTab = "community")}
543-
class="flex items-center gap-2 border-b-2 px-1 py-3 text-sm font-medium transition-colors {activeTab === 'community'
544-
? 'border-blue-500 text-blue-600 dark:text-blue-400'
545-
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'}"
539+
<button
540+
type="button"
541+
onclick={() => (activeTab = "community")}
542+
class="flex items-center gap-2 border-b-2 px-1 py-3 text-sm font-medium transition-colors {activeTab === 'community'
543+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
544+
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'}"
545+
>
546+
Community Records
547+
<span
548+
class="rounded-full px-2 py-0.5 text-xs font-medium {activeTab === 'community'
549+
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
550+
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400'}"
546551
>
547-
Community Records
548-
<span
549-
class="rounded-full px-2 py-0.5 text-xs font-medium {activeTab === 'community'
550-
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
551-
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400'}"
552-
>
553-
{communityRecords.length}
554-
</span>
555-
</button>
556-
{/if}
552+
{communityRecords.length}
553+
</span>
554+
</button>
557555

558-
{#if entity.has_public_access}
559-
<button
560-
type="button"
561-
onclick={() => (activeTab = "public")}
562-
class="flex items-center gap-2 border-b-2 px-1 py-3 text-sm font-medium transition-colors {activeTab === 'public'
563-
? 'border-purple-500 text-purple-600 dark:text-purple-400'
564-
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'}"
556+
<button
557+
type="button"
558+
onclick={() => (activeTab = "public")}
559+
class="flex items-center gap-2 border-b-2 px-1 py-3 text-sm font-medium transition-colors {activeTab === 'public'
560+
? 'border-purple-500 text-purple-600 dark:text-purple-400'
561+
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'}"
562+
>
563+
Public Records
564+
<span
565+
class="rounded-full px-2 py-0.5 text-xs font-medium {activeTab === 'public'
566+
? 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'
567+
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400'}"
565568
>
566-
Public Records
567-
<span
568-
class="rounded-full px-2 py-0.5 text-xs font-medium {activeTab === 'public'
569-
? 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'
570-
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400'}"
571-
>
572-
{publicRecords.length}
573-
</span>
574-
</button>
575-
{/if}
569+
{publicRecords.length}
570+
</span>
571+
</button>
576572
</nav>
577573
</div>
578574

src/routes/(protected)/rbac/entitlement-requests/+page.svelte

Lines changed: 124 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
Clock,
88
User,
99
Calendar,
10+
ArrowUpDown,
1011
} from "@lucide/svelte";
1112
import { toast } from "$lib/utils/toastService";
1213
import PageRoleCheck from "$lib/components/PageRoleCheck.svelte";
@@ -35,19 +36,40 @@
3536
// Search state
3637
let searchQuery = $state("");
3738
39+
// Sort state
40+
let sortOption = $state("newest");
41+
3842
// Filter requests based on search query
3943
let filteredRequests = $derived.by(() => {
40-
if (!searchQuery.trim()) {
41-
return entitlementRequests;
44+
let results = entitlementRequests;
45+
if (searchQuery.trim()) {
46+
const query = searchQuery.toLowerCase().replace(/\s+/g, "");
47+
results = results.filter(
48+
(request: EntitlementRequest) =>
49+
request.role_name.toLowerCase().includes(query) ||
50+
request.user.username.toLowerCase().includes(query) ||
51+
request.user.email.toLowerCase().includes(query) ||
52+
(request.bank_id && request.bank_id.toLowerCase().includes(query)),
53+
);
4254
}
43-
const query = searchQuery.toLowerCase().replace(/\s+/g, "");
44-
return entitlementRequests.filter(
45-
(request: EntitlementRequest) =>
46-
request.role_name.toLowerCase().includes(query) ||
47-
request.user.username.toLowerCase().includes(query) ||
48-
request.user.email.toLowerCase().includes(query) ||
49-
(request.bank_id && request.bank_id.toLowerCase().includes(query)),
50-
);
55+
56+
// Sort
57+
const sorted = [...results];
58+
switch (sortOption) {
59+
case "newest":
60+
sorted.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
61+
break;
62+
case "oldest":
63+
sorted.sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
64+
break;
65+
case "role_asc":
66+
sorted.sort((a, b) => a.role_name.localeCompare(b.role_name));
67+
break;
68+
case "username":
69+
sorted.sort((a, b) => a.user.username.localeCompare(b.user.username));
70+
break;
71+
}
72+
return sorted;
5173
});
5274
5375
// Helper function to format date
@@ -270,25 +292,36 @@
270292
</div>
271293
</div>
272294

273-
<!-- Search Box -->
295+
<!-- Search and Sort -->
274296
<div class="search-container">
275-
<div class="search-input-wrapper">
276-
<Search class="search-icon" size={20} />
277-
<input
278-
type="text"
279-
bind:value={searchQuery}
280-
placeholder="Search by role, user, or bank..."
281-
class="search-input"
282-
/>
283-
{#if searchQuery}
284-
<button
285-
class="clear-button"
286-
onclick={() => (searchQuery = "")}
287-
aria-label="Clear search"
288-
>
289-
×
290-
</button>
291-
{/if}
297+
<div class="search-sort-row">
298+
<div class="search-input-wrapper">
299+
<Search class="search-icon" size={20} />
300+
<input
301+
type="text"
302+
bind:value={searchQuery}
303+
placeholder="Search by role, user, or bank..."
304+
class="search-input"
305+
/>
306+
{#if searchQuery}
307+
<button
308+
class="clear-button"
309+
onclick={() => (searchQuery = "")}
310+
aria-label="Clear search"
311+
>
312+
×
313+
</button>
314+
{/if}
315+
</div>
316+
<div class="sort-wrapper">
317+
<ArrowUpDown size={16} class="sort-icon" />
318+
<select bind:value={sortOption} class="sort-select">
319+
<option value="newest">Most Recent First</option>
320+
<option value="oldest">Oldest First</option>
321+
<option value="role_asc">Role Name (A–Z)</option>
322+
<option value="username">Username (A–Z)</option>
323+
</select>
324+
</div>
292325
</div>
293326
{#if searchQuery}
294327
<div class="search-results-info">
@@ -520,11 +553,65 @@
520553
border-bottom-color: rgb(var(--color-surface-700));
521554
}
522555
556+
.search-sort-row {
557+
display: flex;
558+
align-items: center;
559+
gap: 1rem;
560+
}
561+
523562
.search-input-wrapper {
524563
position: relative;
525564
display: flex;
526565
align-items: center;
527566
max-width: 33.333%;
567+
flex: 1;
568+
}
569+
570+
.sort-wrapper {
571+
position: relative;
572+
display: flex;
573+
align-items: center;
574+
flex-shrink: 0;
575+
}
576+
577+
.sort-wrapper :global(.sort-icon) {
578+
position: absolute;
579+
left: 0.75rem;
580+
color: #6b7280;
581+
pointer-events: none;
582+
}
583+
584+
:global([data-mode="dark"]) .sort-wrapper :global(.sort-icon) {
585+
color: var(--color-surface-400);
586+
}
587+
588+
.sort-select {
589+
padding: 0.75rem 1rem 0.75rem 2.25rem;
590+
border: 1px solid #d1d5db;
591+
border-radius: 8px;
592+
font-size: 0.875rem;
593+
background: white;
594+
color: #111827;
595+
cursor: pointer;
596+
transition: all 0.2s;
597+
appearance: auto;
598+
}
599+
600+
.sort-select:focus {
601+
outline: none;
602+
border-color: #3b82f6;
603+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
604+
}
605+
606+
:global([data-mode="dark"]) .sort-select {
607+
background: rgb(var(--color-surface-700));
608+
border-color: rgb(var(--color-surface-600));
609+
color: var(--color-surface-100);
610+
}
611+
612+
:global([data-mode="dark"]) .sort-select:focus {
613+
border-color: rgb(var(--color-primary-500));
614+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
528615
}
529616
530617
.search-input-wrapper :global(.search-icon) {
@@ -888,10 +975,19 @@
888975
width: 100%;
889976
}
890977
978+
.search-sort-row {
979+
flex-direction: column;
980+
align-items: stretch;
981+
}
982+
891983
.search-input-wrapper {
892984
max-width: 100%;
893985
}
894986
987+
.sort-select {
988+
width: 100%;
989+
}
990+
895991
.request-body {
896992
flex-direction: column;
897993
align-items: flex-start;

0 commit comments

Comments
 (0)