@@ -19,6 +19,7 @@ | |||||
"@mui/material-nextjs": "^5.15.0", | "@mui/material-nextjs": "^5.15.0", | ||||
"@mui/x-data-grid": "^6.18.7", | "@mui/x-data-grid": "^6.18.7", | ||||
"@mui/x-date-pickers": "^6.18.7", | "@mui/x-date-pickers": "^6.18.7", | ||||
"@tiptap/react": "^2.12.0", | |||||
"@unly/universal-language-detector": "^2.0.3", | "@unly/universal-language-detector": "^2.0.3", | ||||
"apexcharts": "^3.45.2", | "apexcharts": "^3.45.2", | ||||
"axios": "^1.9.0", | "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", | label: "QC Check Template", | ||||
path: "/settings/user", | path: "/settings/user", | ||||
}, | }, | ||||
{ | |||||
icon: <RequestQuote />, | |||||
label: "Mail", | |||||
path: "/settings/mail", | |||||
}, | |||||
], | ], | ||||
}, | }, | ||||
]; | ]; | ||||