Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit 48af416

Browse files
committed
Chat Room creation
1 parent f755d73 commit 48af416

15 files changed

Lines changed: 1544 additions & 2 deletions

File tree

src/lib/config/navigation.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
Link,
4343
Zap,
4444
ShieldOff,
45+
MessageSquare,
4546
} from "@lucide/svelte";
4647
import { env } from "$env/dynamic/public";
4748

@@ -668,6 +669,37 @@ export function getActiveCustomersMenuItem(pathname: string) {
668669
return found || customersItems[0];
669670
}
670671

672+
// Chat Rooms navigation items
673+
function buildChatRoomsItems(): NavigationItem[] {
674+
const items: NavigationItem[] = [
675+
{
676+
href: "/chat-rooms/system",
677+
label: "System",
678+
iconComponent: Settings,
679+
},
680+
{
681+
href: "/chat-rooms/bank",
682+
label: "Bank",
683+
iconComponent: Building2,
684+
},
685+
];
686+
687+
return items;
688+
}
689+
690+
export const chatRoomsItems = buildChatRoomsItems();
691+
692+
export function getActiveChatRoomsMenuItem(pathname: string) {
693+
const found = chatRoomsItems.find((item) => {
694+
if (item.external) {
695+
return false;
696+
}
697+
return pathname.startsWith(item.href);
698+
});
699+
700+
return found || chatRoomsItems[0];
701+
}
702+
671703
export const navSections: NavigationSection[] = [
672704
{ id: "my-account", label: "My Profile", iconComponent: User, items: myAccountItems, basePaths: ["/user", "/account-access/accounts"] },
673705
{ id: "system", label: "System", iconComponent: Server, items: systemItems, basePaths: ["/system"] },
@@ -684,5 +716,6 @@ export const navSections: NavigationSection[] = [
684716
{ id: "account-access", label: "Account Access", iconComponent: Landmark, items: accountAccessItems, basePaths: ["/account-access", "/mandates"] },
685717
{ id: "dynamic-entities", label: "Dynamic Entities", iconComponent: Box, items: dynamicEntitiesItems, basePaths: ["/dynamic-entities"] },
686718
{ id: "dynamic-endpoints", label: "Dynamic Endpoints", iconComponent: Plug, items: dynamicEndpointsItems, basePaths: ["/dynamic-endpoints"] },
719+
{ id: "chat-rooms", label: "Chat Rooms", iconComponent: MessageSquare, items: chatRoomsItems, basePaths: ["/chat-rooms"] },
687720
{ id: "management-docs", label: "Management Docs", iconComponent: BookOpen, items: managementDocsItems, basePaths: ["/management-docs"] },
688721
];

src/lib/obp/types.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,33 @@ export interface OBPWebUIPropRequestBody {
9999
name: string;
100100
value: string;
101101
}
102+
103+
export interface OBPChatRoom {
104+
chat_room_id: string;
105+
bank_id: string;
106+
name: string;
107+
description: string;
108+
joining_key: string;
109+
created_by: string;
110+
created_by_username: string;
111+
created_by_provider: string;
112+
all_users_are_participants: boolean;
113+
is_archived: boolean;
114+
created_at: string;
115+
updated_at: string;
116+
}
117+
118+
export interface OBPChatRoomParticipant {
119+
participant_id: string;
120+
chat_room_id: string;
121+
user_id: string;
122+
username: string;
123+
provider: string;
124+
consumer_id: string;
125+
consumer_name: string;
126+
permissions: string[];
127+
webhook_url: string;
128+
joined_at: string;
129+
last_read_at: string;
130+
is_muted: boolean;
131+
}

src/lib/utils/roleChecker.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,24 @@ export const SITE_MAP: Record<string, PageRoleConfig> = {
289289
],
290290
},
291291

