Sfoglia il codice sorgente

[Mail] Copied. Now can send + set. Mail template need to update.

create_edit_user
cyril.tsui 2 mesi fa
parent
commit
6e44d9ea2e
16 ha cambiato i file con 1146 aggiunte e 0 eliminazioni
  1. +1
    -0
      package.json
  2. +41
    -0
      src/app/(main)/settings/mail/page.tsx
  3. +50
    -0
      src/app/api/mail/actions.ts
  4. +42
    -0
      src/app/api/mail/index.ts
  5. +61
    -0
      src/components/MailField/MailField.css
  6. +90
    -0
      src/components/MailField/MailField.tsx
  7. +21
    -0
      src/components/MailField/MailFieldWrapper.tsx
  8. +319
    -0
      src/components/MailField/MailToolbar.tsx
  9. +1
    -0
      src/components/MailField/index.ts
  10. +197
    -0
      src/components/MailSetting/MailSetting.tsx
  11. +38
    -0
      src/components/MailSetting/MailSettingLoading.tsx
  12. +40
    -0
      src/components/MailSetting/MailSettingWrapper.tsx
  13. +143
    -0
      src/components/MailSetting/SettingDetails.tsx
  14. +96
    -0
      src/components/MailSetting/TimesheetMailDetails.tsx
  15. +1
    -0
      src/components/MailSetting/index.ts
  16. +5
    -0
      src/components/NavigationContent/NavigationContent.tsx

+ 1
- 0
package.json Vedi File

@@ -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",


+ 41
- 0
src/app/(main)/settings/mail/page.tsx Vedi File

@@ -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;

+ 50
- 0
src/app/api/mail/actions.ts Vedi File

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

+ 42
- 0
src/app/api/mail/index.ts Vedi File

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

+ 61
- 0
src/components/MailField/MailField.css Vedi File

@@ -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;
} */

+ 90
- 0
src/components/MailField/MailField.tsx Vedi File

@@ -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;

+ 21
- 0
src/components/MailField/MailFieldWrapper.tsx Vedi File

@@ -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;

+ 319
- 0
src/components/MailField/MailToolbar.tsx Vedi File

@@ -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;

+ 1
- 0
src/components/MailField/index.ts Vedi File

@@ -0,0 +1 @@
export { default } from "./MailFieldWrapper";

+ 197
- 0
src/components/MailSetting/MailSetting.tsx Vedi File

@@ -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;

+ 38
- 0
src/components/MailSetting/MailSettingLoading.tsx Vedi File

@@ -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;

+ 40
- 0
src/components/MailSetting/MailSettingWrapper.tsx Vedi File

@@ -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;

+ 143
- 0
src/components/MailSetting/SettingDetails.tsx Vedi File

@@ -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;

+ 96
- 0
src/components/MailSetting/TimesheetMailDetails.tsx Vedi File

@@ -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;

+ 1
- 0
src/components/MailSetting/index.ts Vedi File

@@ -0,0 +1 @@
export { default } from "./MailSettingWrapper";

+ 5
- 0
src/components/NavigationContent/NavigationContent.tsx Vedi File

@@ -258,6 +258,11 @@ const NavigationContent: React.FC = () => {
label: "QC Check Template",
path: "/settings/user",
},
{
icon: <RequestQuote />,
label: "Mail",
path: "/settings/mail",
},
],
},
];


Caricamento…
Annulla
Salva