feat: project role-bound postboxes
This commit is contained in:
@@ -35,6 +35,25 @@ export type PortalServiceListResponse = {
|
||||
services: PortalServiceEntry[];
|
||||
};
|
||||
|
||||
export type PortalPostboxEntry = {
|
||||
postbox: {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
organization_unit_name?: string | null;
|
||||
function_name?: string | null;
|
||||
classification: string;
|
||||
};
|
||||
unread_count: number;
|
||||
latest_message_at?: string | null;
|
||||
route_path: string;
|
||||
};
|
||||
|
||||
export type PortalPostboxListResponse = {
|
||||
provider_available: boolean;
|
||||
postboxes: PortalPostboxEntry[];
|
||||
};
|
||||
|
||||
export type PortalServiceLaunchResult = {
|
||||
service_ref: PortalServiceDefinition["reference"];
|
||||
binding: PortalServiceBinding;
|
||||
@@ -66,6 +85,17 @@ export function listPortalServices(
|
||||
);
|
||||
}
|
||||
|
||||
export function listPortalPostboxes(
|
||||
settings: ApiSettings,
|
||||
signal?: AbortSignal
|
||||
): Promise<PortalPostboxListResponse> {
|
||||
return apiFetch<PortalPostboxListResponse>(
|
||||
settings,
|
||||
"/api/v1/portal/postboxes",
|
||||
{ signal }
|
||||
);
|
||||
}
|
||||
|
||||
export function launchPortalService(
|
||||
settings: ApiSettings,
|
||||
serviceId: string,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowUpRight, Search } from "lucide-react";
|
||||
import { ArrowUpRight, Inbox, Search } from "lucide-react";
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -20,7 +20,9 @@ import {
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
launchPortalService,
|
||||
listPortalPostboxes,
|
||||
listPortalServices,
|
||||
type PortalPostboxEntry,
|
||||
type PortalServiceEntry
|
||||
} from "../../api/portal";
|
||||
|
||||
@@ -31,6 +33,7 @@ export default function PortalPage({ settings }: PlatformRouteContext) {
|
||||
const [submittedQuery, setSubmittedQuery] = useState("");
|
||||
const [includeUnavailable, setIncludeUnavailable] = useState(true);
|
||||
const [services, setServices] = useState<PortalServiceEntry[]>([]);
|
||||
const [postboxes, setPostboxes] = useState<PortalPostboxEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [launchingId, setLaunchingId] = useState("");
|
||||
@@ -62,6 +65,18 @@ export default function PortalPage({ settings }: PlatformRouteContext) {
|
||||
return () => controller.abort();
|
||||
}, [includeUnavailable, settings, submittedQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
listPortalPostboxes(settings, controller.signal)
|
||||
.then((response) => setPostboxes(response.postboxes))
|
||||
.catch((reason) => {
|
||||
if ((reason as Error).name !== "AbortError") {
|
||||
setError(reason instanceof Error ? reason.message : "Postboxes could not be loaded.");
|
||||
}
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [settings]);
|
||||
|
||||
const counts = useMemo(() => ({
|
||||
available: services.filter((entry) => entry.state === "available").length,
|
||||
unavailable: services.filter((entry) => entry.state === "unavailable").length
|
||||
@@ -146,6 +161,37 @@ export default function PortalPage({ settings }: PlatformRouteContext) {
|
||||
</DismissibleAlert>
|
||||
}
|
||||
{loading && <LoadingIndicator label="Loading services" />}
|
||||
{postboxes.length > 0 && (
|
||||
<section className="portal-postboxes" aria-labelledby="portal-postboxes-heading">
|
||||
<div className="portal-section-heading">
|
||||
<Inbox size={18} aria-hidden="true" />
|
||||
<h2 id="portal-postboxes-heading">My function postboxes</h2>
|
||||
</div>
|
||||
<div className="portal-postbox-list">
|
||||
{postboxes.map((entry) => (
|
||||
<button
|
||||
key={entry.postbox.id}
|
||||
type="button"
|
||||
className="portal-postbox-entry"
|
||||
onClick={() => navigate(entry.route_path)}
|
||||
>
|
||||
<span>
|
||||
<strong>{entry.postbox.name}</strong>
|
||||
<small>
|
||||
{[entry.postbox.organization_unit_name, entry.postbox.function_name]
|
||||
.filter(Boolean)
|
||||
.join(" / ") || entry.postbox.address}
|
||||
</small>
|
||||
</span>
|
||||
<span className="portal-postbox-count" aria-label={`${entry.unread_count} unread messages`}>
|
||||
{entry.unread_count > 99 ? "99+" : entry.unread_count}
|
||||
</span>
|
||||
<ArrowUpRight size={15} aria-hidden="true" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{!loading && !error && services.length === 0 &&
|
||||
<div className="portal-empty">No matching services.</div>
|
||||
}
|
||||
|
||||
@@ -56,6 +56,73 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.portal-postboxes {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.portal-section-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.portal-section-heading h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.portal-postbox-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.portal-postbox-entry {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 56px;
|
||||
padding: 9px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-raised);
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.portal-postbox-entry:hover,
|
||||
.portal-postbox-entry:focus-visible {
|
||||
border-color: var(--accent);
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.portal-postbox-entry > span:first-child {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.portal-postbox-entry small {
|
||||
overflow: hidden;
|
||||
color: var(--text-soft);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.portal-postbox-count {
|
||||
min-width: 24px;
|
||||
padding: 2px 5px;
|
||||
border-radius: 10px;
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.portal-service-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user