From 66c565cf74d1f987200e325e9c5e62e483012d5c Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 12 Jun 2025 13:05:15 +0800 Subject: [PATCH] add mail & comment some fields --- src/app/api/mail/actions.ts | 3 +- src/app/api/mail/index.ts | 22 ++ src/components/MailField/MailField.css | 72 +++++- src/components/MailField/MailField.tsx | 24 +- src/components/MailField/MailToolbar.tsx | 52 +++- src/components/MailSetting/MailSetting.tsx | 32 ++- .../MailSetting/MailSettingWrapper.tsx | 5 +- src/components/MailSetting/SettingDetails.tsx | 2 +- .../MailSetting/TemplateDetails.tsx | 239 ++++++++++++++++++ 9 files changed, 430 insertions(+), 21 deletions(-) create mode 100644 src/components/MailSetting/TemplateDetails.tsx diff --git a/src/app/api/mail/actions.ts b/src/app/api/mail/actions.ts index eb6a7a1..eb0460c 100644 --- a/src/app/api/mail/actions.ts +++ b/src/app/api/mail/actions.ts @@ -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; } diff --git a/src/app/api/mail/index.ts b/src/app/api/mail/index.ts index 0ab8e7e..97fc42e 100644 --- a/src/app/api/mail/index.ts +++ b/src/app/api/mail/index.ts @@ -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(`${BASE_API_URL}/mailTemplates`, { + next: { tags: ["mailTemplates"] }, + }); +}); + // export const fetchMailTimesheetTemplate = cache(async () => { // return serverFetchJson(`${BASE_API_URL}/mails/timesheet-template`, { // next: { tags: ["mailTimesheetTemplate"] }, diff --git a/src/components/MailField/MailField.css b/src/components/MailField/MailField.css index bfa2515..50237e1 100644 --- a/src/components/MailField/MailField.css +++ b/src/components/MailField/MailField.css @@ -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; diff --git a/src/components/MailField/MailField.tsx b/src/components/MailField/MailField.tsx index 915e3ec..9d80956 100644 --- a/src/components/MailField/MailField.tsx +++ b/src/components/MailField/MailField.tsx @@ -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 = ({ 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 = ({ } console.log(editor.getHTML()) }, - }) + }, []) return ( - - - + {/* + {editor && } + */} - + ); diff --git a/src/components/MailField/MailToolbar.tsx b/src/components/MailField/MailToolbar.tsx index efaadbe..a79951b 100644 --- a/src/components/MailField/MailToolbar.tsx +++ b/src/components/MailField/MailToolbar.tsx @@ -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 = ({ editor }) => { + const { t } = useTranslation() if (editor == null) { return null } - const [fontStyle, setFontStyle] = React.useState(() => ["alignLeft"]); + const [fontStyle, setFontStyle] = React.useState(["alignLeft"]); const [colorHighlightValue, setColorHighlightValue] = React.useState("red"); const [colorTextValue, setColorTextValue] = React.useState("black"); const colorHighlightValueInputRef = React.useRef(null); @@ -189,6 +192,10 @@ const MailToolbar: React.FC = ({ } }, []) + 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 = ({ + + + editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}> + {t("Insert Table")} + + editor.chain().focus().deleteTable().run()}>{t("Delete table")} + editor.chain().focus().addColumnBefore().run()}> + {t("Add column before")} + + editor.chain().focus().addColumnAfter().run()}>{t("Add column after")} + editor.chain().focus().deleteColumn().run()}>{t("Delete column")} + editor.chain().focus().addRowBefore().run()}>{t("Add row before")} + editor.chain().focus().addRowAfter().run()}>{t("Add row after")} + editor.chain().focus().deleteRow().run()}>{t("Delete row")} + editor.chain().focus().mergeCells().run()}>{t("Merge cells")} + editor.chain().focus().splitCell().run()}>{t("Split cell")} + + + editor.chain().focus().toggleHeaderColumn().run()}> + {t("Toggle header column")} + + editor.chain().focus().toggleHeaderRow().run()}> + {t("Toggle header row")} + + editor.chain().focus().toggleHeaderCell().run()}> + {t("Toggle header cell")} + + editor.chain().focus().mergeOrSplit().run()}>{t("Merge or split")} + editor.chain().focus().setCellAttribute('colspan', 2).run()}> + {t("Set cell attribute")} + + editor.chain().focus().fixTables().run()}>{t("Fix tables")} + editor.chain().focus().goToNextCell().run()}>{t("Go to next cell")} + editor.chain().focus().goToPreviousCell().run()}> + {t("Go to previous cell")} + + ); }; diff --git a/src/components/MailSetting/MailSetting.tsx b/src/components/MailSetting/MailSetting.tsx index 9cffb2d..a20fae4 100644 --- a/src/components/MailSetting/MailSetting.tsx +++ b/src/components/MailSetting/MailSetting.tsx @@ -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 = ({ 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 ( = ({ } iconPosition="end" /> - {/* ) : undefined } iconPosition="end" - /> */} + /> + {/* */} diff --git a/src/components/MailSetting/MailSettingWrapper.tsx b/src/components/MailSetting/MailSettingWrapper.tsx index 91ba265..7774b8d 100644 --- a/src/components/MailSetting/MailSettingWrapper.tsx +++ b/src/components/MailSetting/MailSettingWrapper.tsx @@ -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 () => { diff --git a/src/components/MailSetting/SettingDetails.tsx b/src/components/MailSetting/SettingDetails.tsx index 9f81765..b1cca0c 100644 --- a/src/components/MailSetting/SettingDetails.tsx +++ b/src/components/MailSetting/SettingDetails.tsx @@ -55,7 +55,7 @@ const SettingDetails: React.FC = ({ isActive }) => { fields.map((field, index) => ( - {t(field.name)} + {`${t(field.name)}${requiredFields.some(name => field.name.toLowerCase().includes(name)) ? "*" : ""}`} { field.name.toLowerCase().includes("password") === true ? diff --git a/src/components/MailSetting/TemplateDetails.tsx b/src/components/MailSetting/TemplateDetails.tsx new file mode 100644 index 0000000..f878c12 --- /dev/null +++ b/src/components/MailSetting/TemplateDetails.tsx @@ -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 = ({ 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(); + + 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 ( + // + // ( + // + // )} + // rules={{ + // required: requiredFields.includes(name), + // }} + // /> + // + // ) + // default: + // return ( + // + // + // + // ) + // } + // }) + // }, [fields]) + + return ( + + + + + {t("Template")} + + + + ( + option.group && option.group.trim() !== '' ? option.group : 'Ungrouped' + )} + renderGroup={(params) => ( + + {params.group} + {params.children} + + )} + renderOption={( + params: React.HTMLAttributes & { key?: React.Key }, + option, + { selected }, + ) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key, ...rest } = params; + return ( + + {option.label} + + ); + }} + renderInput={(params) => } + /> + + + + + + + + {/* + + + + + + + + */} + + + + + ( + + )} + rules={{ + required: "Mail Content is required!", + }} + /> + + + + + + ); +}; + +export default TemplateDetails;