292+
// ── Chat Rooms ────────────────────────────────────────
293+
"/chat-rooms/system": {
294+
required: [],
295+
},
296+
"/chat-rooms/bank": {
297+
required: [],
298+
},
299+
"/chat-rooms/[chat_room_id]": {
300+
required: [],
301+
},
302+
"/chat-rooms/[chat_room_id]/edit": {
303+
required: [
304+
{ role: "CanSetSystemChatRoomAUAP" },
305+
{ role: "CanSetBankChatRoomAUAP", bankScoped: true },
306+
],
307+
requirementType: "OR",
308+
},
309+
292310
// ── Dynamic Entities ──────────────────────────────────
293311
"/dynamic-entities/diagnostics": {
294312
required: [{ role: "CanGetSystemLevelDynamicEntities" }],
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
<script lang="ts">
2+
import { page } from "$app/state";
3+
import { trackedFetch } from "$lib/utils/trackedFetch";
4+
import type { OBPChatRoomParticipant } from "$lib/obp/types";
5+
6+
const chatRoomId = $derived(page.params.chat_room_id);
7+
const level = $derived(page.url.searchParams.get("level") || "system");
8+
const bankId = $derived(page.url.searchParams.get("bank_id") || "");
9+
10+
let participants = $state<OBPChatRoomParticipant[]>([]);
11+
let loading = $state(false);
12+
let error = $state<string | null>(null);
13+
let successMessage = $state<string | null>(null);
14+
15+
let showAddForm = $state(false);
16+
let addUserId = $state("");
17+
let addPermissions = $state("can_send_message");
18+
19+
async function fetchParticipants() {
20+
if (!chatRoomId) return;
21+
loading = true;
22+
error = null;
23+
try {
24+
const bankParam = bankId ? `&bank_id=${encodeURIComponent(bankId)}` : "";
25+
const res = await trackedFetch(
26+
`/api/obp/chat-rooms/${encodeURIComponent(chatRoomId)}/participants?level=${level}${bankParam}`,
27+
);
28+
if (!res.ok) {
29+
const data = await res.json().catch(() => ({}));
30+
throw new Error(data.error || "Failed to fetch participants");
31+
}
32+
const data = await res.json();
33+
participants = data.participants || [];
34+
} catch (err) {
35+
error = err instanceof Error ? err.message : "Failed to fetch participants";
36+
participants = [];
37+
} finally {
38+
loading = false;
39+
}
40+
}
41+
42+
async function addParticipant() {
43+
if (!addUserId.trim() || !chatRoomId) return;
44+
error = null;
45+
successMessage = null;
46+
try {
47+
const bankParam = bankId ? `&bank_id=${encodeURIComponent(bankId)}` : "";
48+
const res = await trackedFetch(
49+
`/api/obp/chat-rooms/${encodeURIComponent(chatRoomId)}/participants?level=${level}${bankParam}`,
50+
{
51+
method: "POST",
52+
headers: { "Content-Type": "application/json" },
53+
body: JSON.stringify({
54+
user_id: addUserId,
55+
permissions: addPermissions.split(",").map((p) => p.trim()).filter(Boolean),
56+
}),
57+
},
58+
);
59+
if (!res.ok) {
60+
const data = await res.json().catch(() => ({}));
61+
throw new Error(data.error || "Failed to add participant");
62+
}
63+
successMessage = "Participant added.";
64+
addUserId = "";
65+
showAddForm = false;
66+
fetchParticipants();
67+
} catch (err) {
68+
error = err instanceof Error ? err.message : "Failed to add participant";
69+
}
70+
}
71+
72+
async function removeParticipant(userId: string) {
73+
if (!chatRoomId) return;
74+
error = null;
75+
successMessage = null;
76+
try {
77+
const bankParam = bankId ? `&bank_id=${encodeURIComponent(bankId)}` : "";
78+
const res = await trackedFetch(
79+
`/api/obp/chat-rooms/${encodeURIComponent(chatRoomId)}/participants?user_id=${encodeURIComponent(userId)}&level=${level}${bankParam}`,
80+
{ method: "DELETE" },
81+
);
82+
if (!res.ok) {
83+
const data = await res.json().catch(() => ({}));
84+
throw new Error(data.error || "Failed to remove participant");
85+
}
86+
successMessage = "Participant removed.";
87+
fetchParticipants();
88+
} catch (err) {
89+
error = err instanceof Error ? err.message : "Failed to remove participant";
90+
}
91+
}
92+
93+
function formatDate(dateString: string): string {
94+
try {
95+
return new Date(dateString).toLocaleString(undefined, {
96+
year: "numeric",
97+
month: "short",
98+
day: "numeric",
99+
hour: "2-digit",
100+
minute: "2-digit",
101+
});
102+
} catch {
103+
return "Unknown";
104+
}
105+
}
106+
107+
$effect(() => {
108+
fetchParticipants();
109+
});
110+
</script>
111+
112+
<div class="mb-4">
113+
<a
114+
href="/chat-rooms/{level}"
115+
class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
116+
data-testid="back-link"
117+
>
118+
&larr; Back to {level === "bank" ? "Bank" : "System"} Chat Rooms
119+
</a>
120+
</div>
121+
122+
<div class="flex items-center justify-between mb-4">
123+
<div>
124+
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
125+
Chat Room Participants
126+
</h1>
127+
<p class="text-sm text-gray-600 dark:text-gray-400 font-mono mt-1">{chatRoomId}</p>
128+
<p class="text-xs text-gray-500 dark:text-gray-500 mt-0.5">
129+
Level: {level}{bankId ? ` | Bank: ${bankId}` : ""}
130+
</p>
131+
</div>
132+
<div class="flex items-center gap-3">
133+
{#if participants.length > 0}
134+
<span class="text-sm text-gray-600 dark:text-gray-400">
135+
Participants: {participants.length}
136+
</span>
137+
{/if}
138+
<button
139+
onclick={() => (showAddForm = !showAddForm)}
140+
class="inline-flex items-center rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"
141+
data-testid="add-participant-btn"
142+
>
143+
{showAddForm ? "Cancel" : "Add Participant"}
144+
</button>
145+
</div>
146+
</div>
147+
148+
{#if successMessage}
149+
<div class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-200" data-testid="success-message">
150+
{successMessage}
151+
</div>
152+
{/if}
153+
154+
{#if error}
155+
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-900/20 dark:text-red-200" data-testid="error-message">
156+
{error}
157+
</div>
158+
{/if}
159+
160+
{#if showAddForm}
161+
<div class="mb-4 rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800" data-testid="add-participant-form">
162+
<h2 class="mb-3 text-lg font-semibold text-gray-900 dark:text-gray-100">Add Participant</h2>
163+
<div class="space-y-3">
164+
<div>
165+
<label for="user_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">User ID</label>
166+
<input
167+
type="text"
168+
id="user_id"
169+
bind:value={addUserId}
170+
class="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100"
171+
data-testid="participant-user-id-input"
172+
/>
173+
</div>
174+
<div>
175+
<label for="permissions" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Permissions (comma-separated)</label>
176+
<input
177+
type="text"
178+
id="permissions"
179+
bind:value={addPermissions}
180+
placeholder="can_send_message, can_delete_message, can_update_room"
181+
class="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100"
182+
data-testid="participant-permissions-input"
183+
/>
184+
</div>
185+
<button
186+
onclick={addParticipant}
187+
disabled={!addUserId.trim()}
188+
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-600"
189+
data-testid="submit-add-participant"
190+
>
191+
Add
192+
</button>
193+
</div>
194+
</div>
195+
{/if}
196+
197+
{#if loading}
198+
<div class="flex items-center justify-center py-8">
199+
<div class="h-6 w-6 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"></div>
200+
<span class="ml-2 text-gray-600 dark:text-gray-400">Loading...</span>
201+
</div>
202+
{:else if participants.length > 0}
203+
<div class="overflow-x-auto">
204+
<table class="w-full border-collapse" data-testid="participants-table">
205+
<thead>
206+
<tr class="border-b-2 border-gray-200 dark:border-gray-700">
207+
<th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">User ID</th>
208+
<th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Permissions</th>
209+
<th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Joined</th>
210+
<th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Muted</th>
211+
<th class="text-right px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Actions</th>
212+
</tr>
213+
</thead>
214+
<tbody>
215+
{#each participants as participant (participant.participant_id)}
216+
<tr class="border-b border-gray-100 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800/50" data-testid="participant-row-{participant.user_id}">
217+
<td class="px-3 py-2 text-xs text-gray-900 dark:text-gray-100">
218+
<span class="font-medium">{participant.username || participant.user_id}</span>
219+
{#if participant.provider}
220+
<br /><span class="text-gray-500 dark:text-gray-500">{participant.provider}</span>
221+
{/if}
222+
{#if participant.username}
223+
<br /><span class="font-mono text-gray-500 dark:text-gray-500">{participant.user_id}</span>
224+
{/if}
225+
{#if participant.consumer_id}
226+
<br /><span class="text-gray-500">Consumer: {participant.consumer_name || participant.consumer_id}</span>
227+
{/if}
228+
</td>
229+
<td class="px-3 py-2 text-xs text-gray-900 dark:text-gray-100">
230+
<div class="flex flex-wrap gap-1">
231+
{#each participant.permissions as perm}
232+
<span class="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
233+
{perm}
234+
</span>
235+
{/each}
236+
</div>
237+
</td>
238+
<td class="px-3 py-2 text-xs text-gray-900 dark:text-gray-100">{formatDate(participant.joined_at)}</td>
239+
<td class="px-3 py-2 text-xs">
240+
<span class="rounded-full px-2 py-0.5 text-xs font-medium {participant.is_muted
241+
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400'
242+
: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'}">
243+
{participant.is_muted ? "Muted" : "Active"}
244+
</span>
245+
</td>
246+
<td class="px-3 py-2 text-right">
247+
<button
248+
onclick={() => removeParticipant(participant.user_id)}
249+
class="inline-flex items-center rounded border border-red-300 bg-red-50 px-2 py-1 text-xs font-medium text-red-700 hover:bg-red-100 dark:border-red-600 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
250+
data-testid="remove-participant-{participant.user_id}"
251+
>
252+
Remove
253+
</button>
254+
</td>
255+
</tr>
256+
{/each}
257+
</tbody>
258+
</table>
259+
</div>
260+
{:else if !error}
261+
<div class="rounded-lg bg-gray-100 p-8 text-center dark:bg-gray-800">
262+
<p class="text-lg font-medium text-gray-700 dark:text-gray-300">
263+
No participants found
264+
</p>
265+
<p class="mt-1 text-gray-600 dark:text-gray-400">
266+
Add a participant to this chat room.
267+
</p>
268+
</div>
269+
{/if}

0 commit comments

Comments
 (0)