+ {props.label && (
+
+ )}
+
+
{
+ setIsOpen(true);
+ editableRef.current?.focus();
+ }}
+ >
+ {props.selectedUsers.map((user) => (
+
+ {`${user.firstName} ${user.lastName}`}
+ {props.disabled ? null : (
+
+ )}
+
+ ))}
+
+
setIsOpen(true)}
+ onKeyDown={handleKeyDown}
+ className="flex-grow border-none sm:text-sm outline-none focus:ring-0 focus:border-transparent min-w-[100px]"
+ data-placeholder={
+ props.selectedUsers.length ? "" : "Type a name or email..."
+ }
+ style={{ minHeight: "1.5rem" }}
+ />
+
+
+ {isOpen && filteredUsers.length > 0 && (
+
+ {filteredUsers.map((user) => (
+ - handleUserSelect(user)}
+ className="px-3 py-2 hover:bg-slate-50 cursor-pointer flex flex-col"
+ >
+ {`${user.firstName} ${user.lastName}`}
+ {user.mail}
+
+ ))}
+
+ )}
+
+ );
+}
+
+
+function KernAIReport() {
+ return (
+
+
+
+
+
+ KernAI Team
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/inbox-mail/InboxMailNavigator.tsx b/components/inbox-mail/InboxMailNavigator.tsx
new file mode 100644
index 0000000..5250625
--- /dev/null
+++ b/components/inbox-mail/InboxMailNavigator.tsx
@@ -0,0 +1,71 @@
+import { MemoIconMail } from "@/submodules/react-components/components/kern-icons/icons";
+import { useRouter } from "next/router";
+import { useCallback, useMemo } from "react";
+import tinycolor from 'tinycolor2'
+import { useNewMailCount } from "./helper";
+
+type InboxMailNavigatorProps = {
+ forChatArea?: boolean;
+ project?: { customerColorPrimary: string; id: string; };
+ chatId?: string;
+}
+
+export default function InboxMailNavigator(props: InboxMailNavigatorProps) {
+ const router = useRouter();
+
+ const navigateToMailPage = useCallback(() => {
+ const chatIdParam = props.chatId ? `?chatId=${props.chatId}` : '';
+ const projectIdParam = props.project ? props.chatId ? `&projectId=${props.project.id}` : `?projectId=${props.project.id}` : '';
+ router.push(`/inbox-mail${chatIdParam}${projectIdParam}`);
+ }, [props.chatId, props.project]);
+
+ const isLightDesign = useMemo(() => tinycolor(props.project?.customerColorPrimary).isLight(), [props.project?.customerColorPrimary]);
+
+ const buttonClasses = useMemo(() => {
+ if (props.forChatArea) {
+ const classes = "items-center justify-center w-8 h-8 border group flex -x-3 rounded-md p-1 text-sm leading-6 font-semibold"
+ if (isLightDesign) return 'bg-gray-100 text-gray-700 border-gray-300 ' + classes;
+ else return 'bg-zinc-900 text-zinc-100 border-zinc-700 ' + classes;
+ }
+ return "text-gray-400 hover:text-green-600 hover:bg-zinc-800 border-gray-700 items-center justify-center w-10 h-10 border group flex -x-3 rounded-md p-2 text-sm leading-6 font-semibold"
+ }, [props.forChatArea, isLightDesign]);
+
+ return
+
+
+}
+
+
+
+interface NewMailBadgeProps {
+ forChatArea?: boolean;
+ refreshInterval?: number; // optional, default to 60000ms
+}
+
+export function InboxMailBadge(props: NewMailBadgeProps) {
+ const newMailCount = useNewMailCount(props.refreshInterval);
+
+ if (newMailCount === 0) return;
+
+ const badgeClasses = props.forChatArea
+ ? 'top-0 right-0'
+ : 'top-1 right-1';
+
+ return (
+
+ {newMailCount}
+
+ );
+}
+
+export function InboxMailTitleBadge() {
+ return (
+
+ Inbox Mail
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/inbox-mail/InboxMailView.tsx b/components/inbox-mail/InboxMailView.tsx
new file mode 100644
index 0000000..4595d2d
--- /dev/null
+++ b/components/inbox-mail/InboxMailView.tsx
@@ -0,0 +1,524 @@
+
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import CreateNewMailModal from "./CreateNewMailModal";
+import { InboxMail, InboxMailThread, User, InboxMailThreadSupportProgressState } from "./types-mail";
+import { getInboxMailOverviewByThreadsPaginated, getInboxMailsByThread, createInboxMailByThread, updateInboxMailThreadProgress, deleteInboxMailById } from "./service-mail";
+import KernButton from "../kern-button/KernButton";
+import { MemoIconPlus } from "../kern-icons/icons";
+import { IconUser, IconTrash, IconAlertTriangle, IconHelpCircle, IconProgressCheck, IconRefresh, IconCircleCheck } from "@tabler/icons-react";
+import { Tooltip } from "@nextui-org/react";
+import useRefState from "../../hooks/useRefState";
+import { MAIL_LIMIT_PER_PAGE, prepareThreadDisplayData, formatDisplayTimestamp, formatDisplayTimestampFull } from "./helper";
+import useEnumOptionsTranslated, { getEnumOptionsForLanguage } from "../../hooks/enums/useEnumOptionsTranslated";
+import { Dialog, Transition } from "@headlessui/react";
+import { Fragment } from "react";
+import { getUsers, getUserInfoExtended, getIsAdmin, getAllOrganizations } from "./service-mail";
+import Pagination from "../pagination/Pagination";
+import InboxMailAdminPanel from "../InboxMailAdminPanel";
+
+export default function InboxMailView(props: { InboxMailHeader, translatorScope }) {
+
+ const t = props.translatorScope?.translator;
+ const [inboxMailThreads, setInboxMailThreads] = useState
([]);
+ const [openCreateMail, setOpenCreateMail] = useState(false);
+ const [isNewThread, setIsNewThread] = useState(false);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [fullCount, setFullCount] = useState(0);
+ const [selectedThread, setSelectedThread] = useState(null);
+ const [threadMails, setThreadMails] = useState([]);
+ const [refreshing, setRefreshing] = useState(false);
+ const { state: isAdminSupportThread, setState: setIsAdminSupportThread, ref: isAdminSupportThreadRef } = useRefState(false);
+ const progressStateOptions =
+ props.translatorScope?.type === "local"
+ ? getEnumOptionsForLanguage(
+ InboxMailThreadSupportProgressState,
+ "InboxMailThreadSupportProgressState",
+ t,
+ "en"
+ )
+ : props.translatorScope?.type === "i18n" ? useEnumOptionsTranslated(
+ InboxMailThreadSupportProgressState,
+ "InboxMailThreadSupportProgressState",
+ "enums"
+ ) : [];
+
+ const [currentUser, setCurrentUser] = useState(null);
+ const [users, setUsers] = useState([]);
+ const [organizations, setOrganizations] = useState([]);
+ const [selectedOrganization, setSelectedOrganization] = useState(null);
+ const [isAdmin, setIsAdmin] = useState(undefined);
+
+ useEffect(() => {
+ getUserInfoExtended(res => {
+ setCurrentUser(res);
+ });
+ getIsAdmin((isAdmin) => setIsAdmin(isAdmin));
+ }, []);
+
+ useEffect(() => {
+ if (!isAdmin) return;
+ getAllOrganizations((res) => {
+ setOrganizations(res);
+ });
+ }, [isAdmin]);
+
+ useEffect(() => {
+ if (isAdmin === undefined) return;
+ if (isAdmin) {
+ getUsers((res) => setUsers(res), false, false, selectedOrganization?.id);
+ } else {
+ getUsers((res) => setUsers(res), false, true);
+ }
+ }, [isAdmin, selectedOrganization]);
+
+ useEffect(() => {
+ refetchInboxMailOverview();
+ }, [currentPage]);
+
+ useEffect(() => {
+ if (!selectedThread?.id) return
+ refetchSelectedThreadMails();
+ if (selectedThread.unreadMailCount > 0) {
+ setInboxMailThreads(prevThreads => prevThreads.map(t => t.id === selectedThread.id ? { ...t, unreadMailCount: 0 } : t));
+ }
+ if (selectedThread.isAdminSupportThread && isAdmin && selectedThread.metaData?.unreadMailCountAdmin > 0) {
+ setInboxMailThreads(prevThreads => prevThreads.map(t => t.id === selectedThread.id ? {
+ ...t,
+ metaData: {
+ ...t.metaData,
+ unreadMailCountAdmin: 0
+ }
+ } : t));
+ }
+ }, [selectedThread?.id]);
+
+ const refetchInboxMailOverview = useCallback(() => {
+ getInboxMailOverviewByThreadsPaginated(currentPage, MAIL_LIMIT_PER_PAGE, (res) => {
+ setInboxMailThreads(res.threads);
+ setFullCount(res?.totalThreads);
+ });
+ }, [currentPage]);
+
+ const refetchSelectedThreadMails = useCallback(() => {
+ if (!selectedThread) return;
+ getInboxMailsByThread(selectedThread.id, (res) => {
+ setThreadMails(res);
+ });
+ }, [selectedThread]);
+
+ const handleInboxMailCreation = useCallback((content: string, recipientIds?: string[], subject?: string, markAsImportant?: boolean, metaData?: any) => {
+ createInboxMailByThread(content, (result) => {
+ setOpenCreateMail(false);
+ getInboxMailOverviewByThreadsPaginated(currentPage, MAIL_LIMIT_PER_PAGE, (res) => {
+ if (isNewThread && res.threads.length > 0) {
+ setSelectedThread(res.threads.find((thread) => thread.id === result.threadId));
+ }
+ setInboxMailThreads(res.threads);
+ });
+ if (selectedThread && !isNewThread) {
+ refetchSelectedThreadMails();
+ }
+ }, recipientIds, subject, markAsImportant, metaData, isNewThread ? undefined : selectedThread?.id, isAdminSupportThreadRef.current);
+ }, [isNewThread, refetchSelectedThreadMails, selectedThread, currentPage]);
+
+ const handleInboxMailProgressChange = useCallback((progressState: InboxMailThreadSupportProgressState) => {
+ if (!selectedThread) return;
+ updateInboxMailThreadProgress(selectedThread.id, progressState, () => {
+ setSelectedThread({
+ ...selectedThread,
+ progressState: progressState
+ });
+ setInboxMailThreads(prevThreads => prevThreads.map(t => t.id === selectedThread.id ? { ...t, progressState: progressState } : t));
+ });
+ }, [selectedThread]);
+
+ const refreshIconFn = useCallback(
+ () => (
+
+ ),
+ [refreshing]
+ );
+
+ const setOffset = useCallback((offset: number) => setCurrentPage(~~(offset / MAIL_LIMIT_PER_PAGE + 1)), [])
+
+ const preparedThreads = useMemo(
+
+ () => inboxMailThreads.map(t => ({
+ ...t,
+ display: prepareThreadDisplayData(t, currentUser, isAdmin)
+ })),
+ [inboxMailThreads, currentUser, isAdmin]
+ )
+
+ if (!currentUser) return;
+
+ return (
+
+
+
+ {
+ setRefreshing(true);
+ refetchInboxMailOverview();
+ setTimeout(() => setRefreshing(false), 1000);
+ }}
+ disabled={refreshing}
+ />
+ {
+ setIsNewThread(true);
+ setOpenCreateMail(true);
+ setIsAdminSupportThread(true);
+ }} />
+ {
+ setIsNewThread(true);
+ setOpenCreateMail(true);
+ setIsAdminSupportThread(false);
+ }} />
+
+ props.InboxMailHeader >
+ {inboxMailThreads?.length === 0 ? (
+
+
No Inbox Mails
+
You have no mails in your inbox
+
+ ) :
+
+
+ {preparedThreads.map((threadOverview: InboxMailThread) => (
+
+ ))}
+
+
+
+
+
+ {selectedThread && threadMails && threadMails.length > 0 ? (
+ <>
+ {isAdmin && selectedThread.isAdminSupportThread &&
+
+ }
+ {threadMails.map((mail: InboxMail) => (
+
deleteInboxMailById(mail.id, () => {
+ if (threadMails.length === 1) {
+ setSelectedThread(null);
+ refetchInboxMailOverview();
+ return;
+ }
+ refetchSelectedThreadMails();
+ })}
+ />
+ ))}
+
+ {
+ setIsNewThread(false);
+ setOpenCreateMail(true);
+ setIsAdminSupportThread(selectedThread.isAdminSupportThread);
+ }}
+ />
+
+ >
+ )
+ :
+
Select mail to see the details
+
}
+
+
+ }
+
+
+
+ )
+}
+
+interface ThreadProps {
+ thread: InboxMailThread;
+ isAdmin: boolean;
+ selectedThread: InboxMailThread | null;
+ setSelectedThread: (t: InboxMailThread) => void;
+};
+
+function ThreadOverview(props: ThreadProps) {
+ const {
+ displayName,
+ displayInitials,
+ background,
+ text,
+ recipientIds,
+ DisplayIcon
+ } = props.thread.display;
+
+ const unreadMailCount = useMemo(() => {
+ if (props.isAdmin && props.thread.isAdminSupportThread) {
+ return props.thread.metaData.unreadMailCountAdmin
+ }
+ else {
+ return props.thread.unreadMailCount
+ }
+ }, [props.thread, props.isAdmin]);
+
+ return (
+ props.setSelectedThread(props.thread)}
+ >
+
+
+ {!props.thread.isAdminSupportThread || props.isAdmin ? (
+
+ {displayInitials ||
||
}
+ {recipientIds?.length > 1 && (
+
+ {recipientIds.length}
+
+ )}
+
+ ) : (
+
+

+
+ )}
+
+
+
+
+
+ {displayName ?? ""}
+
+ {unreadMailCount > 0 && (
+
+ {unreadMailCount} new
+
+ )}
+
+
+
+ {props.thread.isAdminSupportThread && (
+
+
+
+ )}
+ {props.thread.isImportant && (
+
+
+
+ )}
+ {props.thread.progressState === InboxMailThreadSupportProgressState.IN_PROGRESS && (
+
+
+
+ )}
+ {props.thread.progressState === InboxMailThreadSupportProgressState.RESOLVED && (
+
+
+
+ )}
+
+
+ {formatDisplayTimestamp(props.thread.latestMail?.createdAt)}
+
+
+
+
+
+ {props.thread.subject}
+
+
+ {props.thread.latestMail?.content}
+
+
+
+
+ );
+}
+
+interface ThreadMailItemProps {
+ mail: InboxMail;
+ currentUser: User;
+ onDelete?: (id: string) => void;
+}
+
+function ThreadMailItem(props: ThreadMailItemProps) {
+ const [openDeleteConfirm, setOpenDeleteConfirm] = useState(false);
+
+ const handleConfirmDelete = useCallback(() => {
+ if (props.onDelete) {
+ props.onDelete(props.mail.id);
+ }
+ }, [props.mail.id, props.onDelete]);
+
+ return (
+ <>
+
+
+
+
+
+ {props.mail.senderName?.first} {props.mail.senderName?.last}
+
+
+
+ {formatDisplayTimestampFull(props.mail.createdAt)}
+
+
+
+
+ To: {props.mail.recipientNames.map((name) => `${name.first} ${name.last}`).join(", ")}
+
+
+
+
+
+ {props.mail.content}
+
+
+
+ {props.currentUser.id === props.mail.senderId && (
+ setOpenDeleteConfirm(true)}
+ />
+ )}
+
+
+
+
+ >
+ );
+}
+interface ConfirmDeleteModalProps {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ onConfirm: () => void;
+}
+
+
+function ConfirmDeleteModal(props: ConfirmDeleteModalProps) {
+
+ const cancelRef = useRef(null);
+
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/inbox-mail/helper.ts b/components/inbox-mail/helper.ts
new file mode 100644
index 0000000..a939f7e
--- /dev/null
+++ b/components/inbox-mail/helper.ts
@@ -0,0 +1,267 @@
+import { red } from "@nextui-org/react";
+import { InboxMailThread, User } from "./types-mail";
+import { IconBug, IconUserOff } from "@tabler/icons-react";
+import { useEffect, useState } from "react";
+import { getNewInboxMailsInfo } from "./service-mail";
+
+export const MAIL_LIMIT_PER_PAGE = 8;
+
+export function prepareThreadDisplayData(
+ thread: InboxMailThread,
+ currentUser: User,
+ isAdmin: boolean
+): {
+ displayName: string;
+ displayInitials: string;
+ background: string;
+ text: string;
+ recipientIds: string[];
+ DisplayIcon?: React.FC
+} {
+ let background = "";
+ let text = "";
+ let displayName = "";
+ let displayInitials = "";
+ let DisplayIcon: React.FC = null;
+
+ if (!currentUser || !thread) return
+
+ const recipientIds = thread.participantIds.filter(
+ id => id !== currentUser.id
+ );
+
+ const setInitials = (first: string, last: string) =>
+ `${first[0]}${last[0]}`.toUpperCase();
+
+ const setColor = (id: string) => {
+ const [bg, font] = uuidToPastelColorWithMatchingFont(id);
+ background = bg;
+ text = font;
+ };
+
+ // This should not happen
+ if (!thread.latestMail) {
+ displayName = "";
+ DisplayIcon = IconUserOff
+ background = "#F5F5F5";
+ text = "#757575";
+ }
+ else if (!thread.isAdminSupportThread) {
+ if (thread.latestMail.senderId === currentUser.id) {
+ displayName = thread.latestMail.recipientNames.map(r => `${r.first} ${r.last}`).join(", ");
+ const r = thread.latestMail.recipientNames[0];
+ displayInitials = setInitials(r.first, r.last);
+ setColor(recipientIds[0]);
+ } else {
+ const s = thread.latestMail.senderName;
+ const first = s.first ?? "";
+ const last = s.last ?? "";
+ displayName = `${first} ${last}`.trim();
+ displayInitials = setInitials(first, last);
+ setColor(thread.latestMail.senderId);
+ }
+ } else if (thread.isAdminSupportThread && thread.metaData?.autoGenerated) {
+ if (thread.latestMail.senderId) {
+ displayName = thread.latestMail.recipientNames.map(r => `${r.first} ${r.last}`).join(", ");
+ }
+ else {
+ displayName = `${thread.latestMail.senderName.first} ${thread.latestMail.senderName.last}`;
+ DisplayIcon = IconBug
+ background = "#FFE5E5";
+ text = "#A30000";
+ }
+ } else if (thread.isAdminSupportThread && isAdmin && thread.latestMail.senderId === currentUser.id) {
+ displayName = `${thread.latestMail.senderName.first} ${thread.latestMail.senderName.last}`;
+ displayInitials = setInitials(thread.latestMail.senderName.first, thread.latestMail.senderName.last);
+ setColor(thread.latestMail.senderId);
+
+ } else if (thread.isAdminSupportThread && isAdmin) {
+ if (thread.latestMail.senderId === currentUser.id && thread.latestMail.recipientNames.length === 0) {
+ displayName = `${thread.latestMail.senderName.first} ${thread.latestMail.senderName.last}`;
+ } else if (thread.latestMail.senderId === currentUser.id) {
+ displayName = thread.latestMail.recipientNames.map(r => `${r.first} ${r.last}`).join(", ");
+ const r = thread.latestMail.recipientNames[0];
+ displayInitials = setInitials(r.first, r.last);
+ setColor(recipientIds[0]);
+ } else {
+ const s = thread.latestMail.senderName;
+ displayName = `${s.first} ${s.last}`;
+ displayInitials = setInitials(s.first ?? "", s.last ?? "");
+ setColor(thread.latestMail.senderId ?? thread.createdBy);
+ }
+ } else {
+ if (thread.latestMail.senderId === currentUser.id) {
+ displayName = `${thread.latestMail.senderName.first} ${thread.latestMail.senderName.last}`;
+ } else {
+ displayName = thread.latestMail.recipientNames.map(r => `${r.first} ${r.last}`).join(", ");
+ }
+ }
+
+ return {
+ displayName,
+ displayInitials,
+ background,
+ text,
+ recipientIds: recipientIds,
+ DisplayIcon
+ };
+}
+
+export function formatDisplayTimestamp(createdAt: string): string {
+ if (!createdAt) return "";
+ const date = parseUTCToLocal(createdAt);
+ const now = new Date();
+
+ const isSameDay = (d1: Date, d2: Date) =>
+ d1.getFullYear() === d2.getFullYear() &&
+ d1.getMonth() === d2.getMonth() &&
+ d1.getDate() === d2.getDate();
+
+ if (isSameDay(date, now)) {
+ const hours = date.getHours().toString().padStart(2, "0");
+ const minutes = date.getMinutes().toString().padStart(2, "0");
+ return `${hours}:${minutes}`;
+ }
+
+ const yesterday = new Date();
+ yesterday.setDate(now.getDate() - 1);
+ if (isSameDay(date, yesterday)) {
+ return "Yesterday";
+ }
+
+ const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ const weekday = weekdays[date.getDay()];
+ const day = date.getDate().toString().padStart(2, "0");
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
+ const year = date.getFullYear().toString().slice(-2);
+
+ return `${weekday}, ${day}.${month}.${year}`;
+}
+
+export function parseUTCToLocal(dateString: string): Date {
+ const d = new Date(dateString);
+
+ if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(dateString)) {
+ const parts = dateString.split(/[-T: ]/).map(Number);
+ return new Date(Date.UTC(parts[0], parts[1] - 1, parts[2], parts[3], parts[4] || 0, parts[5] || 0));
+ }
+
+ return d;
+}
+
+export function formatDisplayTimestampFull(createdAt: string): string {
+ const date = parseUTCToLocal(createdAt);
+ const now = new Date();
+
+ const diffMs = now.getTime() - date.getTime();
+ const diffMinutes = Math.floor(diffMs / (1000 * 60));
+ const diffHours = Math.floor(diffMinutes / 60);
+ const diffDays = Math.floor(diffHours / 24);
+
+ const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ const weekday = weekdays[date.getDay()];
+ const day = date.getDate().toString().padStart(2, "0");
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
+ const year = date.getFullYear().toString().padStart(4, "0");
+ const hours = date.getHours().toString().padStart(2, "0");
+ const minutes = date.getMinutes().toString().padStart(2, "0");
+
+ let relative = "";
+ if (diffMinutes < 1) {
+ relative = "(just now)";
+ } else if (diffMinutes < 60) {
+ relative = `(${diffMinutes} minute${diffMinutes !== 1 ? "s" : ""} ago)`;
+ } else if (diffHours < 24) {
+ relative = `(${diffHours} hour${diffHours !== 1 ? "s" : ""} ago)`;
+ } else if (diffDays === 1) {
+ relative = `(yesterday)`;
+ } else {
+ relative = `(${diffDays} day${diffDays !== 1 ? "s" : ""} ago)`;
+ }
+
+ return `${weekday}, ${day}.${month}.${year} ${hours}:${minutes} ${relative}`;
+}
+
+export function isSameDay(d1: Date, d2: Date): boolean {
+ return (
+ d1.getFullYear() === d2.getFullYear() &&
+ d1.getMonth() === d2.getMonth() &&
+ d1.getDate() === d2.getDate()
+ );
+}
+
+export function uuidToPastelColorWithMatchingFont(uuid: string): [string, string] {
+ let hash = 0;
+ for (let i = 0; i < uuid.length; i++) {
+ hash = (hash * 31 + uuid.charCodeAt(i)) >>> 0;
+ }
+
+ const hue = hash % 360;
+ const saturation = 40 + (hash % 15); // 40–55%
+ const lightness = 70 + (hash % 10); // 70–80%
+ const background = hslToHex(hue, saturation, lightness);
+
+ const textLightness = lightness - 40;
+ const text = hslToHex(hue, saturation, clamp(textLightness, 20, 90));
+ return [background, text];
+}
+
+export function hslToHex(h: number, s: number, l: number): string {
+ s /= 100;
+ l /= 100;
+
+ const k = (n: number) => (n + h / 30) % 12;
+ const a = s * Math.min(l, 1 - l);
+ const f = (n: number) =>
+ l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
+
+ const toHex = (x: number) =>
+ Math.round(255 * x)
+ .toString(16)
+ .padStart(2, "0");
+
+ return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`;
+}
+
+export function clamp(num: number, min: number, max: number): number {
+ return Math.min(Math.max(num, min), max);
+}
+
+
+export function useLocalTranslation(translations: Record) {
+ const t = (key: string) => {
+ const parts = key.split(".");
+ let current: any = translations;
+
+ for (const p of parts) {
+ if (current[p] === undefined) return key;
+ current = current[p];
+ }
+
+ return current;
+ };
+
+ return { t };
+}
+
+export function useNewMailCount(interval: number = 60000) {
+ const [newMailCount, setNewMailCount] = useState(0);
+
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+
+ function fetchNewMailCount() {
+ getNewInboxMailsInfo((result: any) => {
+ const count = result?.totalNewInboxMails ?? 0;
+ setNewMailCount(count);
+ });
+ }
+
+ fetchNewMailCount();
+ timer = setInterval(fetchNewMailCount, interval);
+
+ return () => clearInterval(timer);
+ }, [interval]);
+
+ return newMailCount;
+}
\ No newline at end of file
diff --git a/components/inbox-mail/inboxMailLocalTranslations.json b/components/inbox-mail/inboxMailLocalTranslations.json
new file mode 100644
index 0000000..1276ec0
--- /dev/null
+++ b/components/inbox-mail/inboxMailLocalTranslations.json
@@ -0,0 +1,21 @@
+{
+ "inboxMail": {
+ "modalTitle": "Send New Inbox Mail",
+ "sendTo": "Send To",
+ "subject": "Subject",
+ "content": "Content",
+ "markAsImportant": "Mark as Important",
+ "markAsImportantInfo": "Use this option to highlight messages that require immediate attention.",
+ "includeProjectInfo": "Reference project",
+ "includeChatInfo": "Reference chat",
+ "sendButton": "Send",
+ "cancelButton": "Cancel",
+ "previous": "Previous",
+ "next": "Next"
+ },
+ "InboxMailThreadSupportProgressState": {
+ "PENDING": "Pending",
+ "IN_PROGRESS": "In progress",
+ "RESOLVED": "Resolved"
+ }
+}
\ No newline at end of file
diff --git a/components/inbox-mail/service-mail.tsx b/components/inbox-mail/service-mail.tsx
new file mode 100644
index 0000000..500c098
--- /dev/null
+++ b/components/inbox-mail/service-mail.tsx
@@ -0,0 +1,73 @@
+import { FetchType, jsonFetchWrapper } from "@/submodules/javascript-functions/basic-fetch";
+import { InboxMailThreadSupportProgressState } from "./types-mail";
+
+const url = `/refinery-gateway/api/v1`;
+
+export function createInboxMailByThread(content: string, onResult: (result: any) => void, recipientIds?: string[], subject?: string, isImportant?: boolean, metaData?: any, threadId?: string, isAdminSupportThread?: boolean) {
+ const body = JSON.stringify({ recipientIds, threadId, subject, content, isImportant, metaData, isAdminSupportThread });
+ jsonFetchWrapper(`${url}/inbox-mail`, FetchType.POST, onResult, body);
+}
+
+
+export function getInboxMailOverviewByThreadsPaginated(page: number, limit: number, onResult: (result: any) => void) {
+ const fetchUrl = `${url}/inbox-mail/overview?page=${page}&limit=${limit}`;
+ jsonFetchWrapper(fetchUrl, FetchType.GET, onResult);
+}
+
+
+export function getInboxMailsByThread(threadId: string, onResult: (result: any) => void) {
+ const fetchUrl = `${url}/inbox-mail/thread/${threadId}`;
+ jsonFetchWrapper(fetchUrl, FetchType.GET, onResult);
+}
+
+
+export function updateInboxMailThreadProgress(threadId: string, progressState: InboxMailThreadSupportProgressState, onResult: (result: any) => void) {
+ const fetchUrl = `${url}/inbox-mail/thread/${threadId}/progress`;
+ const body = JSON.stringify({ progressState });
+ jsonFetchWrapper(fetchUrl, FetchType.PUT, onResult, body);
+}
+
+
+export function deleteInboxMailById(mailId: string, onResult: (result: any) => void) {
+ const fetchUrl = `${url}/inbox-mail/${mailId}`;
+ jsonFetchWrapper(fetchUrl, FetchType.DELETE, onResult);
+}
+
+
+export function getNewInboxMailsInfo(onResult: (result: any) => void) {
+ const fetchUrl = `${url}/inbox-mail/new`;
+ jsonFetchWrapper(fetchUrl, FetchType.GET, onResult);
+}
+
+export function getUsers(
+ onResult: (result: any) => void,
+ includeAdminSupport?: boolean,
+ limitedTeams?: boolean,
+ orgId?: string
+) {
+ const searchParams = new URLSearchParams();
+ if (includeAdminSupport) searchParams.append("include_admin_support", "true");
+ if (limitedTeams) searchParams.append("limited_teams", "true");
+ if (orgId) searchParams.append("org_id", orgId);
+
+ const finalUrl = `${url}/organization/all-users${searchParams.toString() ? `?${searchParams}` : ""}`;
+ jsonFetchWrapper(finalUrl, FetchType.GET, onResult);
+}
+
+
+export function getUserInfoExtended(onResult: (result: any) => void) {
+ const finalUrl = `${url}/organization/get-user-info-extended`;
+ jsonFetchWrapper(finalUrl, FetchType.GET, onResult);
+}
+
+
+export function getIsAdmin(onResult: (result: any) => void) {
+ const finalUrl = `${url}/misc/is-admin`;
+ jsonFetchWrapper(finalUrl, FetchType.GET, onResult);
+}
+
+
+export function getAllOrganizations(onResult: (result: any) => void) {
+ const finalUrl = `${url}/organization/all-organizations`;
+ jsonFetchWrapper(finalUrl, FetchType.GET, onResult);
+}
\ No newline at end of file
diff --git a/components/inbox-mail/types-mail.tsx b/components/inbox-mail/types-mail.tsx
new file mode 100644
index 0000000..f03605f
--- /dev/null
+++ b/components/inbox-mail/types-mail.tsx
@@ -0,0 +1,60 @@
+export type InboxMail = {
+ id: string;
+ threadId: string;
+ senderId: string;
+ content: string;
+ createdAt: string;
+ isSeen: boolean;
+ senderName?: {
+ first: string;
+ last: string;
+ };
+ recipientNames?: {
+ first: string;
+ last: string;
+ }[];
+}
+
+export type InboxMailThread = {
+ id: string;
+ subject: string;
+ isImportant: boolean;
+ isAdminSupportThread: boolean;
+ participantIds: string[];
+ latestMail: InboxMail;
+ createdBy: string;
+ progressState?: string;
+ supportOwnerId?: string;
+ metaData?: any;
+ unreadMailCount?: number;
+ projectName?: string;
+ conversationHeader?: string;
+ display?: {
+ displayName: string;
+ displayInitials: string;
+ background: string;
+ text: string;
+ recipientIds: string[];
+ DisplayIcon?: React.FC;
+ }
+}
+
+export type User = {
+ id: string;
+ organizationId: string;
+ firstName: string;
+ lastName: string;
+ mail: string;
+ role: string;
+ languageDisplay: string;
+ logoutUrl: string;
+ isAdmin: boolean;
+ autoLogoutMinutes: number;
+}
+
+
+export enum InboxMailThreadSupportProgressState {
+ PENDING = "PENDING",
+ IN_PROGRESS = "IN_PROGRESS",
+ RESOLVED = "RESOLVED"
+}
\ No newline at end of file
diff --git a/components/kern-icons/icons.ts b/components/kern-icons/icons.ts
index bb7ff6b..4c10820 100644
--- a/components/kern-icons/icons.ts
+++ b/components/kern-icons/icons.ts
@@ -1,5 +1,5 @@
import { memo } from 'react';
-import { IconActivity, IconAdjustments, IconAdjustmentsAlt, IconAdjustmentsOff, IconAlertCircle, IconAlertTriangle, IconAlertTriangleFilled, IconAngle, IconApi, IconArchive, IconArrowAutofitDown, IconArrowCurveRight, IconArrowDown, IconArrowLeft, IconArrowNarrowLeft, IconArrowNarrowRight, IconArrowRight, IconArrowsRandom, IconArrowsSort, IconArrowUp, IconArrowUpRight, IconAssembly, IconBallpen, IconBallpenOff, IconBell, IconBolt, IconBottle, IconBox, IconBoxOff, IconBrandGithub, IconBrandOpenai, IconBrandPython, IconBulb, IconBulldozer, IconCamera, IconCategoryPlus, IconCell, IconChartBubble, IconChartCircles, IconChartDots3, IconChartLine, IconChartPie, IconCheck, IconChecks, IconChevronCompactLeft, IconChevronCompactRight, IconChevronDown, IconChevronLeft, IconChevronRight, IconChevronUp, IconCircle, IconCircleCheck, IconCircleCheckFilled, IconCircleMinus, IconCirclePlus, IconClick, IconClipboard, IconClipboardCheck, IconClipboardOff, IconClock, IconCode, IconCodePlus, IconColorPicker, IconColumns, IconColumns1, IconColumns2, IconColumns3, IconCopy, IconCrown, IconCrownOff, IconDatabase, IconDatabasePlus, IconDeviceFloppy, IconDots, IconDotsVertical, IconDownload, IconEdit, IconEngine, IconExclamationCircle, IconExclamationMark, IconExternalLink, IconEye, IconEyeCancel, IconEyeCheck, IconEyeOff, IconFile, IconFileDownload, IconFileImport, IconFileInfo, IconFilePencil, IconFiles, IconFileText, IconFileUpload, IconFilter, IconFilterOff, IconFishHook, IconFolderBolt, IconGitCommit, IconGripVertical, IconHandClick, IconHeading, IconHelp, IconHexagons, IconHierarchy, IconHierarchy3, IconHistory, IconHome, IconHourglass, IconHourglassEmpty, IconInfoCircle, IconInfoCircleFilled, IconInfoSquare, IconLayoutList, IconLayoutNavbarCollapse, IconLayoutSidebar, IconLetterGSmall, IconLink, IconList, IconLoader, IconLoader2, IconLockAccess, IconMap, IconMaximize, IconMessageCircle, IconMinus, IconMessageCircleSearch, IconMessages, IconMinimize, IconMoustache, IconNews, IconNotes, IconPencil, IconPlayCardStar, IconPlayerPlay, IconPlayerPlayFilled, IconPlus, IconPoint, IconPointerOff, IconPointerSearch, IconPointFilled, IconQuestionMark, IconRecycle, IconRefresh, IconRefreshAlert, IconResize, IconRobot, IconRotate, IconScissors, IconScreenshot, IconSearch, IconSend, IconSettings, IconShare, IconShieldCheckFilled, IconShieldFilled, IconSquare, IconSquareCheck, IconStar, IconTag, IconTemplate, IconTerminal, IconThumbDown, IconThumbDownFilled, IconThumbUp, IconThumbUpFilled, IconTrash, IconTrashXFilled, IconTriangleInverted, IconTriangleSquareCircle, IconUpload, IconUser, IconUsersGroup, IconUserX, IconVariable, IconVariablePlus, IconVersions, IconWand, IconWaveSine, IconWebhook, IconWreckingBall, IconX, IconZoomCode, IconDragDrop2, IconCircleDotted, IconWorld } from '@tabler/icons-react';
+import { IconActivity, IconAdjustments, IconAdjustmentsAlt, IconAdjustmentsOff, IconAlertCircle, IconAlertTriangle, IconAlertTriangleFilled, IconAngle, IconApi, IconArchive, IconArrowAutofitDown, IconArrowCurveRight, IconArrowDown, IconArrowLeft, IconArrowNarrowLeft, IconArrowNarrowRight, IconArrowRight, IconArrowsRandom, IconArrowsSort, IconArrowUp, IconArrowUpRight, IconAssembly, IconBallpen, IconBallpenOff, IconBell, IconBolt, IconBottle, IconBox, IconBoxOff, IconBrandGithub, IconBrandOpenai, IconBrandPython, IconBulb, IconBulldozer, IconCamera, IconCategoryPlus, IconCell, IconChartBubble, IconChartCircles, IconChartDots3, IconChartLine, IconChartPie, IconCheck, IconChecks, IconChevronCompactLeft, IconChevronCompactRight, IconChevronDown, IconChevronLeft, IconChevronRight, IconChevronUp, IconCircle, IconCircleCheck, IconCircleCheckFilled, IconCircleMinus, IconCirclePlus, IconClick, IconClipboard, IconClipboardCheck, IconClipboardOff, IconClock, IconCode, IconCodePlus, IconColorPicker, IconColumns, IconColumns1, IconColumns2, IconColumns3, IconCopy, IconCrown, IconCrownOff, IconDatabase, IconDatabasePlus, IconDeviceFloppy, IconDots, IconDotsVertical, IconDownload, IconEdit, IconEngine, IconExclamationCircle, IconExclamationMark, IconExternalLink, IconEye, IconEyeCancel, IconEyeCheck, IconEyeOff, IconFile, IconFileDownload, IconFileImport, IconFileInfo, IconFilePencil, IconFiles, IconFileText, IconFileUpload, IconFilter, IconFilterOff, IconFishHook, IconFolderBolt, IconGitCommit, IconGripVertical, IconHandClick, IconHeading, IconHelp, IconHexagons, IconHierarchy, IconHierarchy3, IconHistory, IconHome, IconHourglass, IconHourglassEmpty, IconInfoCircle, IconInfoCircleFilled, IconInfoSquare, IconLayoutList, IconLayoutNavbarCollapse, IconLayoutSidebar, IconLetterGSmall, IconLink, IconList, IconLoader, IconLoader2, IconLockAccess, IconMap, IconMaximize, IconMessageCircle, IconMinus, IconMessageCircleSearch, IconMessages, IconMinimize, IconMoustache, IconNews, IconNotes, IconPencil, IconPlayCardStar, IconPlayerPlay, IconPlayerPlayFilled, IconPlus, IconPoint, IconPointerOff, IconPointerSearch, IconPointFilled, IconQuestionMark, IconRecycle, IconRefresh, IconRefreshAlert, IconResize, IconRobot, IconRotate, IconScissors, IconScreenshot, IconSearch, IconSend, IconSettings, IconShare, IconShieldCheckFilled, IconShieldFilled, IconSquare, IconSquareCheck, IconStar, IconTag, IconTemplate, IconTerminal, IconThumbDown, IconThumbDownFilled, IconThumbUp, IconThumbUpFilled, IconTrash, IconTrashXFilled, IconTriangleInverted, IconTriangleSquareCircle, IconUpload, IconUser, IconUsersGroup, IconUserX, IconVariable, IconVariablePlus, IconVersions, IconWand, IconWaveSine, IconWebhook, IconWreckingBall, IconX, IconZoomCode, IconDragDrop2, IconCircleDotted, IconWorld, IconMail } from '@tabler/icons-react';
export const MemoIconHome = memo(IconHome);
export const MemoIconInfoCircle = memo(IconInfoCircle);
@@ -184,4 +184,5 @@ export const MemoIconArrowNarrowRight = memo(IconArrowNarrowRight);
export const MemoIconMinus = memo(IconMinus);
export const MemoIconDragDrop2 = memo(IconDragDrop2);
export const MemoIconCircleDotted = memo(IconCircleDotted);
-export const MemoIconWorld = memo(IconWorld);
\ No newline at end of file
+export const MemoIconWorld = memo(IconWorld);
+export const MemoIconMail = memo(IconMail);
diff --git a/components/pagination/Pagination.tsx b/components/pagination/Pagination.tsx
new file mode 100644
index 0000000..3b9af42
--- /dev/null
+++ b/components/pagination/Pagination.tsx
@@ -0,0 +1,49 @@
+import { PaginationProps } from '../../types/pagination'
+import { MemoIconArrowLeft, MemoIconArrowRight } from '../kern-icons/icons';
+import { useEffect, useMemo, useState } from 'react';
+
+
+export default function Pagination(props: PaginationProps) {
+
+ const [currentPage, setCurrentPage] = useState(0);
+ const [totalPages, setTotalPages] = useState(0);
+
+ useMemo(() => {
+ setCurrentPage(props.offset / props.limit + 1);
+ }, [props.offset, props.limit]);
+
+ useMemo(() => {
+ setTotalPages(Math.ceil(props.fullCount / props.limit));
+ }, [props.fullCount, props.limit]);
+
+ useEffect(() => {
+ props.setOffset((currentPage - 1) * props.limit);
+ }, [currentPage, props.limit]);
+
+ return (
+
+ )
+}
diff --git a/hooks/enums/useEnumOptionsTranslated.tsx b/hooks/enums/useEnumOptionsTranslated.tsx
index d33e602..262729f 100644
--- a/hooks/enums/useEnumOptionsTranslated.tsx
+++ b/hooks/enums/useEnumOptionsTranslated.tsx
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
const cache = {};
-function getEnumOptionsForLanguage(enumObj: T, lookupKey: string, t: any, language: string): { name: string, value: T[keyof T] }[] {
+export function getEnumOptionsForLanguage(enumObj: T, lookupKey: string, t: any, language: string): { name: string, value: T[keyof T] }[] {
if (!(lookupKey in cache)) cache[lookupKey] = {};
if (cache[lookupKey][language]) return cache[lookupKey][language];
const finalArray = enumToArray(enumObj, { nameFunction: (s) => { return t(`${lookupKey}.${s}`) } });
diff --git a/types/pagination.ts b/types/pagination.ts
new file mode 100644
index 0000000..3f4689c
--- /dev/null
+++ b/types/pagination.ts
@@ -0,0 +1,8 @@
+export type PaginationProps = {
+ offset: number;
+ setOffset: (offset: number) => void;
+ fullCount: number;
+ limit: number;
+ previousLabel?: string;
+ nextLabel?: string;
+}
\ No newline at end of file