# Conflicts: # src/components/CreateProject/CreateProjectWrapper.tsxtags/Baseline_180220205_Frontend
| @@ -0,0 +1,17 @@ | |||||
| import { getServerI18n } from "@/i18n"; | |||||
| import { Stack, Typography, Link } from "@mui/material"; | |||||
| import NextLink from "next/link"; | |||||
| export default async function NotFound() { | |||||
| const { t } = await getServerI18n("projects", "common"); | |||||
| return ( | |||||
| <Stack spacing={2}> | |||||
| <Typography variant="h4">{t("Not Found")}</Typography> | |||||
| <Typography variant="body1">{t("The project was not found!")}</Typography> | |||||
| <Link href="/projects" component={NextLink} variant="body2"> | |||||
| {t("Return to all projects")} | |||||
| </Link> | |||||
| </Stack> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,77 @@ | |||||
| import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | |||||
| import { fetchGrades } from "@/app/api/grades"; | |||||
| import { | |||||
| fetchProjectBuildingTypes, | |||||
| fetchProjectCategories, | |||||
| fetchProjectContractTypes, | |||||
| fetchProjectDetails, | |||||
| fetchProjectFundingTypes, | |||||
| fetchProjectLocationTypes, | |||||
| fetchProjectServiceTypes, | |||||
| fetchProjectWorkNatures, | |||||
| } from "@/app/api/projects"; | |||||
| import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | |||||
| import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | |||||
| import { fetchUserAbilities } from "@/app/utils/fetchUtil"; | |||||
| import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||||
| import CreateProject from "@/components/CreateProject"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import { MAINTAIN_PROJECT } from "@/middleware"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { isArray } from "lodash"; | |||||
| import { Metadata } from "next"; | |||||
| import { notFound } from "next/navigation"; | |||||
| interface Props { | |||||
| searchParams: { [key: string]: string | string[] | undefined }; | |||||
| } | |||||
| export const metadata: Metadata = { | |||||
| title: "Copy Project", | |||||
| }; | |||||
| const Projects: React.FC<Props> = async ({ searchParams }) => { | |||||
| const { t } = await getServerI18n("projects"); | |||||
| // Assume projectId is string here | |||||
| const projectId = searchParams["id"]; | |||||
| const abilities = await fetchUserAbilities() | |||||
| if (!projectId || isArray(projectId) || ![MAINTAIN_PROJECT].some(ability => abilities.includes(ability))) { | |||||
| notFound(); | |||||
| } | |||||
| // Preload necessary dependencies | |||||
| fetchAllTasks(); | |||||
| fetchTaskTemplates(); | |||||
| fetchProjectCategories(); | |||||
| fetchProjectContractTypes(); | |||||
| fetchProjectFundingTypes(); | |||||
| fetchProjectLocationTypes(); | |||||
| fetchProjectServiceTypes(); | |||||
| fetchProjectBuildingTypes(); | |||||
| fetchProjectWorkNatures(); | |||||
| fetchAllCustomers(); | |||||
| fetchAllSubsidiaries(); | |||||
| fetchGrades(); | |||||
| preloadTeamLeads(); | |||||
| preloadStaff(); | |||||
| try { | |||||
| console.log(projectId) | |||||
| await fetchProjectDetails(projectId); | |||||
| } catch (e) { | |||||
| if (e instanceof ServerFetchError && e.response?.status === 404) { | |||||
| notFound(); | |||||
| } | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| <I18nProvider namespaces={["projects"]}> | |||||
| <CreateProject isCopyMode projectId={projectId} /> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default Projects; | |||||
| @@ -0,0 +1,17 @@ | |||||
| import { getServerI18n } from "@/i18n"; | |||||
| import { Stack, Typography, Link } from "@mui/material"; | |||||
| import NextLink from "next/link"; | |||||
| export default async function NotFound() { | |||||
| const { t } = await getServerI18n("projects", "common"); | |||||
| return ( | |||||
| <Stack spacing={2}> | |||||
| <Typography variant="h4">{t("Not Found")}</Typography> | |||||
| <Typography variant="body1">{t("The sub project was not found!")}</Typography> | |||||
| <Link href="/projects" component={NextLink} variant="body2"> | |||||
| {t("Return to all projects")} | |||||
| </Link> | |||||
| </Stack> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,79 @@ | |||||
| import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer"; | |||||
| import { fetchGrades } from "@/app/api/grades"; | |||||
| import { | |||||
| fetchMainProjects, | |||||
| fetchProjectBuildingTypes, | |||||
| fetchProjectCategories, | |||||
| fetchProjectContractTypes, | |||||
| fetchProjectDetails, | |||||
| fetchProjectFundingTypes, | |||||
| fetchProjectLocationTypes, | |||||
| fetchProjectServiceTypes, | |||||
| fetchProjectWorkNatures, | |||||
| } from "@/app/api/projects"; | |||||
| import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | |||||
| import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; | |||||
| import { fetchUserAbilities } from "@/app/utils/fetchUtil"; | |||||
| import CreateProject from "@/components/CreateProject"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import { MAINTAIN_PROJECT } from "@/middleware"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { isArray } from "lodash"; | |||||
| import { Metadata } from "next"; | |||||
| import { notFound } from "next/navigation"; | |||||
| interface Props { | |||||
| searchParams: { [key: string]: string | string[] | undefined }; | |||||
| } | |||||
| export const metadata: Metadata = { | |||||
| title: "Edit Sub Project", | |||||
| }; | |||||
| const Projects: React.FC<Props> = async ({ searchParams }) => { | |||||
| const { t } = await getServerI18n("projects"); | |||||
| const projectId = searchParams["id"]; | |||||
| const abilities = await fetchUserAbilities() | |||||
| if (!projectId || isArray(projectId) || !abilities.includes(MAINTAIN_PROJECT)) { | |||||
| notFound(); | |||||
| } | |||||
| // Preload necessary dependencies | |||||
| fetchAllTasks(); | |||||
| fetchTaskTemplates(); | |||||
| fetchProjectCategories(); | |||||
| fetchProjectContractTypes(); | |||||
| fetchProjectFundingTypes(); | |||||
| fetchProjectLocationTypes(); | |||||
| fetchProjectServiceTypes(); | |||||
| fetchProjectBuildingTypes(); | |||||
| fetchProjectWorkNatures(); | |||||
| fetchAllCustomers(); | |||||
| fetchAllSubsidiaries(); | |||||
| fetchGrades(); | |||||
| preloadTeamLeads(); | |||||
| preloadStaff(); | |||||
| try { | |||||
| await fetchProjectDetails(projectId); | |||||
| const data = await fetchMainProjects(); | |||||
| if (!Boolean(data) || data.length === 0) { | |||||
| notFound(); | |||||
| } | |||||
| } catch (e) { | |||||
| notFound(); | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4">{t("Edit Sub Project")}</Typography> | |||||
| <I18nProvider namespaces={["projects"]}> | |||||
| <CreateProject isCopyMode isSubProject projectId={projectId}/> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default Projects; | |||||
| @@ -6,7 +6,8 @@ import { Dayjs } from "dayjs"; | |||||
| import { cache } from "react"; | import { cache } from "react"; | ||||
| import { FileResponse } from "../reports/actions"; | import { FileResponse } from "../reports/actions"; | ||||
| import { revalidateTag } from "next/cache"; | import { revalidateTag } from "next/cache"; | ||||
| import { SumOfByClient } from "@/components/ProjectFinancialSummaryV2/gptFn"; | |||||
| import { FinancialByProject } from "."; | |||||
| export interface FinancialSummaryByClientResult { | export interface FinancialSummaryByClientResult { | ||||
| teamId:number; | teamId:number; | ||||
| @@ -144,6 +145,32 @@ export const exportFinancialSummaryByProjectExcel = cache(async (data: ExportFin | |||||
| return reportBlob | return reportBlob | ||||
| }) | }) | ||||
| export const exportFinancialSummaryV2ByClientExcel = cache(async (data: SumOfByClient[]) => { | |||||
| const reportBlob = await serverFetchBlob<FileResponse>( | |||||
| `${BASE_API_URL}/dashboard/exportFinancialSummaryV2ByClientExcel`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| return reportBlob | |||||
| }) | |||||
| export const exportFinancialSummaryV2ByProjectExcel = cache(async (data: FinancialByProject[]) => { | |||||
| const reportBlob = await serverFetchBlob<FileResponse>( | |||||
| `${BASE_API_URL}/dashboard/exportFinancialSummaryV2ByProjectExcel`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| return reportBlob | |||||
| }) | |||||
| export const revalidate = async(tag: string) => { | export const revalidate = async(tag: string) => { | ||||
| revalidateTag(tag) | revalidateTag(tag) | ||||
| } | } | ||||
| @@ -33,6 +33,7 @@ export type FinancialByProject = { | |||||
| totalBudget: number, | totalBudget: number, | ||||
| manhourExpense: number, | manhourExpense: number, | ||||
| invoicedAmount: number, | invoicedAmount: number, | ||||
| uninvoicedAmount: number, | |||||
| paidAmount: number, | paidAmount: number, | ||||
| projectExpense: number, | projectExpense: number, | ||||
| } | } | ||||
| @@ -64,6 +64,7 @@ import { deleteDraft, loadDraft, saveToLocalStorage } from "@/app/utils/draftUti | |||||
| export interface Props { | export interface Props { | ||||
| isEditMode: boolean; | isEditMode: boolean; | ||||
| isCopyMode: boolean; | |||||
| draftId?: number; | draftId?: number; | ||||
| isSubProject: boolean; | isSubProject: boolean; | ||||
| mainProjects?: MainProject[]; | mainProjects?: MainProject[]; | ||||
| @@ -116,6 +117,7 @@ const hasErrorsInTab = ( | |||||
| const CreateProject: React.FC<Props> = ({ | const CreateProject: React.FC<Props> = ({ | ||||
| isEditMode, | isEditMode, | ||||
| isCopyMode, | |||||
| draftId, | draftId, | ||||
| isSubProject, | isSubProject, | ||||
| mainProjects, | mainProjects, | ||||
| @@ -547,7 +549,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
| } | } | ||||
| }, [totalManhour]); | }, [totalManhour]); | ||||
| const loading = isEditMode ? !Boolean(projectName) : false; | |||||
| const loading = isEditMode || isCopyMode ? !Boolean(projectName) : false; | |||||
| const submitDisabled = | const submitDisabled = | ||||
| loading || | loading || | ||||
| @@ -21,17 +21,26 @@ import { fetchGrades } from "@/app/api/grades"; | |||||
| import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | ||||
| type CreateProjectProps = { | type CreateProjectProps = { | ||||
| isEditMode: false; | |||||
| isEditMode?: false; | |||||
| isCopyMode?: false; | |||||
| isSubProject?: boolean; | isSubProject?: boolean; | ||||
| draftId?: number; | draftId?: number; | ||||
| }; | }; | ||||
| interface EditProjectProps { | interface EditProjectProps { | ||||
| isEditMode: true; | isEditMode: true; | ||||
| isCopyMode?: false; | |||||
| projectId?: string; | projectId?: string; | ||||
| isSubProject?: boolean; | isSubProject?: boolean; | ||||
| } | } | ||||
| type Props = CreateProjectProps | EditProjectProps; | |||||
| interface CopyProjectProps { | |||||
| isEditMode?: false; | |||||
| isCopyMode: true; | |||||
| projectId?: string; | |||||
| isSubProject?: boolean; | |||||
| } | |||||
| type Props = CreateProjectProps | EditProjectProps | CopyProjectProps; | |||||
| const CreateProjectWrapper: React.FC<Props> = async (props) => { | const CreateProjectWrapper: React.FC<Props> = async (props) => { | ||||
| const [ | const [ | ||||
| @@ -79,7 +88,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
| (teamLead) => teamLead.teamId === teamId || teamLead.team == "ST", | (teamLead) => teamLead.teamId === teamId || teamLead.team == "ST", | ||||
| ) | ) | ||||
| } | } | ||||
| const projectInfo = props.isEditMode | |||||
| const projectInfo = props.isEditMode || props.isCopyMode | |||||
| ? await fetchProjectDetails(props.projectId!) | ? await fetchProjectDetails(props.projectId!) | ||||
| : undefined; | : undefined; | ||||
| @@ -87,10 +96,25 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||||
| ? await fetchMainProjects() | ? await fetchMainProjects() | ||||
| : undefined; | : undefined; | ||||
| if (props.isCopyMode && projectInfo) { | |||||
| projectInfo.projectId = null | |||||
| projectInfo.projectCode = projectInfo.projectCode + "-copy" | |||||
| projectInfo.projectName = projectInfo.projectName + "-copy" | |||||
| projectInfo.projectStatus = "" | |||||
| Object.entries(projectInfo.milestones).forEach(([key, value]) => { | |||||
| projectInfo.milestones[Number(key)].payments.forEach(({ ...rest}, idx, orig) => { | |||||
| orig[idx] = { ...rest, id: rest.id * -1 } | |||||
| }) | |||||
| // console.log(projectInfo.milestones[Number(key)].payments) | |||||
| }) | |||||
| } | |||||
| return ( | return ( | ||||
| <CreateProject | <CreateProject | ||||
| isEditMode={props.isEditMode} | |||||
| draftId={props.isEditMode ? undefined : props.draftId} | |||||
| isEditMode={Boolean(props.isEditMode)} | |||||
| isCopyMode={Boolean(props.isCopyMode)} | |||||
| draftId={props.isEditMode || props.isCopyMode ? undefined : props.draftId} | |||||
| isSubProject={Boolean(props.isSubProject)} | isSubProject={Boolean(props.isSubProject)} | ||||
| defaultInputs={projectInfo} | defaultInputs={projectInfo} | ||||
| allTasks={tasks} | allTasks={tasks} | ||||
| @@ -345,7 +345,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
| // Boolean(errors.projectPlanStart) | // Boolean(errors.projectPlanStart) | ||||
| // || | // || | ||||
| new Date(planStart) > new Date(planEnd) | new Date(planStart) > new Date(planEnd) | ||||
| || !Boolean(planStart) | |||||
| || Boolean(errors.projectPlanStart) | |||||
| , | , | ||||
| }, | }, | ||||
| }} | }} | ||||
| @@ -373,7 +373,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
| // Boolean(errors.projectPlanEnd) | // Boolean(errors.projectPlanEnd) | ||||
| // || | // || | ||||
| new Date(planStart) > new Date(planEnd) | new Date(planStart) > new Date(planEnd) | ||||
| || !Boolean(planEnd) | |||||
| || Boolean(errors.projectPlanEnd) | |||||
| , | , | ||||
| }, | }, | ||||
| }} | }} | ||||
| @@ -110,10 +110,19 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||||
| path: "/home", | path: "/home", | ||||
| showOnMobile: true, | showOnMobile: true, | ||||
| }, | }, | ||||
| // { | |||||
| // icon: <SummarizeIcon />, | |||||
| // label: "Financial Summary", | |||||
| // path: "/dashboard/ProjectFinancialSummary", | |||||
| // isHidden: ![VIEW_DASHBOARD_ALL, VIEW_DASHBOARD_SELF].some((ability) => | |||||
| // abilities!.includes(ability), | |||||
| // ), | |||||
| // showOnMobile: false, | |||||
| // }, | |||||
| { | { | ||||
| icon: <SummarizeIcon />, | icon: <SummarizeIcon />, | ||||
| label: "Financial Summary", | label: "Financial Summary", | ||||
| path: "/dashboard/ProjectFinancialSummary", | |||||
| path: "/dashboard/ProjectFinancialSummaryV2", | |||||
| isHidden: ![VIEW_DASHBOARD_ALL, VIEW_DASHBOARD_SELF].some((ability) => | isHidden: ![VIEW_DASHBOARD_ALL, VIEW_DASHBOARD_SELF].some((ability) => | ||||
| abilities!.includes(ability), | abilities!.includes(ability), | ||||
| ), | ), | ||||
| @@ -172,16 +181,6 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| // { | |||||
| // icon: <SummarizeIcon />, | |||||
| // label: "Financial Summary", | |||||
| // path: "/dashboard/ProjectFinancialSummaryV2", | |||||
| // isHidden: ![VIEW_DASHBOARD_ALL, VIEW_DASHBOARD_SELF].some((ability) => | |||||
| // abilities!.includes(ability), | |||||
| // ), | |||||
| // showOnMobile: true, | |||||
| // }, | |||||
| // No Claim function in Breaur, will be implement later | // No Claim function in Breaur, will be implement later | ||||
| // { | // { | ||||
| @@ -14,36 +14,48 @@ interface Props { | |||||
| financialSummByProject: FinancialByProject[] | financialSummByProject: FinancialByProject[] | ||||
| } | } | ||||
| type InputDate = { | |||||
| startDate: string; | |||||
| endDate: string; | |||||
| } | |||||
| type InputDate = { | |||||
| startDate: string; | |||||
| endDate: string; | |||||
| } | |||||
| type DateParams = { | |||||
| [key: number]: InputDate; | |||||
| } | |||||
| type DateParams = { | |||||
| 0: InputDate; | |||||
| 2: InputDate; | |||||
| 3: InputDate; | |||||
| 4: InputDate; | |||||
| 5: InputDate; | |||||
| } | |||||
| const FinancialSummaryPage: React.FC<Props> = ({ | const FinancialSummaryPage: React.FC<Props> = ({ | ||||
| _teamId, | _teamId, | ||||
| financialSummByProject | financialSummByProject | ||||
| }) => { | }) => { | ||||
| console.log(financialSummByProject) | console.log(financialSummByProject) | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const curr = useMemo(() => dayjs().format(INPUT_DATE_FORMAT), []) | |||||
| const currYear = useMemo(() => dayjs().get("year"), []) | |||||
| var currDate: string = useMemo(() => dayjs().format(INPUT_DATE_FORMAT), []) | |||||
| var currYear: number = useMemo(() => dayjs().get("year"), []) | |||||
| // testing date | |||||
| // currDate = "2024-10-28" | |||||
| // currYear = 2024 | |||||
| const startDate = "10-01" | const startDate = "10-01" | ||||
| const endDate = "09-30" | const endDate = "09-30" | ||||
| const currFinancialYear = useMemo(() => curr > `${currYear}-${startDate}` ? currYear + 1 : currYear, [currYear]) | |||||
| const lengthOfCombo = 6 | |||||
| const currFinancialYear = useMemo(() => currDate > `${currYear}-${startDate}` ? currYear + 1 : currYear, [currYear]) | |||||
| const [mainData, setMainData] = useState<FinancialByProject[]>(financialSummByProject) | const [mainData, setMainData] = useState<FinancialByProject[]>(financialSummByProject) | ||||
| const [byTeam, setByTeam] = useState<SumOfByTeam[]>(() => sumUpByTeam(mainData)) // do fetch to set | const [byTeam, setByTeam] = useState<SumOfByTeam[]>(() => sumUpByTeam(mainData)) // do fetch to set | ||||
| const [byProject, setByProject] = useState<FinancialByProject[]>(financialSummByProject) | const [byProject, setByProject] = useState<FinancialByProject[]>(financialSummByProject) | ||||
| const [byClient, setByClient] = useState<SumOfByClient[]>(() => sumUpByClient(mainData)) | const [byClient, setByClient] = useState<SumOfByClient[]>(() => sumUpByClient(mainData)) | ||||
| const [isLoading, setIsLoading] = useState(false) | const [isLoading, setIsLoading] = useState(false) | ||||
| const allTeam = useMemo(()=> { | |||||
| var _allTeam: SumOfByTeam = { | |||||
| const allTeam = useMemo(() => { | |||||
| return byTeam.reduce((acc, curr) => ({ | |||||
| id: 0, | |||||
| team: "All Team", | |||||
| totalFee: acc.totalFee + curr.totalFee, | |||||
| totalBudget: acc.totalBudget + curr.totalBudget, | |||||
| manhourExpense: acc.manhourExpense + curr.manhourExpense, | |||||
| projectExpense: acc.projectExpense + curr.projectExpense, | |||||
| invoicedAmount: acc.invoicedAmount + curr.invoicedAmount, | |||||
| uninvoicedAmount: acc.uninvoicedAmount + curr.uninvoicedAmount, | |||||
| paidAmount: acc.paidAmount + curr.paidAmount, | |||||
| activeProject: acc.activeProject + curr.activeProject | |||||
| }), { | |||||
| id: 0, | id: 0, | ||||
| team: "All Team", | team: "All Team", | ||||
| totalFee: 0, | totalFee: 0, | ||||
| @@ -51,34 +63,60 @@ const FinancialSummaryPage: React.FC<Props> = ({ | |||||
| manhourExpense: 0, | manhourExpense: 0, | ||||
| projectExpense: 0, | projectExpense: 0, | ||||
| invoicedAmount: 0, | invoicedAmount: 0, | ||||
| uninvoicedAmount: 0, | |||||
| paidAmount: 0, | paidAmount: 0, | ||||
| activeProject: 0, | |||||
| } | |||||
| for (let i = 0; i < byTeam.length; i++) { | |||||
| var curr = byTeam[i] | |||||
| _allTeam["totalFee"] += curr.totalFee | |||||
| _allTeam["totalBudget"] += curr.totalBudget | |||||
| _allTeam["manhourExpense"] += curr.manhourExpense | |||||
| _allTeam["projectExpense"] += curr.projectExpense | |||||
| _allTeam["invoicedAmount"] += curr.invoicedAmount | |||||
| _allTeam["paidAmount"] += curr.paidAmount | |||||
| _allTeam["activeProject"] += curr.activeProject | |||||
| } | |||||
| return _allTeam | |||||
| activeProject: 0 | |||||
| }) | |||||
| }, [mainData]) | }, [mainData]) | ||||
| console.log(allTeam) | |||||
| const [teamId, setTeamId] = useState(_teamId) | const [teamId, setTeamId] = useState(_teamId) | ||||
| const [isCardClickedIndex, setIsCardClickedIndex] = useState(_teamId || 0); | const [isCardClickedIndex, setIsCardClickedIndex] = useState(_teamId || 0); | ||||
| const [period, setPeriod] = useState(0); | const [period, setPeriod] = useState(0); | ||||
| const dateMap: DateParams = useMemo(() => ({ | |||||
| 0: {startDate: "", endDate: ""}, | |||||
| 2: {startDate: `${currFinancialYear-2}-${startDate}`, endDate: `${currFinancialYear-1}-${endDate}`}, | |||||
| 3: {startDate: `${currFinancialYear-3}-${startDate}`, endDate: `${currFinancialYear-2}-${endDate}`}, | |||||
| 4: {startDate: `${currFinancialYear-4}-${startDate}`, endDate: `${currFinancialYear-3}-${endDate}`}, | |||||
| 5: {startDate: "", endDate: `${currFinancialYear-4}-${endDate}`}, | |||||
| }), [currYear, startDate, endDate]) | |||||
| const dateMap: DateParams = useMemo(() => { | |||||
| const thisYear = currDate <= `${currYear}-${endDate}` ? | |||||
| {startDate: `${currYear-1}-${startDate}`, endDate: `${currYear}-${endDate}`} : | |||||
| {startDate: `${currYear}-${startDate}`, endDate: `${currFinancialYear}-${endDate}`} | |||||
| const map: DateParams = { | |||||
| 0: {startDate: "", endDate: ""}, | |||||
| 1: thisYear, | |||||
| [lengthOfCombo - 1]: {startDate: "", endDate: `${currFinancialYear-(lengthOfCombo - 2)}-${endDate}`} | |||||
| } | |||||
| for (let i = 2; i < lengthOfCombo - 1; i++) { | |||||
| map[i] = { | |||||
| startDate: `${currFinancialYear-i}-${startDate}`, | |||||
| endDate: `${currFinancialYear-(i - 1)}-${endDate}` | |||||
| } | |||||
| } | |||||
| return map | |||||
| }, [currDate, currYear, currFinancialYear, startDate, endDate, lengthOfCombo]) | |||||
| // const comboList: string[] = useMemo(() => { | |||||
| // const list = ["All"] | |||||
| // var lastYear = "" | |||||
| // for (let i = 1; i < lengthOfCombo; i++) { | |||||
| // var currYearStr = t(" (current year) ") | |||||
| // var yearsStr = `${currFinancialYear - i} - ${currFinancialYear - i + 1}` | |||||
| // var str = yearsStr | |||||
| // if (i == 1) str = yearsStr + currYearStr | |||||
| // lastYear = `${currFinancialYear - i}` | |||||
| // list.push(str) | |||||
| // } | |||||
| // list[lengthOfCombo - 1] = `< ${lastYear}` | |||||
| // return list | |||||
| // }, []) | |||||
| const comboList: string[] = useMemo(() => { | |||||
| const list = ["All"]; | |||||
| for (let i = 1; i < lengthOfCombo - 1; i++) { | |||||
| const yearRange = `${currFinancialYear - i} - ${currFinancialYear - i + 1}`; | |||||
| const label = i === 1 ? `${yearRange} ${t("(current year)")}` : yearRange; | |||||
| list.push(label); | |||||
| } | |||||
| const oldestYear = currFinancialYear - (lengthOfCombo - 2); | |||||
| list.push(`< ${oldestYear}`); | |||||
| return list; | |||||
| }, [currFinancialYear, lengthOfCombo, t]); | |||||
| const fetchFinancialSummaryByProject = useCallback(async (endDate: string, startDate: string) => { | const fetchFinancialSummaryByProject = useCallback(async (endDate: string, startDate: string) => { | ||||
| setIsLoading(true) | setIsLoading(true) | ||||
| @@ -95,27 +133,14 @@ const FinancialSummaryPage: React.FC<Props> = ({ | |||||
| setTeamId(teamId) | setTeamId(teamId) | ||||
| }, []); | }, []); | ||||
| const handleFilter = useCallback((value: number) => { | |||||
| const handleFilter = useCallback(async (value: number) => { | |||||
| setPeriod(value) | setPeriod(value) | ||||
| console.log(value) | console.log(value) | ||||
| var _startDate: string = "" | |||||
| var _endDate = "" | |||||
| if (value == 1) { | |||||
| if (curr <= `${currYear}-${endDate}`) { | |||||
| _startDate = `${currYear - 1}-${startDate}` | |||||
| _endDate = `${currYear}-${endDate}` | |||||
| } else { | |||||
| _startDate = `${currYear}-${startDate}` | |||||
| _endDate = `${currFinancialYear}-${endDate}` | |||||
| } | |||||
| } else { | |||||
| _startDate = dateMap[value as keyof DateParams].startDate | |||||
| _endDate = dateMap[value as keyof DateParams].endDate | |||||
| } | |||||
| var _startDate = dateMap[value as keyof DateParams].startDate | |||||
| var _endDate = dateMap[value as keyof DateParams].endDate | |||||
| console.log(_startDate) | console.log(_startDate) | ||||
| console.log(_endDate) | console.log(_endDate) | ||||
| fetchFinancialSummaryByProject(_endDate, _startDate) | |||||
| await fetchFinancialSummaryByProject(_endDate, _startDate) | |||||
| }, [isCardClickedIndex]) | }, [isCardClickedIndex]) | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -128,7 +153,7 @@ const FinancialSummaryPage: React.FC<Props> = ({ | |||||
| setByClient(sumUpByClient(mainData)) | setByClient(sumUpByClient(mainData)) | ||||
| } | } | ||||
| }, [teamId]) | }, [teamId]) | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Card sx={{ display: "block" }}> | <Card sx={{ display: "block" }}> | ||||
| @@ -144,18 +169,11 @@ const FinancialSummaryPage: React.FC<Props> = ({ | |||||
| label="Age" | label="Age" | ||||
| onChange={(e) => handleFilter(Number(e.target.value))} | onChange={(e) => handleFilter(Number(e.target.value))} | ||||
| > | > | ||||
| {Array.from({ length: 6 }).map((_, i) => { | |||||
| if (i == 0) { | |||||
| return <MenuItem key={i} value={i}>{`All`}</MenuItem> | |||||
| } else if (i == 1) { | |||||
| return <MenuItem key={i} value={i}>{`${currFinancialYear - i} - ${currFinancialYear - i + 1} (current year)`}</MenuItem> | |||||
| } else if (i == 5) { | |||||
| return <MenuItem value={i}>{`< ${currYear - i + 1}`}</MenuItem> | |||||
| } else { | |||||
| return <MenuItem key={i} value={i}>{`${currFinancialYear - i} - ${currFinancialYear - i + 1}`}</MenuItem> | |||||
| } | |||||
| } | |||||
| )} | |||||
| { | |||||
| comboList.map((str, i) => { | |||||
| return <MenuItem key={i} value={i}>{str}</MenuItem> | |||||
| }) | |||||
| } | |||||
| </Select> | </Select> | ||||
| </FormControl> | </FormControl> | ||||
| </Box> | </Box> | ||||
| @@ -166,27 +184,28 @@ const FinancialSummaryPage: React.FC<Props> = ({ | |||||
| <CardContent component={Stack} spacing={4}> | <CardContent component={Stack} spacing={4}> | ||||
| <div className="ml-10 mr-10" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'start'}}> | <div className="ml-10 mr-10" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'start'}}> | ||||
| {_teamId == 0 && allTeam && | {_teamId == 0 && allTeam && | ||||
| <div className="hover:cursor-pointer ml-4 inline-block" key={0} onClick={() => handleCardClick(0)}> | |||||
| <div className="hover:cursor-pointer ml-4 mb-2 inline-block" key={0} onClick={() => handleCardClick(0)}> | |||||
| <ProjectFinancialCard | <ProjectFinancialCard | ||||
| Title={t("All Team")} | |||||
| TeamId={0} | |||||
| Title={t("All Team")} | |||||
| TeamId={0} | |||||
| TotalActiveProjectNumber={allTeam.activeProject} | TotalActiveProjectNumber={allTeam.activeProject} | ||||
| TotalFees={allTeam.totalFee} | TotalFees={allTeam.totalFee} | ||||
| TotalBudget={allTeam.totalBudget} | TotalBudget={allTeam.totalBudget} | ||||
| TotalCumulative={allTeam.manhourExpense + allTeam.projectExpense} | TotalCumulative={allTeam.manhourExpense + allTeam.projectExpense} | ||||
| TotalProjectExpense={allTeam.projectExpense} | TotalProjectExpense={allTeam.projectExpense} | ||||
| TotalInvoicedAmount={allTeam.invoicedAmount} | |||||
| TotalInvoicedAmount={allTeam.invoicedAmount} | |||||
| TotalUnInvoicedAmount={allTeam.totalFee - allTeam.invoicedAmount} | TotalUnInvoicedAmount={allTeam.totalFee - allTeam.invoicedAmount} | ||||
| // TotalUnInvoicedAmount={allTeam.uninvoicedAmount} | |||||
| TotalReceivedAmount={allTeam.paidAmount} | TotalReceivedAmount={allTeam.paidAmount} | ||||
| CashFlowStatus={allTeam.invoicedAmount >= (allTeam.projectExpense + allTeam.manhourExpense) ? "Positive" : "Negative"} | CashFlowStatus={allTeam.invoicedAmount >= (allTeam.projectExpense + allTeam.manhourExpense) ? "Positive" : "Negative"} | ||||
| CostPerformanceIndex={allTeam.invoicedAmount/(allTeam.projectExpense + allTeam.manhourExpense) || 0} | |||||
| CostPerformanceIndex={!isFinite(allTeam.invoicedAmount/(allTeam.projectExpense + allTeam.manhourExpense)) ? 0 : allTeam.invoicedAmount/(allTeam.projectExpense + allTeam.manhourExpense) || 0} | |||||
| ProjectedCashFlowStatus={allTeam.totalFee >= (allTeam.projectExpense + allTeam.manhourExpense) ? "Positive" : "Negative"} | ProjectedCashFlowStatus={allTeam.totalFee >= (allTeam.projectExpense + allTeam.manhourExpense) ? "Positive" : "Negative"} | ||||
| ProjectedCPI={allTeam.totalFee/(allTeam.projectExpense + allTeam.manhourExpense)} | |||||
| ProjectedCPI={!isFinite(allTeam.totalFee/(allTeam.projectExpense + allTeam.manhourExpense)) ? 0 : allTeam.totalFee/(allTeam.projectExpense + allTeam.manhourExpense) || 0} | |||||
| ClickedIndex={isCardClickedIndex} | ClickedIndex={isCardClickedIndex} | ||||
| Index={0}/> | Index={0}/> | ||||
| </div>} | </div>} | ||||
| {byTeam.length > 0 && byTeam.map((record) => ( | {byTeam.length > 0 && byTeam.map((record) => ( | ||||
| <div className="hover:cursor-pointer ml-4 inline-block" key={record.id} onClick={() => handleCardClick(record.id)}> | |||||
| <div className="hover:cursor-pointer ml-4 mb-2 inline-block" key={record.id} onClick={() => handleCardClick(record.id)}> | |||||
| <ProjectFinancialCard | <ProjectFinancialCard | ||||
| Title={record.team} | Title={record.team} | ||||
| TeamId={record.id} | TeamId={record.id} | ||||
| @@ -197,11 +216,12 @@ const FinancialSummaryPage: React.FC<Props> = ({ | |||||
| TotalProjectExpense={record.projectExpense} | TotalProjectExpense={record.projectExpense} | ||||
| TotalInvoicedAmount={record.invoicedAmount} | TotalInvoicedAmount={record.invoicedAmount} | ||||
| TotalUnInvoicedAmount={Math.abs(record.totalFee - record.invoicedAmount)} | TotalUnInvoicedAmount={Math.abs(record.totalFee - record.invoicedAmount)} | ||||
| // TotalUnInvoicedAmount={Math.abs(record.uninvoicedAmount)} | |||||
| TotalReceivedAmount={record.paidAmount} | TotalReceivedAmount={record.paidAmount} | ||||
| CashFlowStatus={record.invoicedAmount >= (record.projectExpense + record.manhourExpense) ? "Positive" : "Negative"} | CashFlowStatus={record.invoicedAmount >= (record.projectExpense + record.manhourExpense) ? "Positive" : "Negative"} | ||||
| CostPerformanceIndex={record.invoicedAmount/(record.projectExpense + record.manhourExpense) || 0} | |||||
| CostPerformanceIndex={!isFinite(record.invoicedAmount/(record.projectExpense + record.manhourExpense)) ? 0 : record.invoicedAmount/(record.projectExpense + record.manhourExpense) || 0} | |||||
| ProjectedCashFlowStatus={record.totalFee >= (record.projectExpense + record.manhourExpense) ? "Positive" : "Negative"} | ProjectedCashFlowStatus={record.totalFee >= (record.projectExpense + record.manhourExpense) ? "Positive" : "Negative"} | ||||
| ProjectedCPI={record.totalFee/(record.projectExpense + record.manhourExpense)} | |||||
| ProjectedCPI={!isFinite(record.totalFee/(record.projectExpense + record.manhourExpense)) ? 0 : record.totalFee/(record.projectExpense + record.manhourExpense) || 0} | |||||
| ClickedIndex={isCardClickedIndex} | ClickedIndex={isCardClickedIndex} | ||||
| Index={record.id}/> | Index={record.id}/> | ||||
| </div> | </div> | ||||
| @@ -8,8 +8,10 @@ import { useEffect, useMemo, useState } from "react"; | |||||
| import CustomDatagrid from "../CustomDatagrid"; | import CustomDatagrid from "../CustomDatagrid"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
| import { Box } from "@mui/material"; | |||||
| import { Box, Card, CardHeader } from "@mui/material"; | |||||
| import { SumOfByClient } from "./gptFn"; | import { SumOfByClient } from "./gptFn"; | ||||
| import { exportFinancialSummaryV2ByClientExcel, exportFinancialSummaryV2ByProjectExcel } from "@/app/api/financialsummary/actions"; | |||||
| import { downloadFile } from "@/app/utils/commonUtil"; | |||||
| // import { summarizeFinancialData } from "./gptFn"; | // import { summarizeFinancialData } from "./gptFn"; | ||||
| interface Props { | interface Props { | ||||
| @@ -99,7 +101,8 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| headerName: t("Cash Flow Status"), | headerName: t("Cash Flow Status"), | ||||
| minWidth: 80, | minWidth: 80, | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| if (params.row.invoicedAmount >= params.row.cumulativeExpenditure) { | |||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense | |||||
| if (params.row.invoicedAmount >= cumulativeExpenditure) { | |||||
| return <span className={greenColor}>{t("Positive")}</span>; | return <span className={greenColor}>{t("Positive")}</span>; | ||||
| } else { | } else { | ||||
| return <span className={redColor}>{t("Negative")}</span>; | return <span className={redColor}>{t("Negative")}</span>; | ||||
| @@ -112,7 +115,8 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| headerName: "CPI", | headerName: "CPI", | ||||
| minWidth: 50, | minWidth: 50, | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| var cpi = params.row.invoicedAmount/(params.row.projectExpense + params.row.invoicedAmount) || 0 | |||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense | |||||
| var cpi = params.row.invoicedAmount/cumulativeExpenditure || 0 | |||||
| return ( | return ( | ||||
| <span className={cpi >= 1 ? greenColor : redColor}> | <span className={cpi >= 1 ? greenColor : redColor}> | ||||
| {cpi.toLocaleString(undefined, { | {cpi.toLocaleString(undefined, { | ||||
| @@ -129,7 +133,7 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| headerName: t("Projected Cash Flow Status"), | headerName: t("Projected Cash Flow Status"), | ||||
| minWidth: 100, | minWidth: 100, | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.invoicedAmount | |||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense | |||||
| if (params.row.totalFee >= cumulativeExpenditure) { | if (params.row.totalFee >= cumulativeExpenditure) { | ||||
| return <span className={greenColor}>{t("Positive")}</span>; | return <span className={greenColor}>{t("Positive")}</span>; | ||||
| } else { | } else { | ||||
| @@ -143,7 +147,8 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| headerName: t("Projected CPI"), | headerName: t("Projected CPI"), | ||||
| minWidth: 50, | minWidth: 50, | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| var projectedCpi = params.row.totalFee/(params.row.projectExpense + params.row.invoicedAmount) == Infinity ? 'N/A' : params.row.totalFee/(params.row.projectExpense + params.row.invoicedAmount) | |||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense | |||||
| var projectedCpi = params.row.totalFee/cumulativeExpenditure == Infinity ? 'N/A' : params.row.totalFee/cumulativeExpenditure | |||||
| return ( | return ( | ||||
| <span | <span | ||||
| className={(typeof projectedCpi == "number" && projectedCpi >= 1 ? greenColor : redColor)} | className={(typeof projectedCpi == "number" && projectedCpi >= 1 ? greenColor : redColor)} | ||||
| @@ -199,7 +204,7 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| minWidth: 250, | minWidth: 250, | ||||
| type: "number", | type: "number", | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.invoicedAmount | |||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense | |||||
| return ( | return ( | ||||
| <span> | <span> | ||||
| $ | $ | ||||
| @@ -272,7 +277,9 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| minWidth: 250, | minWidth: 250, | ||||
| type: "number", | type: "number", | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| var nonInvoiced = params.row.totalFee - params.row.invoicedAmount | |||||
| var fee = params.row.totalFee | |||||
| var invoiced = params.row.invoicedAmount | |||||
| var nonInvoiced = fee - invoiced < 0 ? 0 : fee - invoiced | |||||
| return ( | return ( | ||||
| <span> | <span> | ||||
| $ | $ | ||||
| @@ -341,7 +348,7 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| headerName: t("Cash Flow Status"), | headerName: t("Cash Flow Status"), | ||||
| minWidth: 100, | minWidth: 100, | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.invoicedAmount | |||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense | |||||
| return params.row.invoicedAmount >= cumulativeExpenditure ? | return params.row.invoicedAmount >= cumulativeExpenditure ? | ||||
| <span className={greenColor}>{t("Positive")}</span> | <span className={greenColor}>{t("Positive")}</span> | ||||
| : <span className={redColor}>{t("Negative")}</span> | : <span className={redColor}>{t("Negative")}</span> | ||||
| @@ -353,7 +360,7 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| headerName: t("CPI"), | headerName: t("CPI"), | ||||
| minWidth: 50, | minWidth: 50, | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.invoicedAmount | |||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense | |||||
| var cpi = cumulativeExpenditure != 0 ? params.row.invoicedAmount/cumulativeExpenditure : 0 | var cpi = cumulativeExpenditure != 0 ? params.row.invoicedAmount/cumulativeExpenditure : 0 | ||||
| var cpiString = cpi.toLocaleString(undefined, { | var cpiString = cpi.toLocaleString(undefined, { | ||||
| minimumFractionDigits: 2, | minimumFractionDigits: 2, | ||||
| @@ -370,8 +377,8 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| headerName: t("Projected Cash Flow Status"), | headerName: t("Projected Cash Flow Status"), | ||||
| minWidth: 100, | minWidth: 100, | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.invoicedAmount | |||||
| var status = params.row.invoiceAmount >= cumulativeExpenditure | |||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense | |||||
| var status = params.row.totalFee >= cumulativeExpenditure | |||||
| return status ? | return status ? | ||||
| <span className={greenColor}>{t("Positive")}</span> | <span className={greenColor}>{t("Positive")}</span> | ||||
| : <span className={redColor}>{t("Negative")}</span> | : <span className={redColor}>{t("Negative")}</span> | ||||
| @@ -383,7 +390,7 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| headerName: t("Projected CPI"), | headerName: t("Projected CPI"), | ||||
| minWidth: 50, | minWidth: 50, | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.invoicedAmount | |||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense | |||||
| var projectCpi = cumulativeExpenditure != 0 ? params.row.totalFee/cumulativeExpenditure : 0 | var projectCpi = cumulativeExpenditure != 0 ? params.row.totalFee/cumulativeExpenditure : 0 | ||||
| var projectCpiString = projectCpi.toLocaleString(undefined, { | var projectCpiString = projectCpi.toLocaleString(undefined, { | ||||
| minimumFractionDigits: 2, | minimumFractionDigits: 2, | ||||
| @@ -439,7 +446,7 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| minWidth: 280, | minWidth: 280, | ||||
| type: "number", | type: "number", | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.invoicedAmount | |||||
| var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense | |||||
| return ( | return ( | ||||
| <span> | <span> | ||||
| $ | $ | ||||
| @@ -512,11 +519,13 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| minWidth: 250, | minWidth: 250, | ||||
| type: "number", | type: "number", | ||||
| renderCell: (params: any) => { | renderCell: (params: any) => { | ||||
| var uninvoiced = params.row.totalFee - params.row.invoicedAmount | |||||
| var fee = params.row.totalFee | |||||
| var invoiced = params.row.invoicedAmount | |||||
| var nonInvoiced = fee - invoiced < 0 ? 0 : fee - invoiced | |||||
| return ( | return ( | ||||
| <span> | <span> | ||||
| $ | $ | ||||
| {uninvoiced.toLocaleString(undefined, { | |||||
| {nonInvoiced.toLocaleString(undefined, { | |||||
| minimumFractionDigits: 2, | minimumFractionDigits: 2, | ||||
| maximumFractionDigits: 2, | maximumFractionDigits: 2, | ||||
| })} | })} | ||||
| @@ -544,64 +553,104 @@ const FinancialStatusByProject: React.FC<Props> = ({ | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| const handleExportByClient = async () => { | |||||
| const response = await exportFinancialSummaryV2ByClientExcel(filteredByClientRows) | |||||
| if (response) { | |||||
| downloadFile(new Uint8Array(response.blobValue), response.filename!!) | |||||
| } | |||||
| console.log(filteredByClientRows) | |||||
| }; | |||||
| const handleExportByProject = async () => { | |||||
| const response = await exportFinancialSummaryV2ByProjectExcel(filteredByProjectRows) | |||||
| if (response) { | |||||
| downloadFile(new Uint8Array(response.blobValue), response.filename!!) | |||||
| } | |||||
| console.log(filteredByProjectRows) | |||||
| }; | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Box sx={{ mt: 3 }}> | <Box sx={{ mt: 3 }}> | ||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={(query) => { | |||||
| console.log(query) | |||||
| if (query.projectCode.length > 0 || query.projectName.length > 0) { | |||||
| setFilteredByProjectRows( | |||||
| financialSummByProject.filter( | |||||
| (cp) => | |||||
| cp.projectCode.toLowerCase().includes(query.projectCode.trim().toLowerCase()) && | |||||
| cp.projectName.toLowerCase().includes(query.projectName.trim().toLowerCase()) | |||||
| ), | |||||
| ); | |||||
| } else { | |||||
| setFilteredByProjectRows(financialSummByProject) | |||||
| } | |||||
| }} | |||||
| /> | |||||
| <div style={{ display: "inline-block", width: "99%", marginLeft: 10 }}> | |||||
| <CustomDatagrid | |||||
| rows={filteredByProjectRows} | |||||
| columns={columns1} | |||||
| columnWidth={200} | |||||
| dataGridHeight={300} | |||||
| loading={isLoading} | |||||
| <Card className="mt-5"> | |||||
| <div style={{display:"inline-block"}}> | |||||
| <CardHeader className="text-slate-500" title= {t("Financial Status (by Project)")}/> | |||||
| </div> | |||||
| <div style={{display:"inline-block"}}> | |||||
| {filteredByProjectRows.length > 0 && ( | |||||
| <button onClick={handleExportByProject} className="hover:cursor-pointer hover:bg-lime-50 text-base bg-transparent border-lime-600 text-lime-600 border-solid rounded-md w-36"> | |||||
| {t("Export Excel")} | |||||
| </button> | |||||
| )} | |||||
| </div> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={(query) => { | |||||
| console.log(query) | |||||
| if (query.projectCode.length > 0 || query.projectName.length > 0) { | |||||
| setFilteredByProjectRows( | |||||
| financialSummByProject.filter( | |||||
| (cp) => | |||||
| cp.projectCode.toLowerCase().includes(query.projectCode.trim().toLowerCase()) && | |||||
| cp.projectName.toLowerCase().includes(query.projectName.trim().toLowerCase()) | |||||
| ), | |||||
| ); | |||||
| } else { | |||||
| setFilteredByProjectRows(financialSummByProject) | |||||
| } | |||||
| }} | |||||
| /> | /> | ||||
| </div> | |||||
| <div style={{ display: "inline-block", width: "99%", marginLeft: 10 }}> | |||||
| <CustomDatagrid | |||||
| rows={filteredByProjectRows} | |||||
| columns={columns1} | |||||
| columnWidth={200} | |||||
| dataGridHeight={300} | |||||
| loading={isLoading} | |||||
| /> | |||||
| </div> | |||||
| {/* <SearchResults<StaffResult> items={filteredStaff} columns={columns} /> */} | {/* <SearchResults<StaffResult> items={filteredStaff} columns={columns} /> */} | ||||
| </Card> | |||||
| </Box> | </Box> | ||||
| <Box sx={{ mt: 3 }}> | <Box sx={{ mt: 3 }}> | ||||
| <SearchBox | |||||
| criteria={searchCriteria2} | |||||
| onSearch={(query) => { | |||||
| console.log(query) | |||||
| if (query.customerCode.length > 0 || query.customerName.length > 0) { | |||||
| setFilteredByClientRows( | |||||
| financialSummByClient.filter( | |||||
| (cp) => | |||||
| cp.customerCode.toLowerCase().includes(query.customerCode.trim().toLowerCase()) && | |||||
| cp.customerName.toLowerCase().includes(query.customerName.trim().toLowerCase()) | |||||
| ), | |||||
| ); | |||||
| } else { | |||||
| setFilteredByClientRows(financialSummByClient) | |||||
| } | |||||
| }} | |||||
| /> | |||||
| <div style={{ display: "inline-block", width: "99%", marginLeft: 10 }}> | |||||
| <CustomDatagrid | |||||
| rows={filteredByClientRows} | |||||
| columns={columns2} | |||||
| columnWidth={200} | |||||
| dataGridHeight={300} | |||||
| loading={isLoading} | |||||
| <Card className="mt-5"> | |||||
| <div style={{display:"inline-block"}}> | |||||
| <CardHeader className="text-slate-500" title= {t("Financial Status (by Client)")}/> | |||||
| </div> | |||||
| <div style={{display:"inline-block"}}> | |||||
| {filteredByProjectRows.length > 0 && ( | |||||
| <button onClick={handleExportByClient} className="hover:cursor-pointer hover:bg-lime-50 text-base bg-transparent border-lime-600 text-lime-600 border-solid rounded-md w-36"> | |||||
| {t("Export Excel")} | |||||
| </button> | |||||
| )} | |||||
| </div> | |||||
| <SearchBox | |||||
| criteria={searchCriteria2} | |||||
| onSearch={(query) => { | |||||
| console.log(query) | |||||
| if (query.customerCode.length > 0 || query.customerName.length > 0) { | |||||
| setFilteredByClientRows( | |||||
| financialSummByClient.filter( | |||||
| (cp) => | |||||
| cp.customerCode.toLowerCase().includes(query.customerCode.trim().toLowerCase()) && | |||||
| cp.customerName.toLowerCase().includes(query.customerName.trim().toLowerCase()) | |||||
| ), | |||||
| ); | |||||
| } else { | |||||
| setFilteredByClientRows(financialSummByClient) | |||||
| } | |||||
| }} | |||||
| /> | /> | ||||
| </div> | |||||
| <div style={{ display: "inline-block", width: "99%", marginLeft: 10 }}> | |||||
| <CustomDatagrid | |||||
| rows={filteredByClientRows} | |||||
| columns={columns2} | |||||
| columnWidth={200} | |||||
| dataGridHeight={300} | |||||
| loading={isLoading} | |||||
| /> | |||||
| </div> | |||||
| </Card> | |||||
| </Box> | </Box> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -8,6 +8,7 @@ export type SumOfByTeam = { | |||||
| manhourExpense: number, | manhourExpense: number, | ||||
| projectExpense: number, | projectExpense: number, | ||||
| invoicedAmount: number, | invoicedAmount: number, | ||||
| uninvoicedAmount: number, | |||||
| paidAmount: number, | paidAmount: number, | ||||
| activeProject: number, | activeProject: number, | ||||
| } | } | ||||
| @@ -21,6 +22,7 @@ export type SumOfByClient = { | |||||
| manhourExpense: number, | manhourExpense: number, | ||||
| projectExpense: number, | projectExpense: number, | ||||
| invoicedAmount: number, | invoicedAmount: number, | ||||
| uninvoicedAmount: number, | |||||
| paidAmount: number, | paidAmount: number, | ||||
| sumOfProjects: number, | sumOfProjects: number, | ||||
| } | } | ||||
| @@ -36,6 +38,7 @@ export function sumUpByClient(data: FinancialByProject[]): SumOfByClient[] { | |||||
| manhourExpense: 0, | manhourExpense: 0, | ||||
| projectExpense: 0, | projectExpense: 0, | ||||
| invoicedAmount: 0, | invoicedAmount: 0, | ||||
| uninvoicedAmount: 0, | |||||
| paidAmount: 0, | paidAmount: 0, | ||||
| sumOfProjects: 0 | sumOfProjects: 0 | ||||
| }; | }; | ||||
| @@ -46,6 +49,7 @@ export function sumUpByClient(data: FinancialByProject[]): SumOfByClient[] { | |||||
| acc[item.custId].manhourExpense += item.manhourExpense; | acc[item.custId].manhourExpense += item.manhourExpense; | ||||
| acc[item.custId].projectExpense += item.projectExpense; | acc[item.custId].projectExpense += item.projectExpense; | ||||
| acc[item.custId].invoicedAmount += item.invoicedAmount; | acc[item.custId].invoicedAmount += item.invoicedAmount; | ||||
| acc[item.custId].uninvoicedAmount += item.uninvoicedAmount; | |||||
| acc[item.custId].paidAmount += item.paidAmount; | acc[item.custId].paidAmount += item.paidAmount; | ||||
| acc[item.custId].sumOfProjects += 1; | acc[item.custId].sumOfProjects += 1; | ||||
| @@ -64,6 +68,7 @@ export function sumUpByTeam(data: FinancialByProject[]): SumOfByTeam[] { | |||||
| manhourExpense: 0, | manhourExpense: 0, | ||||
| projectExpense: 0, | projectExpense: 0, | ||||
| invoicedAmount: 0, | invoicedAmount: 0, | ||||
| uninvoicedAmount: 0, | |||||
| paidAmount: 0, | paidAmount: 0, | ||||
| activeProject: 0 | activeProject: 0 | ||||
| }; | }; | ||||
| @@ -75,6 +80,7 @@ export function sumUpByTeam(data: FinancialByProject[]): SumOfByTeam[] { | |||||
| acc[item.teamId].manhourExpense += item.manhourExpense; | acc[item.teamId].manhourExpense += item.manhourExpense; | ||||
| acc[item.teamId].projectExpense += item.projectExpense; | acc[item.teamId].projectExpense += item.projectExpense; | ||||
| acc[item.teamId].invoicedAmount += item.invoicedAmount; | acc[item.teamId].invoicedAmount += item.invoicedAmount; | ||||
| acc[item.teamId].uninvoicedAmount += item.uninvoicedAmount; | |||||
| acc[item.teamId].paidAmount += item.paidAmount; | acc[item.teamId].paidAmount += item.paidAmount; | ||||
| acc[item.teamId].activeProject += 1; | acc[item.teamId].activeProject += 1; | ||||
| @@ -13,6 +13,7 @@ import { reverse, uniqBy } from "lodash"; | |||||
| import { loadDrafts } from "@/app/utils/draftUtils"; | import { loadDrafts } from "@/app/utils/draftUtils"; | ||||
| import { TeamResult } from "@/app/api/team"; | import { TeamResult } from "@/app/api/team"; | ||||
| import { Customer } from "@/app/api/customer"; | import { Customer } from "@/app/api/customer"; | ||||
| import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | |||||
| type ProjectResultOrDraft = ProjectResult & { isDraft?: boolean }; | type ProjectResultOrDraft = ProjectResult & { isDraft?: boolean }; | ||||
| @@ -129,6 +130,17 @@ const ProjectSearch: React.FC<Props> = ({ | |||||
| [router], | [router], | ||||
| ); | ); | ||||
| const onProjectCopyClick = useCallback( | |||||
| (project: ProjectResultOrDraft) => { | |||||
| if (!project.isDraft) { | |||||
| if (Boolean(project.mainProject)) { | |||||
| router.push(`/projects/copySub?id=${project.id}`); | |||||
| } else router.push(`/projects/copy?id=${project.id}`); | |||||
| } | |||||
| }, | |||||
| [router], | |||||
| ); | |||||
| const columns = useMemo<Column<ProjectResult>[]>( | const columns = useMemo<Column<ProjectResult>[]>( | ||||
| () => [ | () => [ | ||||
| { | { | ||||
| @@ -138,6 +150,16 @@ const ProjectSearch: React.FC<Props> = ({ | |||||
| buttonIcon: <EditNote />, | buttonIcon: <EditNote />, | ||||
| disabled: !abilities.includes(MAINTAIN_PROJECT), | disabled: !abilities.includes(MAINTAIN_PROJECT), | ||||
| }, | }, | ||||
| { | |||||
| name: "id", | |||||
| label: t("Copy"), | |||||
| onClick: onProjectCopyClick, | |||||
| buttonIcon: <ContentCopyIcon />, | |||||
| disabled: !abilities.includes(MAINTAIN_PROJECT), | |||||
| disabledRows: { | |||||
| status: ["Draft"] | |||||
| } | |||||
| }, | |||||
| { name: "code", label: t("Project Code") }, | { name: "code", label: t("Project Code") }, | ||||
| { name: "name", label: t("Project Name") }, | { name: "name", label: t("Project Name") }, | ||||
| { name: "category", label: t("Project Category") }, | { name: "category", label: t("Project Category") }, | ||||
| @@ -35,6 +35,7 @@ interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | |||||
| onClick: (item: T) => void; | onClick: (item: T) => void; | ||||
| buttonIcon: React.ReactNode; | buttonIcon: React.ReactNode; | ||||
| disabled?: boolean; | disabled?: boolean; | ||||
| disabledRows?: { [columnName in keyof T]: string[] }; // Filter the row which is going to be disabled | |||||
| } | } | ||||
| export type Column<T extends ResultWithId> = | export type Column<T extends ResultWithId> = | ||||
| @@ -84,6 +85,22 @@ function SearchResults<T extends ResultWithId>({ | |||||
| setPage(0); | setPage(0); | ||||
| }; | }; | ||||
| const disabledRows = <T extends ResultWithId> ( | |||||
| column: ColumnWithAction<T>, | |||||
| item: T | |||||
| ): Boolean => { | |||||
| if (column.disabledRows) { | |||||
| for (const [key, value] of Object.entries(column.disabledRows)) { | |||||
| if (value | |||||
| .map(v => v.toLowerCase()) | |||||
| .includes(String(item[key as keyof T]).toLowerCase()) | |||||
| ) return true; | |||||
| } | |||||
| } | |||||
| return false; | |||||
| }; | |||||
| const table = ( | const table = ( | ||||
| <> | <> | ||||
| <TableContainer sx={{ maxHeight: 440 }}> | <TableContainer sx={{ maxHeight: 440 }}> | ||||
| @@ -112,7 +129,7 @@ function SearchResults<T extends ResultWithId>({ | |||||
| <IconButton | <IconButton | ||||
| color={column.color ?? "primary"} | color={column.color ?? "primary"} | ||||
| onClick={() => column.onClick(item)} | onClick={() => column.onClick(item)} | ||||
| disabled={Boolean(column.disabled)} | |||||
| disabled={Boolean(column.disabled) || Boolean(disabledRows(column, item))} | |||||
| > | > | ||||
| {column.buttonIcon} | {column.buttonIcon} | ||||
| </IconButton> | </IconButton> | ||||