feat(notifications): severity (info/warning) + cover the gaps in the flow matrix
Modelled the real crown-and-bridge workflow (impression → accept →
framework try-in → bisque try-in → glaze/cila → delivery, with revisions
bouncing back at any try-in stage). Mapped every event in our system to
the appropriate side and tagged the ones a user actually needs to act on
versus normal progress traffic.
DB
- notifications.severity enum ('info' | 'warning', default 'info').
Existing rows default to info.
Helper
- createNotification gains an optional severity param; persists into
the new column.
Matrix changes (warnings vs infos)
Lab side:
- Düzeltme talebi (clinic → lab) WARNING
- İş iptal edildi (counterpart action) WARNING
Clinic side:
- Ödeme reddedildi (lab → clinic) WARNING
- Bağlantı reddedildi (counterpart → req) WARNING (newly fired —
rejectConnectionAction now sends a notification, previously silent)
Everything else (accept, hand-off, prova ready, prova approved,
delivery, payment submitted/approved, connection request/approved):
stays at info.
Behaviour additions in actions
- cancelJobAction now notifies the *other* party. Previously cancelled
jobs vanished from one side's inbox without warning. severity=warning.
- rejectConnectionAction notifies the requester (severity=warning) so
they know the connection didn't come through.
- requestRevisionAction tags its existing notification as warning.
- rejectPaymentAction tags its existing notification as warning.
UI
- Notifications list row: unread warning rows get an amber dot, an
amber-tinted background, and a 'Dikkat' destructive badge instead
of the regular 'Yeni' secondary one.
- Dashboard recent-notifications widget mirrors the same dot + bold
treatment so warnings stand out at a glance.
Net result: a user opening the bell sees normal traffic muted (still
listed for the 'herkes her şeyden haberdar' guarantee) and the things
that actually need them coloured amber and labeled Dikkat.
This commit is contained in:
@@ -249,23 +249,34 @@ export default async function DashboardPage() {
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y">
|
||||
{data.recentNotifications.map((n) => (
|
||||
{data.recentNotifications.map((n) => {
|
||||
const isWarning = n.severity === "warning";
|
||||
return (
|
||||
<li
|
||||
key={n.$id}
|
||||
className={`flex items-start gap-3 py-2.5 ${n.read ? "opacity-70" : ""}`}
|
||||
>
|
||||
<span
|
||||
className={`mt-1.5 size-2 shrink-0 rounded-full ${n.read ? "bg-muted" : "bg-primary"}`}
|
||||
className={`mt-1.5 size-2 shrink-0 rounded-full ${
|
||||
n.read
|
||||
? "bg-muted"
|
||||
: isWarning
|
||||
? "bg-amber-500"
|
||||
: "bg-primary"
|
||||
}`}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm leading-tight">{n.message}</p>
|
||||
<p className={`text-sm leading-tight ${isWarning && !n.read ? "font-medium" : ""}`}>
|
||||
{n.message}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-0.5 text-xs">
|
||||
{datetimeFormatter.format(new Date(n.$createdAt))}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -66,10 +66,21 @@ function NotificationRow({ row }: { row: Notification }) {
|
||||
? "/connections"
|
||||
: null;
|
||||
|
||||
const isWarning = row.severity === "warning";
|
||||
return (
|
||||
<li className={`flex items-start gap-3 px-3 py-3 ${row.read ? "opacity-70" : ""}`}>
|
||||
<li
|
||||
className={`flex items-start gap-3 px-3 py-3 ${row.read ? "opacity-70" : ""} ${
|
||||
isWarning && !row.read ? "bg-amber-50/60 dark:bg-amber-950/30" : ""
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`mt-1.5 size-2 shrink-0 rounded-full ${row.read ? "bg-muted" : "bg-primary"}`}
|
||||
className={`mt-1.5 size-2 shrink-0 rounded-full ${
|
||||
row.read
|
||||
? "bg-muted"
|
||||
: isWarning
|
||||
? "bg-amber-500"
|
||||
: "bg-primary"
|
||||
}`}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -79,8 +90,11 @@ function NotificationRow({ row }: { row: Notification }) {
|
||||
</p>
|
||||
</div>
|
||||
{!row.read && (
|
||||
<Badge variant="secondary" className="text-[10px] uppercase">
|
||||
Yeni
|
||||
<Badge
|
||||
variant={isWarning ? "destructive" : "secondary"}
|
||||
className="text-[10px] uppercase"
|
||||
>
|
||||
{isWarning ? "Dikkat" : "Yeni"}
|
||||
</Badge>
|
||||
)}
|
||||
{link && (
|
||||
|
||||
@@ -278,6 +278,15 @@ export async function rejectConnectionAction(
|
||||
entityId: connectionId,
|
||||
changes: { status: "rejected" },
|
||||
});
|
||||
// Tell the requester their request was turned down — warning, not info.
|
||||
const requesterTenant =
|
||||
conn.clinicTenantId === ctx.tenantId ? conn.labTenantId : conn.clinicTenantId;
|
||||
void createNotification({
|
||||
tenantId: requesterTenant,
|
||||
connectionId,
|
||||
severity: "warning",
|
||||
message: `${ctx.settings?.companyName ?? "Karşı taraf"} bağlantı talebinizi reddetti.`,
|
||||
});
|
||||
} catch (e) {
|
||||
return { ok: false, error: appwriteError(e, "Reddedilemedi.") };
|
||||
}
|
||||
|
||||
@@ -664,6 +664,7 @@ export async function requestRevisionAction(
|
||||
void createNotification({
|
||||
tenantId: job.labTenantId,
|
||||
jobId,
|
||||
severity: "warning",
|
||||
message: `Hasta ${job.patientCode} ${stepLabel} provası için düzeltme istendi: ${note.slice(0, 120)}`,
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -773,6 +774,16 @@ export async function cancelJobAction(
|
||||
entityId: jobId,
|
||||
changes: { status: "cancelled" },
|
||||
});
|
||||
// Notify the other side — cancellation is a warning, not normal traffic.
|
||||
const otherTenantId =
|
||||
ctx.tenantId === job.clinicTenantId ? job.labTenantId : job.clinicTenantId;
|
||||
const actor = ctx.kind === "lab" ? "Laboratuvar" : "Klinik";
|
||||
void createNotification({
|
||||
tenantId: otherTenantId,
|
||||
jobId,
|
||||
severity: "warning",
|
||||
message: `${actor} hasta ${job.patientCode} işini iptal etti.`,
|
||||
});
|
||||
} catch (e) {
|
||||
return { ok: false, error: appwriteError(e, "İptal edilemedi.") };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import "server-only";
|
||||
|
||||
import { ID, Permission, Query, Role } from "node-appwrite";
|
||||
|
||||
import { DATABASE_ID, TABLES, type Notification } from "./schema";
|
||||
import {
|
||||
DATABASE_ID,
|
||||
TABLES,
|
||||
type Notification,
|
||||
type NotificationSeverity,
|
||||
} from "./schema";
|
||||
import { createAdminClient } from "./server";
|
||||
import { toPlain } from "./serialize";
|
||||
|
||||
@@ -12,6 +17,9 @@ type CreateNotificationInput = {
|
||||
jobId?: string;
|
||||
connectionId?: string;
|
||||
message: string;
|
||||
/** Defaults to 'info'. Use 'warning' for things that need the user's
|
||||
* attention (revision, cancellation, rejections). */
|
||||
severity?: NotificationSeverity;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -32,6 +40,7 @@ export async function createNotification(input: CreateNotificationInput): Promis
|
||||
connectionId: input.connectionId,
|
||||
message: input.message.slice(0, 500),
|
||||
read: false,
|
||||
severity: input.severity ?? "info",
|
||||
},
|
||||
[
|
||||
Permission.read(Role.team(input.tenantId)),
|
||||
|
||||
@@ -269,6 +269,7 @@ export async function rejectPaymentAction(
|
||||
});
|
||||
void createNotification({
|
||||
tenantId: row.tenantId,
|
||||
severity: "warning",
|
||||
message: `Ödeme bildiriminiz reddedildi: ${row.amount.toLocaleString("tr-TR")} ${row.currency}.`,
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -194,6 +194,8 @@ export interface Payment extends Row {
|
||||
recordedBy: string;
|
||||
}
|
||||
|
||||
export type NotificationSeverity = "info" | "warning";
|
||||
|
||||
export interface Notification extends Row {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
@@ -201,6 +203,9 @@ export interface Notification extends Row {
|
||||
connectionId?: string;
|
||||
message: string;
|
||||
read: boolean;
|
||||
/** Visual + filtering hint. 'warning' for things requiring attention
|
||||
* (revision request, cancellation, payment / connection rejection). */
|
||||
severity?: NotificationSeverity;
|
||||
}
|
||||
|
||||
export type AuditAction = "create" | "update" | "delete";
|
||||
|
||||
Reference in New Issue
Block a user