Bläddra i källkod

add mail & comment some fields

production_process
cyril.tsui 2 månader sedan
förälder
incheckning
66c565cf74
9 ändrade filer med 430 tillägg och 21 borttagningar
  1. +2
    -1
      src/app/api/mail/actions.ts
  2. +22
    -0
      src/app/api/mail/index.ts
  3. +70
    -2
      src/components/MailField/MailField.css
  4. +18
    -6
      src/components/MailField/MailField.tsx
  5. +50
    -2
      src/components/MailField/MailToolbar.tsx
  6. +24
    -8
      src/components/MailSetting/MailSetting.tsx
  7. +4
    -1
      src/components/MailSetting/MailSettingWrapper.tsx
  8. +1
    -1
      src/components/MailSetting/SettingDetails.tsx
  9. +239
    -0
      src/components/MailSetting/TemplateDetails.tsx

+ 2
- 1
src/app/api/mail/actions.ts Visa fil

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



+ 22
- 0
src/app/api/mail/index.ts Visa fil

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


+ 70
- 2
src/components/MailField/MailField.css Visa fil

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


+ 18
- 6
src/components/MailField/MailField.tsx Visa fil

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


+ 50
- 2
src/components/MailField/MailToolbar.tsx Visa fil

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


+ 24
- 8
src/components/MailSetting/MailSetting.tsx Visa fil

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


+ 4
- 1
src/components/MailSetting/MailSettingWrapper.tsx Visa fil

@@ -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,
}}
/>


+ 1
- 1
src/components/MailSetting/SettingDetails.tsx Visa fil

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


+ 239
- 0
src/components/MailSetting/TemplateDetails.tsx Visa fil

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

Laddar…
Avbryt
Spara