Explorar el Código

Add mail master

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui hace 1 año
padre
commit
ef6d3b33d7
Se han modificado 19 ficheros con 1141 adiciones y 10 borrados
  1. +11
    -0
      package.json
  2. +41
    -0
      src/app/(main)/settings/mail/page.tsx
  3. +26
    -0
      src/app/api/mail/actions.ts
  4. +42
    -0
      src/app/api/mail/index.ts
  5. +1
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  6. +8
    -8
      src/components/CreateProject/ProjectClientDetails.tsx
  7. +61
    -0
      src/components/MailField/MailField.css
  8. +90
    -0
      src/components/MailField/MailField.tsx
  9. +21
    -0
      src/components/MailField/MailFieldWrapper.tsx
  10. +319
    -0
      src/components/MailField/MailToolbar.tsx
  11. +1
    -0
      src/components/MailField/index.ts
  12. +183
    -0
      src/components/MailSetting/MailSetting.tsx
  13. +38
    -0
      src/components/MailSetting/MailSettingLoading.tsx
  14. +40
    -0
      src/components/MailSetting/MailSettingWrapper.tsx
  15. +143
    -0
      src/components/MailSetting/SettingDetails.tsx
  16. +97
    -0
      src/components/MailSetting/TimesheetMailDetails.tsx
  17. +1
    -0
      src/components/MailSetting/index.ts
  18. +11
    -1
      src/components/NavigationContent/NavigationContent.tsx
  19. +7
    -1
      src/middleware.ts

+ 11
- 0
package.json Ver fichero

@@ -21,6 +21,15 @@
"@mui/material-nextjs": "^5.15.0",
"@mui/x-data-grid": "^6.18.7",
"@mui/x-date-pickers": "^6.18.7",
"@tiptap/extension-color": "^2.5.8",
"@tiptap/extension-highlight": "^2.5.8",
"@tiptap/extension-list-item": "^2.5.9",
"@tiptap/extension-text-align": "^2.5.9",
"@tiptap/extension-text-style": "^2.5.8",
"@tiptap/extension-underline": "^2.5.8",
"@tiptap/pm": "^2.5.8",
"@tiptap/react": "^2.5.8",
"@tiptap/starter-kit": "^2.5.8",
"@unly/universal-language-detector": "^2.0.3",
"apexcharts": "^3.45.2",
"date-holidays": "^3.23.11",
@@ -29,8 +38,10 @@
"i18next": "^23.7.11",
"i18next-resources-to-backend": "^1.2.0",
"lodash": "^4.17.21",
"mui-color-input": "^3.0.0",
"next": "14.0.4",
"next-auth": "^4.24.7",
"next-intl": "^3.13.0",
"next-pwa": "^5.6.0",
"react": "^18",
"react-apexcharts": "^1.4.1",


+ 41
- 0
src/app/(main)/settings/mail/page.tsx Ver fichero

@@ -0,0 +1,41 @@
import { getServerI18n } from "@/i18n";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import { Suspense } from "react";
import { I18nProvider } from "@/i18n";
import { getUserAbilities } from "@/app/utils/commonUtil";
import { preloadMails } from "@/app/api/mail";
import MailSetting from "@/components/MailSetting";

export const metadata: Metadata = {
title: "Mail",
};

