작성자 | SHA1 | 메시지 | 날짜 |
---|---|---|---|
|
c2e110fa8b | Add payment summary break down | 1 년 전 |
|
41919bb25b | Adjust color and login page | 1 년 전 |
@@ -0,0 +1,73 @@ | |||
import { useMemo } from "react"; | |||
import StyledDataGrid from "../StyledDataGrid"; | |||
import { GridColDef, GridValidRowModel } from "@mui/x-data-grid"; | |||
import { useTranslation } from "react-i18next"; | |||
import dayjs from "dayjs"; | |||
import { moneyFormatter, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { TaskGroup } from "@/app/api/tasks"; | |||
interface Props { | |||
taskGroups: TaskGroup[]; | |||
} | |||
interface Row extends GridValidRowModel { | |||
taskStage: string; | |||
description: string; | |||
date: string; | |||
amount: number; | |||
} | |||
const PaymentSummary: React.FC<Props> = ({ taskGroups }) => { | |||
const { t } = useTranslation(); | |||
const { watch } = useFormContext<CreateProjectInputs>(); | |||
const milestones = watch("milestones"); | |||
const rows = useMemo<Row[]>(() => { | |||
return taskGroups.flatMap((group) => { | |||
const payments = milestones[group.id]?.payments || []; | |||
return payments.map(({ description, date, amount, id }) => ({ | |||
id, | |||
taskStage: group.name, | |||
description, | |||
date, | |||
amount, | |||
})); | |||
}); | |||
}, [milestones, taskGroups]); | |||
const columns = useMemo<GridColDef<GridValidRowModel>[]>( | |||
() => [ | |||
{ field: "taskStage", headerName: t("Task Stage"), width: 300 }, | |||
{ | |||
field: "description", | |||
headerName: t("Payment Milestone Description"), | |||
width: 300, | |||
}, | |||
{ | |||
field: "date", | |||
headerName: t("Payment Milestone Date"), | |||
width: 200, | |||
type: "date", | |||
valueFormatter(params) { | |||
return dayjs(params.value).format(OUTPUT_DATE_FORMAT); | |||
}, | |||
}, | |||
{ | |||
field: "amount", | |||
headerName: t("Payment Milestone Amount"), | |||
width: 300, | |||
type: "number", | |||
valueFormatter(params) { | |||
return moneyFormatter.format(params.value); | |||
}, | |||
}, | |||
], | |||
[t], | |||
); | |||
return <StyledDataGrid columns={columns} rows={rows} />; | |||
}; | |||
export default PaymentSummary; |
@@ -1,21 +1,51 @@ | |||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
import { TaskGroup } from "@/app/api/tasks"; | |||
import { moneyFormatter } from "@/app/utils/formatUtil"; | |||
import { Divider, Stack, Typography } from "@mui/material"; | |||
import React from "react"; | |||
import { | |||
Button, | |||
Divider, | |||
Modal, | |||
Paper, | |||
Stack, | |||
SxProps, | |||
Typography, | |||
} from "@mui/material"; | |||
import React, { useCallback, useState } from "react"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { useTranslation } from "react-i18next"; | |||
import PaymentSummary from "./PaymentSummary"; | |||
interface Props { | |||
taskGroups: TaskGroup[]; | |||
} | |||
const modalSx: SxProps = { | |||
position: "absolute", | |||
top: "50%", | |||
left: "50%", | |||
transform: "translate(-50%, -50%)", | |||
width: "90%", | |||
maxHeight: "90%", | |||
padding: 3, | |||
display: "flex", | |||
flexDirection: "column", | |||
gap: 2, | |||
}; | |||
const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => { | |||
const { t } = useTranslation(); | |||
const { watch } = useFormContext<CreateProjectInputs>(); | |||
const milestones = watch("milestones"); | |||
const expectedTotalFee = watch("expectedProjectFee"); | |||
const [paymentSummaryOpen, setPaymentSummaryOpen] = useState(false); | |||
const closePaymentSummary = useCallback(() => { | |||
setPaymentSummaryOpen(false); | |||
}, []); | |||
const openPaymentSummary = useCallback(() => { | |||
setPaymentSummaryOpen(true); | |||
}, []); | |||
let projectTotal = 0; | |||
return ( | |||
@@ -37,9 +67,24 @@ const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => { | |||
</Stack> | |||
); | |||
})} | |||
<Button | |||
variant="outlined" | |||
size="small" | |||
sx={{ alignSelf: "flex-start" }} | |||
onClick={openPaymentSummary} | |||
> | |||
{t("View payment breakdown")} | |||
</Button> | |||
<Modal open={paymentSummaryOpen} onClose={closePaymentSummary}> | |||
<Paper sx={modalSx}> | |||
<PaymentSummary taskGroups={taskGroups} /> | |||
</Paper> | |||
</Modal> | |||
<Divider sx={{ paddingBlockStart: 2 }} /> | |||
<Stack direction="row" justifyContent="space-between"> | |||
<Typography variant="h6">{t("Sum of Payment Milestone Fee")}</Typography> | |||
<Typography variant="h6"> | |||
{t("Sum of Payment Milestone Fee")} | |||
</Typography> | |||
<Typography>{moneyFormatter.format(projectTotal)}</Typography> | |||
</Stack> | |||
<Divider sx={{ paddingBlockStart: 2 }} /> | |||
@@ -49,7 +94,9 @@ const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => { | |||
</Stack> | |||
{projectTotal !== expectedTotalFee && ( | |||
<Typography variant="caption" color="warning.main" alignSelf="flex-end"> | |||
{t("Sum of Payment Milestone Fee should be same as the expected total fee!")} | |||
{t( | |||
"Sum of Payment Milestone Fee should be same as the expected total fee!", | |||
)} | |||
</Typography> | |||
)} | |||
</Stack> | |||
@@ -1,6 +1,6 @@ | |||
"use client"; | |||
import { FormHelperText } from "@mui/material"; | |||
import { FormHelperText, SxProps } from "@mui/material"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import TextField from "@mui/material/TextField"; | |||
@@ -31,7 +31,7 @@ const getHumanFriendlyErrorMessage = ( | |||
} | |||
}; | |||
const LoginForm: React.FC = () => { | |||
const LoginForm: React.FC<{ sx?: SxProps }> = ({ sx }) => { | |||
const { t } = useTranslation("login"); | |||
const { | |||
register, | |||
@@ -65,6 +65,7 @@ const LoginForm: React.FC = () => { | |||
margin={5} | |||
component="form" | |||
onSubmit={handleSubmit(onSubmit)} | |||
sx={sx} | |||
> | |||
<Typography variant="h1">{t("Sign In")}</Typography> | |||
<TextField | |||
@@ -7,14 +7,44 @@ import { Box } from "@mui/material"; | |||
const LoginPage = () => { | |||
return ( | |||
<Grid container height="100vh"> | |||
<Grid item sm sx={{ backgroundColor: 'neutral.000', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> | |||
<Box sx={{ width: '100%', padding: 5, paddingBlockStart: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', svg: { maxHeight: 120 } }}> | |||
<Grid | |||
item | |||
xs={12} | |||
md={6} | |||
sx={{ | |||
backgroundColor: "neutral.000", | |||
display: "flex", | |||
alignItems: "center", | |||
justifyContent: "center", | |||
flexGrow: 1, | |||
}} | |||
> | |||
<Box | |||
sx={{ | |||
width: "100%", | |||
padding: 5, | |||
paddingBlockStart: 10, | |||
display: "flex", | |||
alignItems: "center", | |||
justifyContent: "center", | |||
svg: { maxHeight: 120 }, | |||
}} | |||
> | |||
<Logo /> | |||
</Box> | |||
</Grid> | |||
<Grid item xs={12} sm={8} lg={5} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}> | |||
<Paper square sx={{ width: '100%', padding: 5 }}> | |||
<LoginForm /> | |||
<Grid item xs={12} md={6} sx={{ height: { md: "100%" }, flexGrow: 1 }}> | |||
<Paper | |||
square | |||
sx={{ | |||
height: "100%", | |||
width: "100%", | |||
padding: { lg: 5, md: 3 }, | |||
display: "flex", | |||
alignItems: "center", | |||
}} | |||
> | |||
<LoginForm sx={{ flex: 1 }} /> | |||
</Paper> | |||
</Grid> | |||
</Grid> | |||
@@ -11,7 +11,25 @@ export const neutral = { | |||
900: "#111927", | |||
}; | |||
// export const primary = { | |||
// lightest: "#F5F7FF", | |||
// light: "#EBEEFE", | |||
// main: "#6366F1", | |||
// dark: "#4338CA", | |||
// darkest: "#312E81", | |||
// contrastText: "#FFFFFF", | |||
// }; | |||
export const primary = { | |||
lightest: "#f9fff5", | |||
light: "#f9feeb", | |||
main: "#8dba00", | |||
dark: "#638a01", | |||
darkest: "#4a5f14", | |||
contrastText: "#FFFFFF", | |||
}; | |||
export const secondary = { | |||
lightest: "#F5F7FF", | |||
light: "#EBEEFE", | |||
main: "#6366F1", | |||
@@ -1,6 +1,14 @@ | |||
import { common } from "@mui/material/colors"; | |||
import { PaletteOptions } from "@mui/material/styles"; | |||
import { error, primary, info, neutral, success, warning } from "./colors"; | |||
import { | |||
error, | |||
primary, | |||
secondary, | |||
info, | |||
neutral, | |||
success, | |||
warning, | |||
} from "./colors"; | |||
const palette = { | |||
action: { | |||
@@ -20,6 +28,7 @@ const palette = { | |||
info, | |||
mode: "light", | |||
primary, | |||
secondary, | |||
success, | |||
text: { | |||
primary: neutral[900], | |||