@@ -19,6 +19,7 @@ | |||
"@mui/material-nextjs": "^5.15.0", | |||
"@mui/x-data-grid": "^6.18.7", | |||
"@mui/x-date-pickers": "^6.18.7", | |||
"@tiptap/react": "^2.12.0", | |||
"@unly/universal-language-detector": "^2.0.3", | |||
"apexcharts": "^3.45.2", | |||
"axios": "^1.9.0", | |||
@@ -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 { fetchUserAbilities } from "@/app/utils/fetchUtil"; | |||
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 fetchUserAbilities() | |||
return ( | |||
<> | |||
<Stack | |||
direction="row" | |||
justifyContent="space-between" | |||
flexWrap="wrap" | |||
rowGap={2} | |||
> | |||
<Typography variant="h4" marginInlineEnd={2}> | |||
{t("Mail")} | |||
</Typography> | |||
</Stack> | |||
<I18nProvider namespaces={["mail", "common"]}> | |||
<Suspense fallback={<MailSetting.Loading />}> | |||
<MailSetting /> | |||
</Suspense> | |||
</I18nProvider> | |||
</> | |||
); | |||
}; | |||
export default Customer; |
@@ -0,0 +1,50 @@ | |||
"use server"; | |||
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
import { BASE_API_URL } from "@/config/api"; | |||
import { MailSetting } from "."; | |||
export interface MailSave { | |||
settings: MailSetting[]; | |||
// template: MailTemplate; | |||
} | |||
export const saveMail = async (data: MailSave) => { | |||
return serverFetchJson<MailSetting[]>(`${BASE_API_URL}/mails/save`, { | |||
method: "POST", | |||
body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
}); | |||
}; | |||
export const testSendMail = async () => { | |||
return serverFetchWithNoContent(`${BASE_API_URL}/mails/test-send`, { | |||
method: "GET", | |||
// body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
}); | |||
}; | |||
export const testEveryone = async () => { | |||
return serverFetchWithNoContent(`${BASE_API_URL}/mails/testEveryone`, { | |||
method: "GET", | |||
// body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
}); | |||
}; | |||
export const test7th = async () => { | |||
return serverFetchWithNoContent(`${BASE_API_URL}/mails/test7th`, { | |||
method: "GET", | |||
// body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
}); | |||
}; | |||
export const test15th = async () => { | |||
return serverFetchWithNoContent(`${BASE_API_URL}/mails/test15th`, { | |||
method: "GET", | |||
// body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
}); | |||
}; |
@@ -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<MailSetting[]>(`${BASE_API_URL}/mails/setting`, { | |||
next: { tags: ["mailSetting"] }, | |||
}); | |||
}); | |||
// export const fetchMailTimesheetTemplate = cache(async () => { | |||
// return serverFetchJson<MailSetting[]>(`${BASE_API_URL}/mails/timesheet-template`, { | |||
// next: { tags: ["mailTimesheetTemplate"] }, | |||
// }); | |||
// }); |
@@ -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; | |||
} */ |
@@ -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<Props> = ({ | |||
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 ( | |||
<Grid container rowSpacing={1}> | |||
<Grid item xs={12}> | |||
<MailToolbar editor={editor} /> | |||
</Grid> | |||
<Grid item xs={12} > | |||
<EditorContent className={error === true ? "tiptap-error" : ""} label="Template" editor={editor}/> | |||
</Grid> | |||
</Grid> | |||
); | |||
}; | |||
export default MailField; |
@@ -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<Props> = ({ | |||
content, | |||
onChange, | |||
error | |||
}) => { | |||
return <MailField content={content} onChange={onChange} error={error}/>; | |||
}; | |||
export default TransferListWrapper; |
@@ -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<Props> = ({ | |||
editor | |||
}) => { | |||
if (editor == null) { | |||
return null | |||
} | |||
const [fontStyle, setFontStyle] = React.useState<string[]>(() => ["alignLeft"]); | |||
const [colorHighlightValue, setColorHighlightValue] = React.useState<MuiColorInputValue>("red"); | |||
const [colorTextValue, setColorTextValue] = React.useState<MuiColorInputValue>("black"); | |||
const colorHighlightValueInputRef = React.useRef<any>(null); | |||
const colorTextValueInputRef = React.useRef<any>(null); | |||
const handleFontStyle = useCallback(( | |||
event: React.MouseEvent<HTMLElement>, | |||
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<HTMLElement>) => { | |||
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<HTMLInputElement | HTMLTextAreaElement>) => { | |||
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<HTMLElement>) => { | |||
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<HTMLInputElement | HTMLTextAreaElement>) => { | |||
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<HTMLDivElement>) => { | |||
if (event.key === 'Enter') { | |||
if (colorHighlightValueInputRef.current !== null) { | |||
colorHighlightValueInputRef.current.blur(); | |||
} | |||
if (colorTextValueInputRef.current !== null) { | |||
colorTextValueInputRef.current.blur(); | |||
} | |||
} | |||
} | |||
const handleTextAlign = useCallback((event: React.MouseEvent<HTMLElement, 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 ( | |||
<Grid container> | |||
<ToggleButtonGroup | |||
value={fontStyle} | |||
onChange={handleFontStyle} | |||
> | |||
<ToggleButton id="bold" value="bold" onClick={() => editor.chain().focus().toggleBold().run()}> | |||
<FormatBoldIcon /> | |||
</ToggleButton> | |||
<ToggleButton id="italic" value="italic" onClick={() => editor.chain().focus().toggleItalic().run()}> | |||
<FormatItalic /> | |||
</ToggleButton> | |||
<ToggleButton id="underline" value="underline" onClick={() => editor.chain().focus().toggleUnderline().run()}> | |||
<FormatUnderlined /> | |||
</ToggleButton> | |||
</ToggleButtonGroup> | |||
<ToggleButtonGroup | |||
value={fontStyle} | |||
onChange={handleFontStyle} | |||
sx={{ marginLeft: 2 }} | |||
> | |||
<ToggleButton id="highlight" value="highlight" onClick={handleColorHighlightValueClick}> | |||
<BorderColorIcon sx={{ color: colorHighlightValue as string }} /> | |||
</ToggleButton> | |||
{/* <ToggleButton value="" onClick={() => console.log("Expand more")}> | |||
<ExpandMoreIcon sx={{ width: 15 }} fontSize="large"/> | |||
</ToggleButton> */} | |||
<ToggleButton id="highlight" value="highlight"> | |||
<MuiColorInput | |||
sx={colorInputSx} | |||
format="hex8" | |||
value={colorHighlightValue} | |||
onBlur={handleColorHighlightValueBlur} | |||
onChange={handleColorHighlightValue} | |||
onKeyDown={handleKeyDown} | |||
inputRef={colorHighlightValueInputRef} | |||
PopoverProps={{ | |||
onClose: handleColorHighlightValueClose | |||
}} | |||
/> | |||
</ToggleButton> | |||
</ToggleButtonGroup> | |||
<ToggleButtonGroup | |||
value={fontStyle} | |||
onChange={handleFontStyle} | |||
sx={{ marginLeft: 2 }} | |||
> | |||
<ToggleButton id="textStyle" value="textStyle" onClick={handleColorTextValueClick}> | |||
<FormatColorTextIcon sx={{ color: colorTextValue as string }} /> | |||
</ToggleButton> | |||
<ToggleButton id="textStyle" value="textStyle"> | |||
<MuiColorInput | |||
sx={colorInputSx} | |||
format="hex8" | |||
value={colorTextValue} | |||
onBlur={handleColorTextValueBlur} | |||
onChange={handleColorTextValue} | |||
onKeyDown={handleKeyDown} | |||
inputRef={colorTextValueInputRef} | |||
PopoverProps={{ | |||
onClose: handleColorTextValueClose | |||
}} | |||
/> | |||
</ToggleButton> | |||
</ToggleButtonGroup> | |||
<ToggleButtonGroup | |||
value={fontStyle} | |||
onChange={handleFontStyle} | |||
sx={{ marginLeft: 2 }} | |||
> | |||
<ToggleButton id="alignLeft" value="alignLeft" onClick={handleTextAlign}> | |||
<FormatAlignLeftIcon /> | |||
</ToggleButton> | |||
<ToggleButton id="alignCenter" value="alignCenter" onClick={handleTextAlign}> | |||
<FormatAlignJustifyIcon /> | |||
</ToggleButton> | |||
<ToggleButton id="alignRight" value="alignRight" onClick={handleTextAlign}> | |||
<FormatAlignRightIcon /> | |||
</ToggleButton> | |||
</ToggleButtonGroup> | |||
</Grid> | |||
); | |||
}; | |||
export default MailToolbar; |
@@ -0,0 +1 @@ | |||
export { default } from "./MailFieldWrapper"; |
@@ -0,0 +1,197 @@ | |||
"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, testEveryone, test7th, test15th, testSendMail } 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<MailSave>, | |||
) => { | |||
switch (tabIndex) { | |||
case 0: | |||
return ( | |||
errors.settings | |||
); | |||
// case 1: | |||
// return ( | |||
// errors.template | |||
// ); | |||
default: | |||
false; | |||
} | |||
}; | |||
const MailSetting: React.FC<Props> = ({ | |||
defaultInputs, | |||
}) => { | |||
const [serverError, setServerError] = useState(""); | |||
const { t } = useTranslation(); | |||
const router = useRouter(); | |||
const [tabIndex, setTabIndex] = useState(0); | |||
const [test, setTest] = useState(false) | |||
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
(_e, newValue) => { | |||
setTabIndex(newValue); | |||
}, | |||
[], | |||
); | |||
const formProps = useForm<MailSave>({ | |||
defaultValues: defaultInputs | |||
}); | |||
const handleCancel = () => { | |||
router.back(); | |||
}; | |||
const onSubmit = useCallback<SubmitHandler<MailSave>>( | |||
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 testSendMail() | |||
} | |||
// if (test) { | |||
// let msg = "" | |||
// try { | |||
// msg = "testEveryone" | |||
// await testEveryone() | |||
// msg = "test7th" | |||
// await test7th() | |||
// msg = "test15th" | |||
// await test15th() | |||
// } catch (error) { | |||
// console.log(error) | |||
// console.log(msg) | |||
// } | |||
// } | |||
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<SubmitErrorHandler<MailSave>>( | |||
(errors) => { | |||
console.log(errors) | |||
}, | |||
[], | |||
); | |||
const errors = formProps.formState.errors; | |||
return ( | |||
<FormProvider {...formProps}> | |||
<Stack | |||
spacing={2} | |||
component="form" | |||
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
> | |||
{serverError && ( | |||
<Typography variant="body2" color="error" alignSelf="flex-end"> | |||
{serverError} | |||
</Typography> | |||
)} | |||
<Tabs | |||
value={tabIndex} | |||
onChange={handleTabChange} | |||
variant="scrollable" | |||
> | |||
<Tab | |||
label={t("Setting")} | |||
sx={{ marginInlineEnd: hasErrorsInTab(1, errors) && !hasErrorsInTab(1, errors) ? 1 : undefined }} | |||
icon={ | |||
hasErrorsInTab(0, errors) ? ( | |||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||
) : undefined | |||
} | |||
iconPosition="end" | |||
/> | |||
{/* <Tab | |||
label={t("Timesheet Template")} | |||
icon={ | |||
hasErrorsInTab(1, errors) ? ( | |||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||
) : undefined | |||
} | |||
iconPosition="end" | |||
/> */} | |||
</Tabs> | |||
<SettingDetails isActive={tabIndex === 0}/> | |||
{/* <TimesheetMailDetails isActive={tabIndex === 1} /> */} | |||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||
<Button | |||
variant="outlined" | |||
startIcon={<Close />} | |||
onClick={handleCancel} | |||
> | |||
{t("Cancel")} | |||
</Button> | |||
<Button variant="contained" startIcon={<Check />} onClick={() => setTest(true)} type="submit"> | |||
{t("send to everyone")} | |||
</Button> | |||
<Button variant="contained" startIcon={<Check />} onClick={() => setTest(false)} type="submit"> | |||
{t("Save")} | |||
</Button> | |||
</Stack> | |||
</Stack> | |||
</FormProvider> | |||
); | |||
}; | |||
export default MailSetting; |
@@ -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 ( | |||
<> | |||
<Card> | |||
<CardContent> | |||
<Stack spacing={2}> | |||
<Skeleton variant="rounded" height={60} /> | |||
<Skeleton | |||
variant="rounded" | |||
height={50} | |||
width={100} | |||
sx={{ alignSelf: "flex-end" }} | |||
/> | |||
</Stack> | |||
</CardContent> | |||
</Card> | |||
<Card> | |||
<CardContent> | |||
<Stack spacing={2}> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
</Stack> | |||
</CardContent> | |||
</Card> | |||
</> | |||
); | |||
}; | |||
export default MailSettingLoading; |
@@ -0,0 +1,40 @@ | |||
// import { fetchUserAbilities } from "@/app/utils/fetchUtil"; | |||
import MailSetting from "./MailSetting"; | |||
import MailSettingLoading from "./MailSettingLoading"; | |||
import { fetchMailSetting } from "@/app/api/mail"; | |||
interface SubComponents { | |||
Loading: typeof MailSettingLoading; | |||
} | |||
const MailSettingWrapper: React.FC & SubComponents = async () => { | |||
const [ | |||
// abilities, | |||
settings, | |||
// timesheetTemplate, | |||
] = await Promise.all([ | |||
// fetchUserAbilities(), | |||
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 ( | |||
<MailSetting | |||
defaultInputs={{ | |||
settings: settings, | |||
// template: tempTimesheetTemplate, | |||
}} | |||
/> | |||
); | |||
}; | |||
MailSettingWrapper.Loading = MailSettingLoading; | |||
export default MailSettingWrapper; |
@@ -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<Props> = ({ isActive }) => { | |||
const requiredFields = ["host", "port", "username"] | |||
const { t } = useTranslation(); | |||
const { | |||
register, | |||
formState: { errors }, | |||
control, | |||
watch | |||
} = useFormContext<MailSave>(); | |||
const { fields } = useFieldArray({ | |||
control, | |||
name: "settings" | |||
}) | |||
const [showSMTPPassword, setShowSMTPPassword] = React.useState(false) | |||
const handleClickShowPassword = () => setShowSMTPPassword((show) => !show); | |||
const handleMouseDownPassword = (event: React.MouseEvent<HTMLButtonElement>) => { | |||
event.preventDefault(); | |||
}; | |||
return ( | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent component={Stack} spacing={4}> | |||
<Box> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Settings")} | |||
</Typography> | |||
{ | |||
fields.map((field, index) => ( | |||
<Grid container key={"row-" + index} justifyContent="flex-start" alignItems="center" spacing={2} columns={{ xs: 6, sm: 12 }} sx={{ mt: 1 }}> | |||
<Grid item key={"col-1-" + index} xs={4}> | |||
<Typography variant="body2">{t(field.name)}</Typography> | |||
</Grid> | |||
{ | |||
field.name.toLowerCase().includes("password") === true ? | |||
<Grid item key={"col-2-" + index} xs={8}> | |||
<TextField | |||
label={t(field.name)} | |||
type={showSMTPPassword ? "text" : "password"} | |||
InputProps={{ | |||
endAdornment: ( | |||
<InputAdornment position="end"> | |||
<IconButton | |||
aria-label="toggle password visibility" | |||
onClick={handleClickShowPassword} | |||
onMouseDown={handleMouseDownPassword} | |||
> | |||
{showSMTPPassword ? <Visibility /> : <VisibilityOff />} | |||
</IconButton> | |||
</InputAdornment> | |||
), | |||
}} | |||
fullWidth | |||
{...register(`settings.${index}.value`)} | |||
/> | |||
</Grid> | |||
: | |||
field.type.toLowerCase() === "boolean" ? | |||
<Grid item xs={8}> | |||
<Checkbox | |||
{...register(`settings.${index}.value`)} | |||
checked={Boolean(watch(`settings.${index}.value`))} | |||
/> | |||
</Grid> | |||
: | |||
field.type.toLowerCase() === "integer" ? | |||
<Grid item xs={8}> | |||
<TextField | |||
label={t(field.name)} | |||
type="number" | |||
InputProps={{ | |||
inputProps: { | |||
step: 1, | |||
min: 0, | |||
} | |||
}} | |||
fullWidth | |||
{...register(`settings.${index}.value`, | |||
{ | |||
required: requiredFields.some(name => field.name.toLowerCase().includes(name)) | |||
})} | |||
error={Boolean( | |||
errors.settings | |||
&& errors.settings.length | |||
&& errors.settings.length > index | |||
&& errors.settings[index]?.value | |||
)} | |||
/> | |||
</Grid> | |||
: | |||
<Grid item xs={8}> | |||
<TextField | |||
label={t(field.name)} | |||
fullWidth | |||
{...register(`settings.${index}.value`, | |||
{ | |||
required: requiredFields.some(name => field.name.toLowerCase().includes(name)) | |||
})} | |||
error={Boolean( | |||
errors.settings | |||
&& errors.settings.length | |||
&& errors.settings.length > index | |||
&& errors.settings[index]?.value | |||
)} | |||
/> | |||
</Grid> | |||
} | |||
</Grid> | |||
)) | |||
} | |||
</Box> | |||
</CardContent> | |||
</Card > | |||
); | |||
}; | |||
export default SettingDetails; |
@@ -0,0 +1,96 @@ | |||
"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<Props> = ({ isActive }) => { | |||
const { t } = useTranslation(); | |||
const { | |||
register, | |||
formState: { errors }, | |||
control | |||
} = useFormContext<MailSave>(); | |||
return ( | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent component={Stack} spacing={4}> | |||
<Box> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Timesheet Template")} | |||
</Typography> | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs={8}> | |||
<TextField | |||
label={t("Cc")} | |||
fullWidth | |||
{...register("template.cc")} | |||
/> | |||
</Grid> | |||
<Grid item xs={8}> | |||
<TextField | |||
label={t("Bcc")} | |||
fullWidth | |||
{...register("template.bcc")} | |||
/> | |||
</Grid> | |||
<Grid item xs={8}> | |||
<TextField | |||
label={t("Subject")} | |||
fullWidth | |||
{...register("template.subject", | |||
{ | |||
required: "Subject required!" | |||
} | |||
)} | |||
error={Boolean(errors.template?.subject)} | |||
/> | |||
</Grid> | |||
<Grid item xs={8}> | |||
<TextField | |||
label={t("Required Params")} | |||
fullWidth | |||
value={"${date}"} | |||
// disabled | |||
// error={Boolean(errors.template?.template)} | |||
/> | |||
</Grid> | |||
<Grid item xs={12}> | |||
<Controller | |||
control={control} | |||
name="template.template" | |||
render={({ field }) => ( | |||
<MailField | |||
content={field.value} | |||
onChange={field.onChange} | |||
error={Boolean(errors.template?.template)} | |||
/> | |||
)} | |||
rules={{ | |||
required: true, | |||
validate: value => value?.includes("${date}") | |||
}} | |||
/> | |||
</Grid> | |||
</Grid> | |||
</Box> | |||
</CardContent> | |||
</Card> | |||
); | |||
}; | |||
export default TimesheetMailDetails; |
@@ -0,0 +1 @@ | |||
export { default } from "./MailSettingWrapper"; |
@@ -258,6 +258,11 @@ const NavigationContent: React.FC = () => { | |||
label: "QC Check Template", | |||
path: "/settings/user", | |||
}, | |||
{ | |||
icon: <RequestQuote />, | |||
label: "Mail", | |||
path: "/settings/mail", | |||
}, | |||
], | |||
}, | |||
]; | |||