const Customer: React.FC = async () => {
const { t } = await getServerI18n("mail");
preloadMails();
const abilities = await getUserAbilities()

return (
<>
<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;

+ 26
- 0
src/app/api/mail/actions.ts Ver fichero

@@ -0,0 +1,26 @@
"use server";

import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { MailSetting, MailTemplate } from ".";

export interface MailSave {
settings: MailSetting[];
template: MailTemplate;
}

export const saveMail = async (data: MailSave) => {
return serverFetchJson<MailSetting[]>(`${BASE_API_URL}/mails/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

export const testMail = async () => {
return serverFetchWithNoContent(`${BASE_API_URL}/mails/test`, {
method: "GET",
// body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

+ 42
- 0
src/app/api/mail/index.ts Ver fichero

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

+ 1
- 0
src/components/Breadcrumb/Breadcrumb.tsx Ver fichero

@@ -63,6 +63,7 @@ const pathToLabelMap: { [path: string]: string } = {
"/settings/group/edit": "Edit User Group",
"/settings/group/create": "Create User Group",
"/settings/holiday": "Company Holiday",
"/settings/mail": "Mail",
"/analytics": "Analysis Report",
"/analytics/LateStartReport": "Late Start Report",
"/analytics/ProjectPotentialDelayReport": "Project Potential Delay Report",


+ 8
- 8
src/components/CreateProject/ProjectClientDetails.tsx Ver fichero

@@ -153,21 +153,21 @@ const ProjectClientDetails: React.FC<Props> = ({
selectedCustomerId === defaultValues?.clientId &&
Boolean(defaultValues?.clientSubsidiaryId)
? contacts.find(
(contact) => contact.id === defaultValues.clientContactId,
)?.id ?? contacts[0].id
: contacts[0].id,
(contact) => contact?.id === defaultValues?.clientContactId,
)?.id ?? contacts[0]?.id
: contacts[0]?.id,
);
setValue("isSubsidiaryContact", true);
} else if (customerContacts?.length > 0) {
setSubsidiaryContacts(() => []);
setValue(
"clientContactId",
selectedCustomerId === defaultValues?.clientId &&
!Boolean(defaultValues?.clientSubsidiaryId)
"clientContactId",
selectedCustomerId === defaultValues?.clientId &&
!Boolean(defaultValues?.clientSubsidiaryId)
? customerContacts.find(
(contact) => contact.id === defaultValues.clientContactId,
)?.id ?? customerContacts[0].id
: customerContacts[0].id,
)?.id ?? customerContacts[0].id
: customerContacts[0].id
);
setValue("isSubsidiaryContact", false);
}


+ 61
- 0
src/components/MailField/MailField.css Ver fichero

@@ -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 Ver fichero

@@ -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 Ver fichero

@@ -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 Ver fichero

@@ -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 Ver fichero

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

+ 183
- 0
src/components/MailSetting/MailSetting.tsx Ver fichero

@@ -0,0 +1,183 @@
"use client";

import Check from "@mui/icons-material/Check";
import Close from "@mui/icons-material/Close";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import { useRouter } from "next/navigation";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
} from "react-hook-form";
import { Tab, Tabs, TabsProps, Typography } from "@mui/material";
import TimesheetMailDetails from "./TimesheetMailDetails";
import { Error } from "@mui/icons-material";
import { MailSave, saveMail, testMail } from "@/app/api/mail/actions";
import SettingDetails from "./SettingDetails";
import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts";

export interface Props {
defaultInputs?: MailSave,
}

const hasErrorsInTab = (
tabIndex: number,
errors: FieldErrors<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 testMail()
}

successDialog(t("Save Success"), t)
} else {
errorDialog(t("Save Fail"), t).then(() => {
// formProps.setError("code", { message: response.message, type: "custom" })
// setTabIndex(0)
return false
})
}
}, t)
} catch (e) {
console.log(e)
setServerError(t("An error has occurred. Please try again later."));
}
},
[router, t, test],
);

const onSubmitError = useCallback<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("Save And Test")}
</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 Ver fichero

@@ -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 Ver fichero

@@ -0,0 +1,40 @@
import { getUserAbilities } from "@/app/utils/commonUtil";
import MailSetting from "./MailSetting";
import MailSettingLoading from "./MailSettingLoading";
import { MailTemplate, fetchMailSetting, fetchMailTimesheetTemplate } from "@/app/api/mail";

interface SubComponents {
Loading: typeof MailSettingLoading;
}

const MailSettingWrapper: React.FC & SubComponents = async () => {
const [
abilities,
settings,
timesheetTemplate,
] = await Promise.all([
getUserAbilities(),
fetchMailSetting(),
fetchMailTimesheetTemplate()
]);

const tempTimesheetTemplate: MailTemplate = {
cc: timesheetTemplate.find(template => template.name.includes(".cc"))?.value,
bcc: timesheetTemplate.find(template => template.name.includes(".bcc"))?.value,
subject: timesheetTemplate.find(template => template.name.includes(".subject"))?.value,
template: timesheetTemplate.find(template => template.name.includes(".template"))?.value,
}
return (
<MailSetting
defaultInputs={{
settings: settings,
template: tempTimesheetTemplate,
}}
/>
);
};

MailSettingWrapper.Loading = MailSettingLoading;


export default MailSettingWrapper;

+ 143
- 0
src/components/MailSetting/SettingDetails.tsx Ver fichero

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

+ 97
- 0
src/components/MailSetting/TimesheetMailDetails.tsx Ver fichero

@@ -0,0 +1,97 @@
"use client";

import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import Link from "next/link";
import React from "react";
import MailField from "../MailField/MailField";
import { MailSave } from "@/app/api/mail/actions";

interface Props {
isActive: boolean;
}

const TimesheetMailDetails: React.FC<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 Ver fichero

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

+ 11
- 1
src/components/NavigationContent/NavigationContent.tsx Ver fichero

@@ -35,6 +35,8 @@ import ViewWeekIcon from "@mui/icons-material/ViewWeek";
import ManageAccountsIcon from "@mui/icons-material/ManageAccounts";
import EmojiEventsIcon from "@mui/icons-material/EmojiEvents";
import FileUploadIcon from '@mui/icons-material/FileUpload';
import EmailIcon from "@mui/icons-material/Email";

import {
IMPORT_INVOICE,
IMPORT_RECEIPT,
@@ -76,7 +78,9 @@ import {
GENERATE_FINANCIAL_STATUS_REPORT,
GENERATE_PROJECT_CASH_FLOW_REPORT,
GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT,
GENERATE_CROSS_TEAM_CHARGE_REPORT
GENERATE_CROSS_TEAM_CHARGE_REPORT,
VIEW_MAIL,
MAINTAIN_MAIL
} from "@/middleware";
import { SessionWithAbilities } from "../AppBar/NavigationToggle";
import { authOptions } from "@/config/authConfig";
@@ -394,6 +398,12 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => {
path: "/settings/holiday",
isHidden: ![VIEW_HOLIDAY, MAINTAIN_HOLIDAY].some((ability) => abilities!.includes(ability),),
},
{
icon: <EmailIcon />,
label: "Mail",
path: "/settings/mail",
isHidden: ![VIEW_MAIL, MAINTAIN_MAIL].some((ability) => abilities!.includes(ability),),
},
{ icon: <FileUploadIcon />, label: "Import Excel File", path: "/settings/import", isHidden: username !== "2fi" },
],
},


+ 7
- 1
src/middleware.ts Ver fichero

@@ -34,6 +34,7 @@ export const [
VIEW_SALARY,
VIEW_TEAM,
VIEW_HOLIDAY,
VIEW_MAIL,
MAINTAIN_CLIENT,
MAINTAIN_SUBSIDIARY,
MAINTAIN_STAFF,
@@ -45,6 +46,7 @@ export const [
MAINTAIN_TEAM,
MAINTAIN_GROUP,
MAINTAIN_HOLIDAY,
MAINTAIN_MAIL,
VIEW_DASHBOARD_SELF,
VIEW_DASHBOARD_ALL,
IMPORT_INVOICE,
@@ -84,6 +86,7 @@ export const [
'VIEW_SALARY',
'VIEW_TEAM',
'VIEW_HOLIDAY',
'VIEW_MAIL',
'MAINTAIN_CLIENT',
'MAINTAIN_SUBSIDIARY',
'MAINTAIN_STAFF',
@@ -95,6 +98,7 @@ export const [
'MAINTAIN_TEAM',
'MAINTAIN_GROUP',
'MAINTAIN_HOLIDAY',
'MAINTAIN_MAIL',
'VIEW_DASHBOARD_SELF',
'VIEW_DASHBOARD_ALL',
'IMPORT_INVOICE',
@@ -179,6 +183,7 @@ export default async function middleware(
VIEW_TEAM,
VIEW_GROUP,
VIEW_HOLIDAY,
VIEW_MAIL,
MAINTAIN_CLIENT,
MAINTAIN_SUBSIDIARY,
MAINTAIN_STAFF,
@@ -189,7 +194,8 @@ export default async function middleware(
MAINTAIN_SALARY,
MAINTAIN_TEAM,
MAINTAIN_GROUP,
MAINTAIN_HOLIDAY
MAINTAIN_HOLIDAY,
MAINTAIN_MAIL
].some((ability) => abilities.includes(ability));
}



Cargando…
Cancelar
Guardar