diff --git a/package.json b/package.json
index 14ae99a..a8419eb 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,15 @@
"@mui/material-nextjs": "^5.15.0",
"@mui/x-data-grid": "^6.18.7",
"@mui/x-date-pickers": "^6.18.7",
+ "@tiptap/extension-color": "^2.5.8",
+ "@tiptap/extension-highlight": "^2.5.8",
+ "@tiptap/extension-list-item": "^2.5.9",
+ "@tiptap/extension-text-align": "^2.5.9",
+ "@tiptap/extension-text-style": "^2.5.8",
+ "@tiptap/extension-underline": "^2.5.8",
+ "@tiptap/pm": "^2.5.8",
+ "@tiptap/react": "^2.5.8",
+ "@tiptap/starter-kit": "^2.5.8",
"@unly/universal-language-detector": "^2.0.3",
"apexcharts": "^3.45.2",
"date-holidays": "^3.23.11",
@@ -29,8 +38,10 @@
"i18next": "^23.7.11",
"i18next-resources-to-backend": "^1.2.0",
"lodash": "^4.17.21",
+ "mui-color-input": "^3.0.0",
"next": "14.0.4",
"next-auth": "^4.24.7",
+ "next-intl": "^3.13.0",
"next-pwa": "^5.6.0",
"react": "^18",
"react-apexcharts": "^1.4.1",
diff --git a/src/app/(main)/settings/mail/page.tsx b/src/app/(main)/settings/mail/page.tsx
new file mode 100644
index 0000000..310e39f
--- /dev/null
+++ b/src/app/(main)/settings/mail/page.tsx
@@ -0,0 +1,41 @@
+import { getServerI18n } from "@/i18n";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { Metadata } from "next";
+import { Suspense } from "react";
+import { I18nProvider } from "@/i18n";
+import { getUserAbilities } from "@/app/utils/commonUtil";
+import { preloadMails } from "@/app/api/mail";
+import MailSetting from "@/components/MailSetting";
+
+export const metadata: Metadata = {
+ title: "Mail",
+};
+
+const Customer: React.FC = async () => {
+ const { t } = await getServerI18n("mail");
+ preloadMails();
+ const abilities = await getUserAbilities()
+
+ return (
+ <>
+
+
+ {t("Mail")}
+
+
+
+ }>
+
+
+
+ >
+ );
+};
+
+export default Customer;
\ No newline at end of file
diff --git a/src/app/api/mail/actions.ts b/src/app/api/mail/actions.ts
new file mode 100644
index 0000000..35812da
--- /dev/null
+++ b/src/app/api/mail/actions.ts
@@ -0,0 +1,26 @@
+"use server";
+
+import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
+import { BASE_API_URL } from "@/config/api";
+import { MailSetting, MailTemplate } from ".";
+
+export interface MailSave {
+ settings: MailSetting[];
+ template: MailTemplate;
+}
+
+export const saveMail = async (data: MailSave) => {
+ return serverFetchJson(`${BASE_API_URL}/mails/save`, {
+ method: "POST",
+ body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ });
+};
+
+export const testMail = async () => {
+ return serverFetchWithNoContent(`${BASE_API_URL}/mails/test`, {
+ method: "GET",
+ // body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ });
+};
\ No newline at end of file
diff --git a/src/app/api/mail/index.ts b/src/app/api/mail/index.ts
new file mode 100644
index 0000000..d6239c6
--- /dev/null
+++ b/src/app/api/mail/index.ts
@@ -0,0 +1,42 @@
+import { serverFetchJson } from "@/app/utils/fetchUtil";
+import { BASE_API_URL } from "@/config/api";
+import { cache } from "react";
+
+export interface MailSMTP {
+ host: string;
+ port: number;
+ username: string;
+ password: string;
+}
+
+export interface MailSetting {
+ id: number;
+ name: string;
+ value: string;
+ category: string;
+ type: string;
+}
+
+export interface MailTemplate {
+ cc?: string;
+ bcc?: string;
+ subject?: string;
+ template?: string;
+}
+
+export const preloadMails = () => {
+ fetchMailSetting();
+ fetchMailTimesheetTemplate();
+};
+
+export const fetchMailSetting = cache(async () => {
+ return serverFetchJson(`${BASE_API_URL}/mails/setting`, {
+ next: { tags: ["mailSetting"] },
+ });
+});
+
+export const fetchMailTimesheetTemplate = cache(async () => {
+ return serverFetchJson(`${BASE_API_URL}/mails/timesheet-template`, {
+ next: { tags: ["mailTimesheetTemplate"] },
+ });
+});
diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx
index cec5d05..dcb9b9c 100644
--- a/src/components/Breadcrumb/Breadcrumb.tsx
+++ b/src/components/Breadcrumb/Breadcrumb.tsx
@@ -63,6 +63,7 @@ const pathToLabelMap: { [path: string]: string } = {
"/settings/group/edit": "Edit User Group",
"/settings/group/create": "Create User Group",
"/settings/holiday": "Company Holiday",
+ "/settings/mail": "Mail",
"/analytics": "Analysis Report",
"/analytics/LateStartReport": "Late Start Report",
"/analytics/ProjectPotentialDelayReport": "Project Potential Delay Report",
diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx
index e07352b..1d68084 100644
--- a/src/components/CreateProject/ProjectClientDetails.tsx
+++ b/src/components/CreateProject/ProjectClientDetails.tsx
@@ -153,21 +153,21 @@ const ProjectClientDetails: React.FC = ({
selectedCustomerId === defaultValues?.clientId &&
Boolean(defaultValues?.clientSubsidiaryId)
? contacts.find(
- (contact) => contact.id === defaultValues.clientContactId,
- )?.id ?? contacts[0].id
- : contacts[0].id,
+ (contact) => contact?.id === defaultValues?.clientContactId,
+ )?.id ?? contacts[0]?.id
+ : contacts[0]?.id,
);
setValue("isSubsidiaryContact", true);
} else if (customerContacts?.length > 0) {
setSubsidiaryContacts(() => []);
setValue(
- "clientContactId",
- selectedCustomerId === defaultValues?.clientId &&
- !Boolean(defaultValues?.clientSubsidiaryId)
+ "clientContactId",
+ selectedCustomerId === defaultValues?.clientId &&
+ !Boolean(defaultValues?.clientSubsidiaryId)
? customerContacts.find(
(contact) => contact.id === defaultValues.clientContactId,
- )?.id ?? customerContacts[0].id
- : customerContacts[0].id,
+ )?.id ?? customerContacts[0].id
+ : customerContacts[0].id
);
setValue("isSubsidiaryContact", false);
}
diff --git a/src/components/MailField/MailField.css b/src/components/MailField/MailField.css
new file mode 100644
index 0000000..bfa2515
--- /dev/null
+++ b/src/components/MailField/MailField.css
@@ -0,0 +1,61 @@
+/* Root styles */
+:not(.tiptap-error) .tiptap {
+ padding-left: 15px;
+ padding-right: 15px;
+ background-color: transparent;
+ border-radius: 8px;
+ border-style: solid;
+ border-width: 1px;
+ overflow: hidden;
+ border-color: #e0e0e0;
+ /* palette.neutral[200] */
+ transition: border-color 0.3s, box-shadow 0.3s;
+ /* Assuming muiTheme.transitions.create(["border-color", "box-shadow"]) translates to 0.3s for both */
+}
+
+.tiptap-error {
+ background-color: transparent;
+ border-radius: 8px;
+ border-style: solid;
+ border-width: 1px;
+ overflow: hidden;
+ /* palette.neutral[200] */
+ transition: border-color 0.3s, box-shadow 0.3s;
+
+ border-color: #F04438;
+ /* palette.primary.main */
+ box-shadow: #F04438 0 0 0 2px;
+ /* palette.primary.main */
+}
+
+.tiptap:hover {
+ background-color: #f5f5f5;
+ /* palette.action.hover */
+}
+
+.tiptap::before {
+ display: none;
+}
+
+.tiptap::after {
+ display: none;
+}
+
+:not(.tiptap-error) > .tiptap:focus {
+ background-color: transparent;
+ border-color: #8dba00;
+ /* palette.primary.main */
+ box-shadow: #8dba00 0 0 0 2px;
+ /* palette.primary.main */
+}
+
+.ProseMirror:focus {
+ outline: none;
+}
+
+/* Input styles */
+/* .tiptap-input {
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 12px;
+} */
\ No newline at end of file
diff --git a/src/components/MailField/MailField.tsx b/src/components/MailField/MailField.tsx
new file mode 100644
index 0000000..915e3ec
--- /dev/null
+++ b/src/components/MailField/MailField.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import "./MailField.css"
+import Document from '@tiptap/extension-document'
+import Paragraph from '@tiptap/extension-paragraph'
+import Text from '@tiptap/extension-text'
+import Underline from '@tiptap/extension-underline'
+import { useEditor, EditorContent, Extension } from "@tiptap/react"
+import StarterKit from "@tiptap/starter-kit"
+import MailToolbar from "./MailToolbar";
+import { Grid } from "@mui/material";
+import Highlight from '@tiptap/extension-highlight'
+import { Color } from '@tiptap/extension-color'
+import ListItem from "@tiptap/extension-list-item";
+import TextStyle from "@tiptap/extension-text-style";
+import TextAlign from '@tiptap/extension-text-align'
+
+interface Props {
+ content?: string,
+ onChange?: (richText: string) => void,
+ error?: boolean,
+}
+
+const MailField: React.FC = ({
+ content,
+ onChange,
+ error
+}) => {
+
+ const TAB_CHAR = '\u0009';
+
+ const TabHandler = Extension.create({
+ name: 'tabHandler',
+ addKeyboardShortcuts() {
+ return {
+ Tab: ({ editor }) => {
+ // Sinks a list item / inserts a tab character
+ editor
+ .chain()
+ .sinkListItem('listItem')
+ .command(({ tr }) => {
+ tr.insertText(TAB_CHAR);
+ return true;
+ })
+ .run();
+ // Prevent default behavior (losing focus)
+ return true;
+ },
+ };
+ },
+ });
+
+ const editor = useEditor({
+ extensions: [
+ StarterKit.configure(),
+ Document,
+ Paragraph,
+ Text,
+ TextStyle,
+ TextAlign.configure({
+ types: ['heading', 'paragraph']
+ }),
+ Underline,
+ Highlight.configure({ multicolor: true }),
+ Color,
+ ListItem,
+ TabHandler
+ ],
+ content: content,
+ onUpdate({ editor }) {
+ if (onChange) {
+ onChange(editor.getHTML())
+ }
+ console.log(editor.getHTML())
+ },
+ })
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MailField;
\ No newline at end of file
diff --git a/src/components/MailField/MailFieldWrapper.tsx b/src/components/MailField/MailFieldWrapper.tsx
new file mode 100644
index 0000000..e5d2bf3
--- /dev/null
+++ b/src/components/MailField/MailFieldWrapper.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import React from "react";
+import MailField from "./MailField";
+
+export interface Props {
+ content?: string,
+ onChange?: (richText: string) => void,
+ error?: boolean,
+}
+
+const TransferListWrapper: React.FC = ({
+ content,
+ onChange,
+ error
+}) => {
+
+ return ;
+};
+
+export default TransferListWrapper;
diff --git a/src/components/MailField/MailToolbar.tsx b/src/components/MailField/MailToolbar.tsx
new file mode 100644
index 0000000..efaadbe
--- /dev/null
+++ b/src/components/MailField/MailToolbar.tsx
@@ -0,0 +1,319 @@
+"use client";
+
+import { Button, ButtonGroup, Grid, IconButton, ToggleButton, ToggleButtonGroup } from "@mui/material";
+import "./MailField.css"
+import { useEditor, EditorContent, Editor } from "@tiptap/react"
+import FormatBoldIcon from '@mui/icons-material/FormatBold';
+import React, { useCallback } from "react";
+import { FormatItalic, FormatUnderlined } from "@mui/icons-material";
+import { MuiColorInput, MuiColorInputColors, MuiColorInputValue } from 'mui-color-input'
+import BorderColorIcon from '@mui/icons-material/BorderColor';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import FormatColorTextIcon from '@mui/icons-material/FormatColorText';
+import FormatAlignLeftIcon from '@mui/icons-material/FormatAlignLeft';
+import FormatAlignJustifyIcon from '@mui/icons-material/FormatAlignJustify';
+import FormatAlignRightIcon from '@mui/icons-material/FormatAlignRight';
+
+interface Props {
+ editor: Editor | null;
+}
+
+const colorInputSx = {
+ width: 150,
+ height: 25,
+ ".MuiInputBase-colorPrimary": {
+ margin: -1,
+ borderRadius: "0px 5px 5px 0px",
+ borderColor: "rgba(0, 0, 0, 0)",
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ },
+ ".Mui-focused": {
+ borderColor: "rgba(0, 0, 0, 0)",
+ },
+ ".MuiColorInput-Button": {
+ marginBottom: 2,
+ borderColor: "rgba(0, 0, 0, 0)",
+ backgroundColor: "rgba(0, 0, 0, 0)",
+ },
+ ".MuiInputBase-input": {
+ marginBottom: 1.5,
+ },
+}
+
+const fontFamily = [
+ {
+ label: 'Arial',
+ value: 'Arial',
+ },
+ {
+ label: 'Times New Roman',
+ value: 'Times New Roman',
+ },
+ {
+ label: 'Courier New',
+ value: 'Courier New',
+ },
+ {
+ label: 'Georgia',
+ value: 'Georgia',
+ },
+ {
+
+ }
+]
+
+const MailToolbar: React.FC = ({
+ editor
+}) => {
+
+ if (editor == null) {
+ return null
+ }
+
+ const [fontStyle, setFontStyle] = React.useState(() => ["alignLeft"]);
+ const [colorHighlightValue, setColorHighlightValue] = React.useState("red");
+ const [colorTextValue, setColorTextValue] = React.useState("black");
+ const colorHighlightValueInputRef = React.useRef(null);
+ const colorTextValueInputRef = React.useRef(null);
+
+ const handleFontStyle = useCallback((
+ event: React.MouseEvent,
+ newFontStyles: string[],
+ ) => {
+
+ setFontStyle((prev) => {
+ const id = event.currentTarget?.id
+ const include = prev.includes(id)
+
+ if (include) {
+ return prev.filter(ele => ele !== id)
+ } else {
+ prev = prev.filter(ele => !ele.includes("align"))
+ prev.push(id)
+ return prev
+ }
+ });
+ }, []);
+
+ const handleColorHighlightValue = useCallback((value: string, colors: MuiColorInputColors) => {
+ // console.log(colors)
+ setColorHighlightValue(() => value)
+ // editor.chain().focus().toggleHighlight({ color: value }).run()
+ }, [])
+
+ const handleColorHighlightValueClick = useCallback((event: React.MouseEvent) => {
+ editor.chain().focus().toggleHighlight({ color: colorHighlightValue.toString() }).run()
+ }, [colorHighlightValue])
+
+ const handleColorHighlightValueClose = useCallback((event: {}, reason: "backdropClick" | "escapeKeyDown") => {
+ // console.log(event)
+ editor.chain().focus().toggleHighlight({ color: colorHighlightValue.toString() }).run()
+ }, [colorHighlightValue])
+
+ const handleColorHighlightValueBlur = useCallback((event: React.FocusEvent) => {
+ editor.chain().focus().toggleHighlight({ color: colorHighlightValue.toString() }).run()
+ }, [colorHighlightValue])
+
+ const handleColorTextValue = useCallback((value: string, colors: MuiColorInputColors) => {
+ // console.log(colors)
+ setColorTextValue(() => value)
+ // if (editor.isActive("textStyle")) {
+ // editor.chain().focus().unsetColor().run()
+ // } else {
+ // editor.chain().focus().setColor(value).run()
+ // }
+ }, [])
+
+ const handleColorTextValueClick = useCallback((event: React.MouseEvent) => {
+ if (editor.isActive("textStyle", { color: colorTextValue.toString() })) {
+ editor.chain().focus().unsetColor().run()
+ } else {
+ editor.chain().focus().setColor(colorTextValue.toString()).run()
+ }
+ }, [colorTextValue])
+
+ const handleColorTextValueClose = useCallback((event: {}, reason: "backdropClick" | "escapeKeyDown") => {
+ if (editor.isActive("textStyle", { color: colorTextValue.toString() })) {
+ editor.chain().focus().unsetColor().run()
+ } else {
+ editor.chain().focus().setColor(colorTextValue.toString()).run()
+ }
+ }, [colorTextValue])
+
+ const handleColorTextValueBlur = useCallback((event: React.FocusEvent) => {
+ if (editor.isActive("textStyle", { color: colorTextValue.toString() })) {
+ editor.chain().focus().unsetColor().run()
+ } else {
+ editor.chain().focus().setColor(colorTextValue.toString()).run()
+ }
+ }, [colorTextValue])
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ if (colorHighlightValueInputRef.current !== null) {
+ colorHighlightValueInputRef.current.blur();
+ }
+
+ if (colorTextValueInputRef.current !== null) {
+ colorTextValueInputRef.current.blur();
+ }
+ }
+ }
+
+ const handleTextAlign = useCallback((event: React.MouseEvent, value: any) => {
+ console.log(value)
+ switch (value) {
+ case "alignLeft":
+ if (editor.isActive({ textAlign: 'left' })) {
+ editor.chain().focus().unsetTextAlign().run()
+ } else {
+ editor.chain().focus().setTextAlign('left').run()
+ }
+ break;
+ case "alignCenter":
+ if (editor.isActive({ textAlign: 'center' })) {
+ editor.chain().focus().unsetTextAlign().run()
+ } else {
+ editor.chain().focus().setTextAlign('center').run()
+ }
+ break;
+ case "alignRight":
+ if (editor.isActive({ textAlign: 'right' })) {
+ editor.chain().focus().unsetTextAlign().run()
+ } else {
+ editor.chain().focus().setTextAlign('right').run()
+ }
+ break;
+ default:
+ break;
+ }
+ }, [])
+
+ React.useEffect(() => {
+ editor.on('selectionUpdate', ({ editor }) => {
+ const currentFormatList: string[] = []
+ if (editor.isActive("bold")) {
+ currentFormatList.push("bold")
+ }
+
+ if (editor.isActive("italic")) {
+ currentFormatList.push("italic")
+ }
+
+ if (editor.isActive("underline")) {
+ currentFormatList.push("underline")
+ }
+
+ if (editor.isActive("highlight", { color: colorHighlightValue.toString() })) {
+ currentFormatList.push("highlight")
+ }
+
+ if (editor.isActive("textStyle", { color: colorTextValue.toString() })) {
+ currentFormatList.push("textStyle")
+ }
+
+ if (editor.isActive({ textAlign: 'left' })) {
+ currentFormatList.push("alignLeft")
+ }
+
+ if (editor.isActive({ textAlign: 'center' })) {
+ currentFormatList.push("alignCenter")
+ }
+
+ if (editor.isActive({ textAlign: 'right' })) {
+ currentFormatList.push("alignRight")
+ }
+
+ console.log(currentFormatList)
+ setFontStyle(() => currentFormatList)
+ })
+ }, [editor])
+
+ return (
+
+
+ editor.chain().focus().toggleBold().run()}>
+
+
+ editor.chain().focus().toggleItalic().run()}>
+
+
+ editor.chain().focus().toggleUnderline().run()}>
+
+
+
+
+
+
+
+
+ {/* console.log("Expand more")}>
+
+ */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MailToolbar;
\ No newline at end of file
diff --git a/src/components/MailField/index.ts b/src/components/MailField/index.ts
new file mode 100644
index 0000000..ab7f59d
--- /dev/null
+++ b/src/components/MailField/index.ts
@@ -0,0 +1 @@
+export { default } from "./MailFieldWrapper";
\ No newline at end of file
diff --git a/src/components/MailSetting/MailSetting.tsx b/src/components/MailSetting/MailSetting.tsx
new file mode 100644
index 0000000..4afdd41
--- /dev/null
+++ b/src/components/MailSetting/MailSetting.tsx
@@ -0,0 +1,183 @@
+"use client";
+
+import Check from "@mui/icons-material/Check";
+import Close from "@mui/icons-material/Close";
+import Button from "@mui/material/Button";
+import Stack from "@mui/material/Stack";
+import { useRouter } from "next/navigation";
+import React, { useCallback, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ FieldErrors,
+ FormProvider,
+ SubmitErrorHandler,
+ SubmitHandler,
+ useForm,
+} from "react-hook-form";
+import { Tab, Tabs, TabsProps, Typography } from "@mui/material";
+import TimesheetMailDetails from "./TimesheetMailDetails";
+import { Error } from "@mui/icons-material";
+import { MailSave, saveMail, testMail } from "@/app/api/mail/actions";
+import SettingDetails from "./SettingDetails";
+import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts";
+
+export interface Props {
+ defaultInputs?: MailSave,
+}
+
+const hasErrorsInTab = (
+ tabIndex: number,
+ errors: FieldErrors,
+) => {
+ switch (tabIndex) {
+ case 0:
+ return (
+ errors.settings
+ );
+ case 1:
+ return (
+ errors.template
+ );
+ default:
+ false;
+ }
+};
+
+const MailSetting: React.FC = ({
+ defaultInputs,
+}) => {
+ const [serverError, setServerError] = useState("");
+ const { t } = useTranslation();
+ const router = useRouter();
+ const [tabIndex, setTabIndex] = useState(0);
+ const [test, setTest] = useState(false)
+
+ const handleTabChange = useCallback>(
+ (_e, newValue) => {
+ setTabIndex(newValue);
+ },
+ [],
+ );
+
+ const formProps = useForm({
+ defaultValues: defaultInputs
+ });
+
+ const handleCancel = () => {
+ router.back();
+ };
+
+ const onSubmit = useCallback>(
+ async (data) => {
+ try {
+ console.log(data);
+
+ let haveError = false
+ // if (data.name.length === 0) {
+ // haveError = true
+ // formProps.setError("name", { message: "Name is empty", type: "required" })
+ // }
+
+ if (haveError) {
+ return false
+ }
+
+
+ setServerError("");
+
+ submitDialog(async () => {
+ const response = await saveMail(data);
+
+ console.log(response)
+ if (response !== null) {
+ if (test) {
+ await testMail()
+ }
+
+ successDialog(t("Save Success"), t)
+ } else {
+ errorDialog(t("Save Fail"), t).then(() => {
+ // formProps.setError("code", { message: response.message, type: "custom" })
+ // setTabIndex(0)
+ return false
+ })
+ }
+ }, t)
+ } catch (e) {
+ console.log(e)
+ setServerError(t("An error has occurred. Please try again later."));
+ }
+ },
+ [router, t, test],
+ );
+
+ const onSubmitError = useCallback>(
+ (errors) => {
+ console.log(errors)
+ },
+ [],
+ );
+
+ const errors = formProps.formState.errors;
+
+ return (
+
+
+ {serverError && (
+
+ {serverError}
+
+ )}
+
+
+ ) : undefined
+ }
+ iconPosition="end"
+ />
+
+ ) : undefined
+ }
+ iconPosition="end"
+ />
+
+
+
+
+
+ }
+ onClick={handleCancel}
+ >
+ {t("Cancel")}
+
+ } onClick={() => setTest(true)} type="submit">
+ {t("Save And Test")}
+
+ } onClick={() => setTest(false)} type="submit">
+ {t("Save")}
+
+
+
+
+ );
+};
+
+export default MailSetting;
\ No newline at end of file
diff --git a/src/components/MailSetting/MailSettingLoading.tsx b/src/components/MailSetting/MailSettingLoading.tsx
new file mode 100644
index 0000000..9cd157b
--- /dev/null
+++ b/src/components/MailSetting/MailSettingLoading.tsx
@@ -0,0 +1,38 @@
+import Card from "@mui/material/Card";
+import CardContent from "@mui/material/CardContent";
+import Skeleton from "@mui/material/Skeleton";
+import Stack from "@mui/material/Stack";
+import React from "react";
+
+// Can make this nicer
+export const MailSettingLoading: React.FC = () => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default MailSettingLoading;
\ No newline at end of file
diff --git a/src/components/MailSetting/MailSettingWrapper.tsx b/src/components/MailSetting/MailSettingWrapper.tsx
new file mode 100644
index 0000000..f713c10
--- /dev/null
+++ b/src/components/MailSetting/MailSettingWrapper.tsx
@@ -0,0 +1,40 @@
+import { getUserAbilities } from "@/app/utils/commonUtil";
+import MailSetting from "./MailSetting";
+import MailSettingLoading from "./MailSettingLoading";
+import { MailTemplate, fetchMailSetting, fetchMailTimesheetTemplate } from "@/app/api/mail";
+
+interface SubComponents {
+ Loading: typeof MailSettingLoading;
+}
+
+const MailSettingWrapper: React.FC & SubComponents = async () => {
+ const [
+ abilities,
+ settings,
+ timesheetTemplate,
+ ] = await Promise.all([
+ getUserAbilities(),
+ fetchMailSetting(),
+ fetchMailTimesheetTemplate()
+ ]);
+
+ const tempTimesheetTemplate: MailTemplate = {
+ cc: timesheetTemplate.find(template => template.name.includes(".cc"))?.value,
+ bcc: timesheetTemplate.find(template => template.name.includes(".bcc"))?.value,
+ subject: timesheetTemplate.find(template => template.name.includes(".subject"))?.value,
+ template: timesheetTemplate.find(template => template.name.includes(".template"))?.value,
+ }
+ return (
+
+ );
+};
+
+MailSettingWrapper.Loading = MailSettingLoading;
+
+
+export default MailSettingWrapper;
diff --git a/src/components/MailSetting/SettingDetails.tsx b/src/components/MailSetting/SettingDetails.tsx
new file mode 100644
index 0000000..9f81765
--- /dev/null
+++ b/src/components/MailSetting/SettingDetails.tsx
@@ -0,0 +1,143 @@
+"use client";
+
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Card from "@mui/material/Card";
+import CardContent from "@mui/material/CardContent";
+import Grid from "@mui/material/Grid";
+import TextField from "@mui/material/TextField";
+import Typography from "@mui/material/Typography";
+import { useTranslation } from "react-i18next";
+import { Controller, useFieldArray, useFormContext } from "react-hook-form";
+import Link from "next/link";
+import React from "react";
+import MailField from "../MailField/MailField";
+import { MailSetting } from "@/app/api/mail";
+import { MailSave } from "@/app/api/mail/actions";
+import { Checkbox, IconButton, InputAdornment } from "@mui/material";
+import { Visibility, VisibilityOff } from "@mui/icons-material";
+
+interface Props {
+ isActive: boolean;
+}
+
+const SettingDetails: React.FC = ({ isActive }) => {
+ const requiredFields = ["host", "port", "username"]
+ const { t } = useTranslation();
+ const {
+ register,
+ formState: { errors },
+ control,
+ watch
+ } = useFormContext();
+
+ const { fields } = useFieldArray({
+ control,
+ name: "settings"
+ })
+
+ const [showSMTPPassword, setShowSMTPPassword] = React.useState(false)
+
+ const handleClickShowPassword = () => setShowSMTPPassword((show) => !show);
+
+ const handleMouseDownPassword = (event: React.MouseEvent) => {
+ event.preventDefault();
+ };
+
+ return (
+
+
+
+
+ {t("Settings")}
+
+ {
+ fields.map((field, index) => (
+
+
+ {t(field.name)}
+
+ {
+ field.name.toLowerCase().includes("password") === true ?
+
+
+
+ {showSMTPPassword ? : }
+
+
+ ),
+ }}
+ fullWidth
+ {...register(`settings.${index}.value`)}
+ />
+
+ :
+ field.type.toLowerCase() === "boolean" ?
+
+
+
+ :
+ field.type.toLowerCase() === "integer" ?
+
+ field.name.toLowerCase().includes(name))
+ })}
+ error={Boolean(
+ errors.settings
+ && errors.settings.length
+ && errors.settings.length > index
+ && errors.settings[index]?.value
+ )}
+ />
+
+ :
+
+ field.name.toLowerCase().includes(name))
+ })}
+ error={Boolean(
+ errors.settings
+ && errors.settings.length
+ && errors.settings.length > index
+ && errors.settings[index]?.value
+ )}
+ />
+
+ }
+
+ ))
+ }
+
+
+
+ );
+};
+
+export default SettingDetails;
\ No newline at end of file
diff --git a/src/components/MailSetting/TimesheetMailDetails.tsx b/src/components/MailSetting/TimesheetMailDetails.tsx
new file mode 100644
index 0000000..ed90ed0
--- /dev/null
+++ b/src/components/MailSetting/TimesheetMailDetails.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Card from "@mui/material/Card";
+import CardContent from "@mui/material/CardContent";
+import Grid from "@mui/material/Grid";
+import TextField from "@mui/material/TextField";
+import Typography from "@mui/material/Typography";
+import { useTranslation } from "react-i18next";
+import { Controller, useFormContext } from "react-hook-form";
+import Link from "next/link";
+import React from "react";
+import MailField from "../MailField/MailField";
+import { MailSave } from "@/app/api/mail/actions";
+
+interface Props {
+ isActive: boolean;
+}
+
+const TimesheetMailDetails: React.FC = ({ isActive }) => {
+ const { t } = useTranslation();
+ const {
+ register,
+ formState: { errors },
+ control
+ } = useFormContext();
+
+ return (
+
+
+
+
+ {t("Timesheet Template")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (
+
+ )}
+ rules={{
+ required: true,
+ validate: value => value?.includes("${date}")
+ }}
+
+ />
+
+
+
+
+
+ );
+};
+
+export default TimesheetMailDetails;
diff --git a/src/components/MailSetting/index.ts b/src/components/MailSetting/index.ts
new file mode 100644
index 0000000..fadb1d2
--- /dev/null
+++ b/src/components/MailSetting/index.ts
@@ -0,0 +1 @@
+export { default } from "./MailSettingWrapper";
\ No newline at end of file
diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx
index b8a5056..1f55d44 100644
--- a/src/components/NavigationContent/NavigationContent.tsx
+++ b/src/components/NavigationContent/NavigationContent.tsx
@@ -35,6 +35,8 @@ import ViewWeekIcon from "@mui/icons-material/ViewWeek";
import ManageAccountsIcon from "@mui/icons-material/ManageAccounts";
import EmojiEventsIcon from "@mui/icons-material/EmojiEvents";
import FileUploadIcon from '@mui/icons-material/FileUpload';
+import EmailIcon from "@mui/icons-material/Email";
+
import {
IMPORT_INVOICE,
IMPORT_RECEIPT,
@@ -76,7 +78,9 @@ import {
GENERATE_FINANCIAL_STATUS_REPORT,
GENERATE_PROJECT_CASH_FLOW_REPORT,
GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT,
- GENERATE_CROSS_TEAM_CHARGE_REPORT
+ GENERATE_CROSS_TEAM_CHARGE_REPORT,
+ VIEW_MAIL,
+ MAINTAIN_MAIL
} from "@/middleware";
import { SessionWithAbilities } from "../AppBar/NavigationToggle";
import { authOptions } from "@/config/authConfig";
@@ -394,6 +398,12 @@ const NavigationContent: React.FC = ({ abilities, username }) => {
path: "/settings/holiday",
isHidden: ![VIEW_HOLIDAY, MAINTAIN_HOLIDAY].some((ability) => abilities!.includes(ability),),
},
+ {
+ icon: ,
+ label: "Mail",
+ path: "/settings/mail",
+ isHidden: ![VIEW_MAIL, MAINTAIN_MAIL].some((ability) => abilities!.includes(ability),),
+ },
{ icon: , label: "Import Excel File", path: "/settings/import", isHidden: username !== "2fi" },
],
},
diff --git a/src/middleware.ts b/src/middleware.ts
index c5d081c..50b2db9 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -34,6 +34,7 @@ export const [
VIEW_SALARY,
VIEW_TEAM,
VIEW_HOLIDAY,
+ VIEW_MAIL,
MAINTAIN_CLIENT,
MAINTAIN_SUBSIDIARY,
MAINTAIN_STAFF,
@@ -45,6 +46,7 @@ export const [
MAINTAIN_TEAM,
MAINTAIN_GROUP,
MAINTAIN_HOLIDAY,
+ MAINTAIN_MAIL,
VIEW_DASHBOARD_SELF,
VIEW_DASHBOARD_ALL,
IMPORT_INVOICE,
@@ -84,6 +86,7 @@ export const [
'VIEW_SALARY',
'VIEW_TEAM',
'VIEW_HOLIDAY',
+ 'VIEW_MAIL',
'MAINTAIN_CLIENT',
'MAINTAIN_SUBSIDIARY',
'MAINTAIN_STAFF',
@@ -95,6 +98,7 @@ export const [
'MAINTAIN_TEAM',
'MAINTAIN_GROUP',
'MAINTAIN_HOLIDAY',
+ 'MAINTAIN_MAIL',
'VIEW_DASHBOARD_SELF',
'VIEW_DASHBOARD_ALL',
'IMPORT_INVOICE',
@@ -179,6 +183,7 @@ export default async function middleware(
VIEW_TEAM,
VIEW_GROUP,
VIEW_HOLIDAY,
+ VIEW_MAIL,
MAINTAIN_CLIENT,
MAINTAIN_SUBSIDIARY,
MAINTAIN_STAFF,
@@ -189,7 +194,8 @@ export default async function middleware(
MAINTAIN_SALARY,
MAINTAIN_TEAM,
MAINTAIN_GROUP,
- MAINTAIN_HOLIDAY
+ MAINTAIN_HOLIDAY,
+ MAINTAIN_MAIL
].some((ability) => abilities.includes(ability));
}