| @@ -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", | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| ]; | ]; | ||||