Compare commits
2 Commits
68f82d79c2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2762aceb04 | |||
| f3442e644a |
@@ -249,23 +249,34 @@ export default async function DashboardPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="divide-y">
|
<ul className="divide-y">
|
||||||
{data.recentNotifications.map((n) => (
|
{data.recentNotifications.map((n) => {
|
||||||
|
const isWarning = n.severity === "warning";
|
||||||
|
return (
|
||||||
<li
|
<li
|
||||||
key={n.$id}
|
key={n.$id}
|
||||||
className={`flex items-start gap-3 py-2.5 ${n.read ? "opacity-70" : ""}`}
|
className={`flex items-start gap-3 py-2.5 ${n.read ? "opacity-70" : ""}`}
|
||||||
>
|
>
|
||||||
<span
|
<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
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0 flex-1">
|
<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">
|
<p className="text-muted-foreground mt-0.5 text-xs">
|
||||||
{datetimeFormatter.format(new Date(n.$createdAt))}
|
{datetimeFormatter.format(new Date(n.$createdAt))}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useActionState, useEffect, useState } from "react";
|
import { useActionState, useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Check,
|
Check,
|
||||||
@@ -87,17 +86,13 @@ export function JobActionsPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AcceptButton({ jobId }: { jobId: string }) {
|
function AcceptButton({ jobId }: { jobId: string }) {
|
||||||
const router = useRouter();
|
|
||||||
const [state, action, pending] = useActionState(acceptJobAction, initialJobActionState);
|
const [state, action, pending] = useActionState(acceptJobAction, initialJobActionState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.ok) {
|
// Success path redirects from the server action, so state.ok never
|
||||||
toast.success("İş işleme alındı, alt yapı üretimi başladı.");
|
// shows up here — we only need to surface errors.
|
||||||
router.refresh();
|
if (state.error) toast.error(state.error);
|
||||||
} else if (state.error) {
|
}, [state]);
|
||||||
toast.error(state.error);
|
|
||||||
}
|
|
||||||
}, [state, router]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form action={action}>
|
<form action={action}>
|
||||||
@@ -111,19 +106,12 @@ function AcceptButton({ jobId }: { jobId: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HandToClinicButton({ job }: { job: Job }) {
|
function HandToClinicButton({ job }: { job: Job }) {
|
||||||
const router = useRouter();
|
|
||||||
const [state, action, pending] = useActionState(handToClinicAction, initialJobActionState);
|
const [state, action, pending] = useActionState(handToClinicAction, initialJobActionState);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.ok) {
|
if (state.error) toast.error(state.error);
|
||||||
toast.success("Klinik tarafına gönderildi.");
|
}, [state]);
|
||||||
setOpen(false);
|
|
||||||
router.refresh();
|
|
||||||
} else if (state.error) {
|
|
||||||
toast.error(state.error);
|
|
||||||
}
|
|
||||||
}, [state, router]);
|
|
||||||
|
|
||||||
const isFinal = job.currentStep === "cila_bitim";
|
const isFinal = job.currentStep === "cila_bitim";
|
||||||
const stageLabel =
|
const stageLabel =
|
||||||
@@ -180,19 +168,12 @@ function HandToClinicButton({ job }: { job: Job }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ApproveAtClinicButton({ job }: { job: Job }) {
|
function ApproveAtClinicButton({ job }: { job: Job }) {
|
||||||
const router = useRouter();
|
|
||||||
const [state, action, pending] = useActionState(approveAtClinicAction, initialJobActionState);
|
const [state, action, pending] = useActionState(approveAtClinicAction, initialJobActionState);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.ok) {
|
if (state.error) toast.error(state.error);
|
||||||
toast.success("Prova onaylandı, lab tarafına gönderildi.");
|
}, [state]);
|
||||||
setOpen(false);
|
|
||||||
router.refresh();
|
|
||||||
} else if (state.error) {
|
|
||||||
toast.error(state.error);
|
|
||||||
}
|
|
||||||
}, [state, router]);
|
|
||||||
|
|
||||||
const stageLabel = job.currentStep === "alt_yapi_prova" ? "alt yapı" : "üst yapı";
|
const stageLabel = job.currentStep === "alt_yapi_prova" ? "alt yapı" : "üst yapı";
|
||||||
|
|
||||||
@@ -242,7 +223,6 @@ function ApproveAtClinicButton({ job }: { job: Job }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RequestRevisionButton({ job }: { job: Job }) {
|
function RequestRevisionButton({ job }: { job: Job }) {
|
||||||
const router = useRouter();
|
|
||||||
const [state, action, pending] = useActionState(
|
const [state, action, pending] = useActionState(
|
||||||
requestRevisionAction,
|
requestRevisionAction,
|
||||||
initialJobActionState,
|
initialJobActionState,
|
||||||
@@ -250,14 +230,8 @@ function RequestRevisionButton({ job }: { job: Job }) {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.ok) {
|
if (state.error) toast.error(state.error);
|
||||||
toast.success("Düzeltme talebi gönderildi.");
|
}, [state]);
|
||||||
setOpen(false);
|
|
||||||
router.refresh();
|
|
||||||
} else if (state.error) {
|
|
||||||
toast.error(state.error);
|
|
||||||
}
|
|
||||||
}, [state, router]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@@ -304,17 +278,11 @@ function RequestRevisionButton({ job }: { job: Job }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DeliverButton({ jobId }: { jobId: string }) {
|
function DeliverButton({ jobId }: { jobId: string }) {
|
||||||
const router = useRouter();
|
|
||||||
const [state, action, pending] = useActionState(markDeliveredAction, initialJobActionState);
|
const [state, action, pending] = useActionState(markDeliveredAction, initialJobActionState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.ok) {
|
if (state.error) toast.error(state.error);
|
||||||
toast.success("İş teslim alındı.");
|
}, [state]);
|
||||||
router.refresh();
|
|
||||||
} else if (state.error) {
|
|
||||||
toast.error(state.error);
|
|
||||||
}
|
|
||||||
}, [state, router]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form action={action}>
|
<form action={action}>
|
||||||
@@ -328,19 +296,12 @@ function DeliverButton({ jobId }: { jobId: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CancelButton({ jobId }: { jobId: string }) {
|
function CancelButton({ jobId }: { jobId: string }) {
|
||||||
const router = useRouter();
|
|
||||||
const [state, action, pending] = useActionState(cancelJobAction, initialJobActionState);
|
const [state, action, pending] = useActionState(cancelJobAction, initialJobActionState);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.ok) {
|
if (state.error) toast.error(state.error);
|
||||||
toast.success("İş iptal edildi.");
|
}, [state]);
|
||||||
setOpen(false);
|
|
||||||
router.refresh();
|
|
||||||
} else if (state.error) {
|
|
||||||
toast.error(state.error);
|
|
||||||
}
|
|
||||||
}, [state, router]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import { FlashToast } from "@/components/flash-toast";
|
||||||
import { getActiveContext } from "@/lib/appwrite/active-context";
|
import { getActiveContext } from "@/lib/appwrite/active-context";
|
||||||
import { countUnreadNotifications } from "@/lib/appwrite/notification-helpers";
|
import { countUnreadNotifications } from "@/lib/appwrite/notification-helpers";
|
||||||
import { getLogoUrl } from "@/lib/appwrite/storage";
|
import { getLogoUrl } from "@/lib/appwrite/storage";
|
||||||
@@ -40,6 +42,9 @@ export default async function DashboardLayout({
|
|||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<FlashToast />
|
||||||
|
</Suspense>
|
||||||
</DashboardShell>
|
</DashboardShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,10 +66,21 @@ function NotificationRow({ row }: { row: Notification }) {
|
|||||||
? "/connections"
|
? "/connections"
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const isWarning = row.severity === "warning";
|
||||||
return (
|
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
|
<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
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -79,8 +90,11 @@ function NotificationRow({ row }: { row: Notification }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{!row.read && (
|
{!row.read && (
|
||||||
<Badge variant="secondary" className="text-[10px] uppercase">
|
<Badge
|
||||||
Yeni
|
variant={isWarning ? "destructive" : "secondary"}
|
||||||
|
className="text-[10px] uppercase"
|
||||||
|
>
|
||||||
|
{isWarning ? "Dikkat" : "Yeni"}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{link && (
|
{link && (
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
const MESSAGES: Record<string, string> = {
|
||||||
|
accepted: "İş işleme alındı, alt yapı üretimi başladı.",
|
||||||
|
handed: "Klinik tarafına gönderildi.",
|
||||||
|
approved: "Prova onaylandı, lab tarafına geri gönderildi.",
|
||||||
|
revision: "Düzeltme talebi gönderildi.",
|
||||||
|
delivered: "İş teslim alındı.",
|
||||||
|
cancelled: "İş iptal edildi.",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a one-shot toast based on ?flash=<key>, then strip the param from
|
||||||
|
* the URL so a refresh doesn't replay it. Mounted in the dashboard layout
|
||||||
|
* so it works on every page that server actions might redirect to.
|
||||||
|
*/
|
||||||
|
export function FlashToast() {
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const params = useSearchParams();
|
||||||
|
const fired = useRef<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const flash = params.get("flash");
|
||||||
|
if (!flash) return;
|
||||||
|
// Avoid double-firing under React Strict Mode in dev.
|
||||||
|
if (fired.current === flash) return;
|
||||||
|
fired.current = flash;
|
||||||
|
|
||||||
|
const message = MESSAGES[flash] ?? null;
|
||||||
|
if (message) toast.success(message);
|
||||||
|
|
||||||
|
const next = new URLSearchParams(params.toString());
|
||||||
|
next.delete("flash");
|
||||||
|
const query = next.toString();
|
||||||
|
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false });
|
||||||
|
}, [params, pathname, router]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -278,6 +278,15 @@ export async function rejectConnectionAction(
|
|||||||
entityId: connectionId,
|
entityId: connectionId,
|
||||||
changes: { status: "rejected" },
|
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) {
|
} catch (e) {
|
||||||
return { ok: false, error: appwriteError(e, "Reddedilemedi.") };
|
return { ok: false, error: appwriteError(e, "Reddedilemedi.") };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
import { AppwriteException, ID, Permission, Query, Role } from "node-appwrite";
|
import { AppwriteException, ID, Permission, Query, Role } from "node-appwrite";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -330,7 +331,9 @@ export async function acceptJobAction(
|
|||||||
revalidatePath(`/jobs/${jobId}`);
|
revalidatePath(`/jobs/${jobId}`);
|
||||||
revalidatePath("/jobs/inbound");
|
revalidatePath("/jobs/inbound");
|
||||||
revalidatePath("/jobs/outbound");
|
revalidatePath("/jobs/outbound");
|
||||||
return { ok: true };
|
// Redirect forces a full RSC payload reload — bypasses any client-side
|
||||||
|
// cache that router.refresh() might otherwise miss.
|
||||||
|
redirect(`/jobs/${jobId}?flash=accepted`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -498,7 +501,7 @@ export async function handToClinicAction(
|
|||||||
revalidatePath("/jobs/inbound");
|
revalidatePath("/jobs/inbound");
|
||||||
revalidatePath("/jobs/outbound");
|
revalidatePath("/jobs/outbound");
|
||||||
revalidatePath("/finance");
|
revalidatePath("/finance");
|
||||||
return { ok: true };
|
redirect(`/jobs/${jobId}?flash=handed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -581,7 +584,7 @@ export async function approveAtClinicAction(
|
|||||||
revalidatePath(`/jobs/${jobId}`);
|
revalidatePath(`/jobs/${jobId}`);
|
||||||
revalidatePath("/jobs/inbound");
|
revalidatePath("/jobs/inbound");
|
||||||
revalidatePath("/jobs/outbound");
|
revalidatePath("/jobs/outbound");
|
||||||
return { ok: true };
|
redirect(`/jobs/${jobId}?flash=approved`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -661,6 +664,7 @@ export async function requestRevisionAction(
|
|||||||
void createNotification({
|
void createNotification({
|
||||||
tenantId: job.labTenantId,
|
tenantId: job.labTenantId,
|
||||||
jobId,
|
jobId,
|
||||||
|
severity: "warning",
|
||||||
message: `Hasta ${job.patientCode} ${stepLabel} provası için düzeltme istendi: ${note.slice(0, 120)}`,
|
message: `Hasta ${job.patientCode} ${stepLabel} provası için düzeltme istendi: ${note.slice(0, 120)}`,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -670,7 +674,7 @@ export async function requestRevisionAction(
|
|||||||
revalidatePath(`/jobs/${jobId}`);
|
revalidatePath(`/jobs/${jobId}`);
|
||||||
revalidatePath("/jobs/inbound");
|
revalidatePath("/jobs/inbound");
|
||||||
revalidatePath("/jobs/outbound");
|
revalidatePath("/jobs/outbound");
|
||||||
return { ok: true };
|
redirect(`/jobs/${jobId}?flash=revision`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function markDeliveredAction(
|
export async function markDeliveredAction(
|
||||||
@@ -727,7 +731,7 @@ export async function markDeliveredAction(
|
|||||||
revalidatePath("/jobs/outbound");
|
revalidatePath("/jobs/outbound");
|
||||||
revalidatePath("/jobs/inbound");
|
revalidatePath("/jobs/inbound");
|
||||||
revalidatePath("/finance");
|
revalidatePath("/finance");
|
||||||
return { ok: true };
|
redirect(`/jobs/${jobId}?flash=delivered`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelJobAction(
|
export async function cancelJobAction(
|
||||||
@@ -770,6 +774,16 @@ export async function cancelJobAction(
|
|||||||
entityId: jobId,
|
entityId: jobId,
|
||||||
changes: { status: "cancelled" },
|
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) {
|
} catch (e) {
|
||||||
return { ok: false, error: appwriteError(e, "İptal edilemedi.") };
|
return { ok: false, error: appwriteError(e, "İptal edilemedi.") };
|
||||||
}
|
}
|
||||||
@@ -777,5 +791,5 @@ export async function cancelJobAction(
|
|||||||
revalidatePath(`/jobs/${jobId}`);
|
revalidatePath(`/jobs/${jobId}`);
|
||||||
revalidatePath("/jobs/inbound");
|
revalidatePath("/jobs/inbound");
|
||||||
revalidatePath("/jobs/outbound");
|
revalidatePath("/jobs/outbound");
|
||||||
return { ok: true };
|
redirect(`/jobs/${jobId}?flash=cancelled`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import "server-only";
|
|||||||
|
|
||||||
import { ID, Permission, Query, Role } from "node-appwrite";
|
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 { createAdminClient } from "./server";
|
||||||
import { toPlain } from "./serialize";
|
import { toPlain } from "./serialize";
|
||||||
|
|
||||||
@@ -12,6 +17,9 @@ type CreateNotificationInput = {
|
|||||||
jobId?: string;
|
jobId?: string;
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
message: 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,
|
connectionId: input.connectionId,
|
||||||
message: input.message.slice(0, 500),
|
message: input.message.slice(0, 500),
|
||||||
read: false,
|
read: false,
|
||||||
|
severity: input.severity ?? "info",
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
Permission.read(Role.team(input.tenantId)),
|
Permission.read(Role.team(input.tenantId)),
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ export async function rejectPaymentAction(
|
|||||||
});
|
});
|
||||||
void createNotification({
|
void createNotification({
|
||||||
tenantId: row.tenantId,
|
tenantId: row.tenantId,
|
||||||
|
severity: "warning",
|
||||||
message: `Ödeme bildiriminiz reddedildi: ${row.amount.toLocaleString("tr-TR")} ${row.currency}.`,
|
message: `Ödeme bildiriminiz reddedildi: ${row.amount.toLocaleString("tr-TR")} ${row.currency}.`,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ export interface Payment extends Row {
|
|||||||
recordedBy: string;
|
recordedBy: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NotificationSeverity = "info" | "warning";
|
||||||
|
|
||||||
export interface Notification extends Row {
|
export interface Notification extends Row {
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
@@ -201,6 +203,9 @@ export interface Notification extends Row {
|
|||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
message: string;
|
message: string;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
|
/** Visual + filtering hint. 'warning' for things requiring attention
|
||||||
|
* (revision request, cancellation, payment / connection rejection). */
|
||||||
|
severity?: NotificationSeverity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuditAction = "create" | "update" | "delete";
|
export type AuditAction = "create" | "update" | "delete";
|
||||||
|
|||||||
Reference in New Issue
Block a user