@@ -2,10 +2,11 @@ | |||
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
import { BASE_API_URL } from "@/config/api"; | |||
import { MailSetting } from "."; | |||
import { MailSetting, MailTemplate } from "."; | |||
export interface MailSave { | |||
settings: MailSetting[]; | |||
templates: MailTemplate[]; | |||
// template: MailTemplate; | |||
} | |||
@@ -1,6 +1,7 @@ | |||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
import { BASE_API_URL } from "@/config/api"; | |||
import { cache } from "react"; | |||
import "server-only"; | |||
export interface MailSMTP { | |||
host: string; | |||
@@ -17,6 +18,20 @@ export interface MailSetting { | |||
type: string; | |||
} | |||
export interface MailTemplate { | |||
id: number; | |||
to: string; | |||
cc: string; | |||
code: string; | |||
description: string; | |||
type: string; | |||
params: string; | |||
subjectCht: string; | |||
subjectEng: string; | |||
contentCht: string; | |||
contentEng: string; | |||
} | |||
// export interface MailTemplate { | |||
// cc?: string; | |||
// bcc?: string; | |||
@@ -26,6 +41,7 @@ export interface MailSetting { | |||
export const preloadMails = () => { | |||
fetchMailSetting(); | |||
fetchMailTemplates(); | |||
// fetchMailTimesheetTemplate(); | |||
}; | |||
@@ -35,6 +51,12 @@ export const fetchMailSetting = cache(async () => { | |||
}); | |||
}); | |||
export const fetchMailTemplates = cache(async () => { | |||
return serverFetchJson<MailTemplate[]>(`${BASE_API_URL}/mailTemplates`, { | |||
next: { tags: ["mailTemplates"] }, | |||
}); | |||
}); | |||
// export const fetchMailTimesheetTemplate = cache(async () => { | |||
// return serverFetchJson<MailSetting[]>(`${BASE_API_URL}/mails/timesheet-template`, { | |||
// next: { tags: ["mailTimesheetTemplate"] }, | |||
@@ -21,7 +21,7 @@ | |||
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; | |||
@@ -41,7 +41,7 @@ | |||
display: none; | |||
} | |||
:not(.tiptap-error) > .tiptap:focus { | |||
:not(.tiptap-error)>.tiptap:focus { | |||
background-color: transparent; | |||
border-color: #8dba00; | |||
/* palette.primary.main */ | |||
@@ -53,6 +53,74 @@ | |||
outline: none; | |||
} | |||
/* Basic editor styles */ | |||
.tiptap { | |||
:first-child { | |||
margin-top: 0; | |||
} | |||
/* Table-specific styling */ | |||
table { | |||
border-collapse: collapse; | |||
margin: 0; | |||
overflow: hidden; | |||
table-layout: fixed; | |||
width: 100%; | |||
td, | |||
th { | |||
border: 1px solid #000000; | |||
box-sizing: border-box; | |||
min-width: 1em; | |||
padding: 6px 8px; | |||
position: relative; | |||
vertical-align: top; | |||
>* { | |||
margin-bottom: 0; | |||
} | |||
} | |||
th { | |||
background-color: #939393; | |||
font-weight: bold; | |||
text-align: left; | |||
} | |||
.selectedCell:after { | |||
background: rgba(180, 180, 180, 0.467); | |||
content: ""; | |||
left: 0; | |||
right: 0; | |||
top: 0; | |||
bottom: 0; | |||
pointer-events: none; | |||
position: absolute; | |||
z-index: 2; | |||
} | |||
.column-resize-handle { | |||
background-color: #d000ff; | |||
bottom: -2px; | |||
pointer-events: none; | |||
position: absolute; | |||
right: -2px; | |||
top: 0; | |||
width: 4px; | |||
} | |||
} | |||
.tableWrapper { | |||
margin: 1.5rem 0; | |||
overflow-x: auto; | |||
} | |||
&.resize-cursor { | |||
cursor: ew-resize; | |||
cursor: col-resize; | |||
} | |||
} | |||
/* Input styles */ | |||
/* .tiptap-input { | |||
font-size: 14px; | |||
@@ -14,6 +14,11 @@ 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' | |||
import Table from '@tiptap/extension-table' | |||
import TableCell from '@tiptap/extension-table-cell' | |||
import TableHeader from '@tiptap/extension-table-header' | |||
import TableRow from '@tiptap/extension-table-row' | |||
import Gapcursor from '@tiptap/extension-gapcursor' | |||
interface Props { | |||
content?: string, | |||
@@ -64,7 +69,14 @@ const MailField: React.FC<Props> = ({ | |||
Highlight.configure({ multicolor: true }), | |||
Color, | |||
ListItem, | |||
TabHandler | |||
TabHandler, | |||
Gapcursor, | |||
Table.configure({ | |||
resizable: true, | |||
}), | |||
TableRow, | |||
TableHeader, | |||
TableCell, | |||
], | |||
content: content, | |||
onUpdate({ editor }) { | |||
@@ -73,15 +85,15 @@ const MailField: React.FC<Props> = ({ | |||
} | |||
console.log(editor.getHTML()) | |||
}, | |||
}) | |||
}, []) | |||
return ( | |||
<Grid container rowSpacing={1}> | |||
<Grid item xs={12}> | |||
<MailToolbar editor={editor} /> | |||
</Grid> | |||
{/* <Grid item xs={12}> | |||
{editor && <MailToolbar editor={editor} />} | |||
</Grid> */} | |||
<Grid item xs={12} > | |||
<EditorContent className={error === true ? "tiptap-error" : ""} label="Template" editor={editor}/> | |||
<EditorContent className={error === true ? "tiptap-error" : ""} label="Template" editor={editor} /> | |||
</Grid> | |||
</Grid> | |||
); | |||
@@ -13,6 +13,8 @@ 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'; | |||
import { useTranslation } from "react-i18next"; | |||
import GridOnIcon from '@mui/icons-material/GridOn'; | |||
interface Props { | |||
editor: Editor | null; | |||
@@ -58,7 +60,7 @@ const fontFamily = [ | |||
value: 'Georgia', | |||
}, | |||
{ | |||
} | |||
] | |||
@@ -66,11 +68,12 @@ const MailToolbar: React.FC<Props> = ({ | |||
editor | |||
}) => { | |||
const { t } = useTranslation() | |||
if (editor == null) { | |||
return null | |||
} | |||
const [fontStyle, setFontStyle] = React.useState<string[]>(() => ["alignLeft"]); | |||
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); | |||
@@ -189,6 +192,10 @@ const MailToolbar: React.FC<Props> = ({ | |||
} | |||
}, []) | |||
const handleInsertTable = useCallback(() => { | |||
editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() | |||
}, []) | |||
React.useEffect(() => { | |||
editor.on('selectionUpdate', ({ editor }) => { | |||
const currentFormatList: string[] = [] | |||
@@ -312,6 +319,47 @@ const MailToolbar: React.FC<Props> = ({ | |||
<FormatAlignRightIcon /> | |||
</ToggleButton> | |||
</ToggleButtonGroup> | |||
<ToggleButtonGroup | |||
sx={{ marginTop: 2 }} | |||
> | |||
<ToggleButton id="insertTable" value="insertTable" onClick={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}> | |||
<GridOnIcon />{t("Insert Table")} | |||
</ToggleButton> | |||
<ToggleButton id="deleteTable" value="deleteTable" onClick={() => editor.chain().focus().deleteTable().run()}>{t("Delete table")}</ToggleButton> | |||
<ToggleButton id="addColumnBefore" value="addColumnBefore" onClick={() => editor.chain().focus().addColumnBefore().run()}> | |||
{t("Add column before")} | |||
</ToggleButton> | |||
<ToggleButton id="addColumnAfter" value="addColumnAfter" onClick={() => editor.chain().focus().addColumnAfter().run()}>{t("Add column after")}</ToggleButton> | |||
<ToggleButton id="deleteColumn" value="deleteColumn" onClick={() => editor.chain().focus().deleteColumn().run()}>{t("Delete column")}</ToggleButton> | |||
<ToggleButton id="addRowBefore" value="addRowBefore" onClick={() => editor.chain().focus().addRowBefore().run()}>{t("Add row before")}</ToggleButton> | |||
<ToggleButton id="addRowAfter" value="addRowAfter" onClick={() => editor.chain().focus().addRowAfter().run()}>{t("Add row after")}</ToggleButton> | |||
<ToggleButton id="deleteRow" value="deleteRow" onClick={() => editor.chain().focus().deleteRow().run()}>{t("Delete row")}</ToggleButton> | |||
<ToggleButton id="mergeCells" value="mergeCells" onClick={() => editor.chain().focus().mergeCells().run()}>{t("Merge cells")}</ToggleButton> | |||
<ToggleButton id="splitCell" value="splitCell" onClick={() => editor.chain().focus().splitCell().run()}>{t("Split cell")}</ToggleButton> | |||
</ToggleButtonGroup> | |||
<ToggleButtonGroup | |||
// sx={{ marginLeft: 2 }} | |||
> | |||
<ToggleButton id="toggleHeaderColumn" value="toggleHeaderColumn" onClick={() => editor.chain().focus().toggleHeaderColumn().run()}> | |||
{t("Toggle header column")} | |||
</ToggleButton> | |||
<ToggleButton id="toggleHeaderRow" value="toggleHeaderRow" onClick={() => editor.chain().focus().toggleHeaderRow().run()}> | |||
{t("Toggle header row")} | |||
</ToggleButton> | |||
<ToggleButton id="toggleHeaderCell" value="toggleHeaderCell" onClick={() => editor.chain().focus().toggleHeaderCell().run()}> | |||
{t("Toggle header cell")} | |||
</ToggleButton> | |||
<ToggleButton id="mergeOrSplit" value="mergeOrSplit" onClick={() => editor.chain().focus().mergeOrSplit().run()}>{t("Merge or split")}</ToggleButton> | |||
<ToggleButton id="setCellAttribute" value="setCellAttribute" onClick={() => editor.chain().focus().setCellAttribute('colspan', 2).run()}> | |||
{t("Set cell attribute")} | |||
</ToggleButton> | |||
<ToggleButton id="fixTables" value="fixTables" onClick={() => editor.chain().focus().fixTables().run()}>{t("Fix tables")}</ToggleButton> | |||
<ToggleButton id="goToNextCell" value="goToNextCell" onClick={() => editor.chain().focus().goToNextCell().run()}>{t("Go to next cell")}</ToggleButton> | |||
<ToggleButton id="goToPreviousCell" value="goToPreviousCell" onClick={() => editor.chain().focus().goToPreviousCell().run()}> | |||
{t("Go to previous cell")} | |||
</ToggleButton> | |||
</ToggleButtonGroup> | |||
</Grid> | |||
); | |||
}; | |||
@@ -5,7 +5,7 @@ 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 React, { useCallback, useContext, useEffect, useState } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import { | |||
FieldErrors, | |||
@@ -20,6 +20,10 @@ 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"; | |||
import { MailTemplate } from "@/app/api/mail"; | |||
import TemplateDetails from "./TemplateDetails"; | |||
import QrCodeScanner from "../QrCodeScanner/QrCodeScanner"; | |||
import { QcCodeScannerContext, useQcCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
export interface Props { | |||
defaultInputs?: MailSave, | |||
@@ -34,10 +38,10 @@ const hasErrorsInTab = ( | |||
return ( | |||
errors.settings | |||
); | |||
// case 1: | |||
// return ( | |||
// errors.template | |||
// ); | |||
case 1: | |||
return ( | |||
errors.templates | |||
); | |||
default: | |||
false; | |||
} | |||
@@ -134,6 +138,17 @@ const MailSetting: React.FC<Props> = ({ | |||
const errors = formProps.formState.errors; | |||
const scanner = useQcCodeScanner() | |||
useEffect(() => { | |||
scanner.startScan() | |||
}, []) | |||
console.log("test", scanner.values) | |||
if (scanner.values.length > 3) { | |||
console.log("test", scanner.values) | |||
scanner.resetScan() | |||
scanner.stopScan() | |||
} | |||
return ( | |||
<FormProvider {...formProps}> | |||
<Stack | |||
@@ -161,17 +176,18 @@ const MailSetting: React.FC<Props> = ({ | |||
} | |||
iconPosition="end" | |||
/> | |||
{/* <Tab | |||
label={t("Timesheet Template")} | |||
<Tab | |||
label={t("Template")} | |||
icon={ | |||
hasErrorsInTab(1, errors) ? ( | |||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||
) : undefined | |||
} | |||
iconPosition="end" | |||
/> */} | |||
/> | |||
</Tabs> | |||
<SettingDetails isActive={tabIndex === 0}/> | |||
<TemplateDetails isActive={tabIndex === 1} /> | |||
{/* <TimesheetMailDetails isActive={tabIndex === 1} /> */} | |||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||
@@ -1,7 +1,7 @@ | |||
// import { fetchUserAbilities } from "@/app/utils/fetchUtil"; | |||
import MailSetting from "./MailSetting"; | |||
import MailSettingLoading from "./MailSettingLoading"; | |||
import { fetchMailSetting } from "@/app/api/mail"; | |||
import { fetchMailSetting, fetchMailTemplates } from "@/app/api/mail"; | |||
interface SubComponents { | |||
Loading: typeof MailSettingLoading; | |||
@@ -11,10 +11,12 @@ const MailSettingWrapper: React.FC & SubComponents = async () => { | |||
const [ | |||
// abilities, | |||
settings, | |||
templates, | |||
// timesheetTemplate, | |||
] = await Promise.all([ | |||
// fetchUserAbilities(), | |||
fetchMailSetting(), | |||
fetchMailTemplates(), | |||
// fetchMailTimesheetTemplate() | |||
]); | |||
@@ -28,6 +30,7 @@ const MailSettingWrapper: React.FC & SubComponents = async () => { | |||
<MailSetting | |||
defaultInputs={{ | |||
settings: settings, | |||
templates: templates, | |||
// template: tempTimesheetTemplate, | |||
}} | |||
/> | |||
@@ -55,7 +55,7 @@ const SettingDetails: React.FC<Props> = ({ isActive }) => { | |||
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> | |||
<Typography variant="body2">{`${t(field.name)}${requiredFields.some(name => field.name.toLowerCase().includes(name)) ? "*" : ""}`}</Typography> | |||
</Grid> | |||
{ | |||
field.name.toLowerCase().includes("password") === true ? | |||
@@ -0,0 +1,239 @@ | |||
"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, { SyntheticEvent, useCallback, useMemo, useState } from "react"; | |||
import MailField from "../MailField/MailField"; | |||
import { MailSave } from "@/app/api/mail/actions"; | |||
import { MailTemplate } from "@/app/api/mail"; | |||
import { isEmpty, keys } from "lodash"; | |||
import { Autocomplete, ListSubheader, MenuItem, UseAutocompleteProps } from "@mui/material"; | |||
interface Props { | |||
isActive: boolean; | |||
} | |||
interface OptionType { | |||
value: number, | |||
label: string, | |||
group: string, | |||
} | |||
type fieldKeys = keyof MailTemplate | |||
const TemplateDetails: React.FC<Props> = ({ isActive }) => { | |||
const { t } = useTranslation(); | |||
// const disabledFields: fieldKeys[] = [] | |||
// const requiredFields: fieldKeys[] = [] | |||
// const skipFields: fieldKeys[] = ["code", "subjectEng", "contentEng"] | |||
const [selectedIndex, setSelectedIndex] = useState(0) | |||
const { | |||
register, | |||
formState: { errors }, | |||
control, | |||
watch, | |||
getValues | |||
} = useFormContext<MailSave>(); | |||
const { | |||
fields, | |||
} = useFieldArray({ | |||
control, | |||
name: "templates" | |||
}) | |||
const options: OptionType[] = useMemo(() => ( | |||
fields.map((field, index) => { | |||
return { | |||
value: index, | |||
label: `${field.code} - ${field.description}`, | |||
group: field.type | |||
} as OptionType | |||
}) | |||
), [fields]) | |||
const makeAutocompleteChangeHandler = useCallback(() => { | |||
return (e: SyntheticEvent, newValue: OptionType) => { | |||
setSelectedIndex(() => newValue.value); | |||
}; | |||
}, []); | |||
// const renderFieldsBySwitchCases = useCallback(() => { | |||
// const fieldNames = keys(fields[0]) as fieldKeys[]; | |||
// return fieldNames | |||
// .filter((name) => !skipFields.includes(name)) | |||
// .map((name: fieldKeys, index: number) => { | |||
// switch (name) { | |||
// case "contentCht": | |||
// return ( | |||
// <Grid item xs={12}> | |||
// <Controller | |||
// control={control} | |||
// name={`templates.${selectedIndex}.contentCht`} | |||
// render={({ field }) => ( | |||
// <MailField | |||
// content={field.value} | |||
// onChange={field.onChange} | |||
// error={Boolean(errors.templates?.[selectedIndex]?.contentCht)} | |||
// /> | |||
// )} | |||
// rules={{ | |||
// required: requiredFields.includes(name), | |||
// }} | |||
// /> | |||
// </Grid> | |||
// ) | |||
// default: | |||
// return ( | |||
// <Grid item xs={8}> | |||
// <TextField | |||
// label={t(name)} | |||
// fullWidth | |||
// {...register(`templates.${selectedIndex}.cc`), | |||
// { | |||
// required: requiredFields.includes(name) | |||
// }} | |||
// /> | |||
// </Grid> | |||
// ) | |||
// } | |||
// }) | |||
// }, [fields]) | |||
return ( | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent component={Stack} spacing={4}> | |||
<Box> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Template")} | |||
</Typography> | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs={8}> | |||
<Autocomplete | |||
disableClearable | |||
options={options} | |||
value={options[selectedIndex]} | |||
onChange={makeAutocompleteChangeHandler} | |||
groupBy={(option) => ( | |||
option.group && option.group.trim() !== '' ? option.group : 'Ungrouped' | |||
)} | |||
renderGroup={(params) => ( | |||
<React.Fragment> | |||
<ListSubheader>{params.group}</ListSubheader> | |||
{params.children} | |||
</React.Fragment> | |||
)} | |||
renderOption={( | |||
params: React.HTMLAttributes<HTMLLIElement> & { key?: React.Key }, | |||
option, | |||
{ selected }, | |||
) => { | |||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
const { key, ...rest } = params; | |||
return ( | |||
<MenuItem | |||
{...rest} | |||
disableRipple | |||
value={option.value} | |||
> | |||
{option.label} | |||
</MenuItem> | |||
); | |||
}} | |||
renderInput={(params) => <TextField {...params} variant="outlined" label={t("Select Template (View By Code - Description)")} />} | |||
/> | |||
</Grid> | |||
<Grid item xs={8}> | |||
<TextField | |||
label={t("Code")} | |||
fullWidth | |||
{...register(`templates.${selectedIndex}.code`, { | |||
required: "Code is required!" | |||
})} | |||
required | |||
error={Boolean(errors.templates?.[selectedIndex]?.code)} | |||
/> | |||
</Grid> | |||
<Grid item xs={8}> | |||
<TextField | |||
label={t("Description")} | |||
fullWidth | |||
{...register(`templates.${selectedIndex}.description`, { | |||
required: "Description is required!" | |||
})} | |||
required | |||
error={Boolean(errors.templates?.[selectedIndex]?.description)} | |||
/> | |||
</Grid> | |||
{/* <Grid item xs={8}> | |||
<TextField | |||
label={t("To")} | |||
fullWidth | |||
{...register(`templates.${selectedIndex}.to`, { | |||
required: "To is required!" | |||
})} | |||
required | |||
error={Boolean(errors.templates?.[selectedIndex]?.to)} | |||
/> | |||
</Grid> | |||
<Grid item xs={8}> | |||
<TextField | |||
label={t("Cc")} | |||
fullWidth | |||
{...register(`templates.${selectedIndex}.cc`)} | |||
/> | |||
</Grid> | |||
<Grid item xs={8}> | |||
<TextField | |||
label={t("Required Params (split by ',')")} | |||
fullWidth | |||
{...register(`templates.${selectedIndex}.params`)} | |||
/> | |||
</Grid> */} | |||
<Grid item xs={8}> | |||
<TextField | |||
label={t("Subject CHT")} | |||
fullWidth | |||
{...register(`templates.${selectedIndex}.subjectCht`, | |||
{ | |||
required: "Mail Subject is required!", | |||
})} | |||
required | |||
error={Boolean(errors.templates?.[selectedIndex]?.subjectCht)} | |||
/> | |||
</Grid> | |||
<Grid item xs={12}> | |||
<Controller | |||
control={control} | |||
name={`templates.${selectedIndex}.contentCht`} | |||
render={({ field }) => ( | |||
<MailField | |||
content={field.value} | |||
onChange={field.onChange} | |||
error={Boolean(errors.templates?.[selectedIndex]?.contentCht)} | |||
/> | |||
)} | |||
rules={{ | |||
required: "Mail Content is required!", | |||
}} | |||
/> | |||
</Grid> | |||
</Grid> | |||
</Box> | |||
</CardContent> | |||
</Card> | |||
); | |||
}; | |||
export default TemplateDetails; |