Browse Source

Merge branch 'develop'

# Conflicts:
#	src/components/CreateProject/CreateProjectWrapper.tsx
tags/Baseline_180220205_Frontend
MSI\2Fi 6 months ago
parent
commit
37e509ddd9
15 changed files with 519 additions and 162 deletions
  1. +17
    -0
      src/app/(main)/projects/copy/not-found.tsx
  2. +77
    -0
      src/app/(main)/projects/copy/page.tsx
  3. +17
    -0
      src/app/(main)/projects/copySub/not-found.tsx
  4. +79
    -0
      src/app/(main)/projects/copySub/page.tsx
  5. +28
    -1
      src/app/api/financialsummary/actions.ts
  6. +1
    -0
      src/app/api/financialsummary/index.ts
  7. +3
    -1
      src/components/CreateProject/CreateProject.tsx
  8. +29
    -5
      src/components/CreateProject/CreateProjectWrapper.tsx
  9. +2
    -2
      src/components/CreateProject/ProjectClientDetails.tsx
  10. +10
    -11
      src/components/NavigationContent/NavigationContent.tsx
  11. +96
    -76
      src/components/ProjectFinancialSummaryV2/FinancialSummary.tsx
  12. +114
    -65
      src/components/ProjectFinancialSummaryV2/FinnancialStatusByProject.tsx
  13. +6
    -0
      src/components/ProjectFinancialSummaryV2/gptFn.tsx
  14. +22
    -0
      src/components/ProjectSearch/ProjectSearch.tsx
  15. +18
    -1
      src/components/SearchResults/SearchResults.tsx

+ 17
- 0
src/app/(main)/projects/copy/not-found.tsx View File

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

+ 77
- 0
src/app/(main)/projects/copy/page.tsx View File

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

+ 17
- 0
src/app/(main)/projects/copySub/not-found.tsx View File

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

+ 79
- 0
src/app/(main)/projects/copySub/page.tsx View File

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

+ 28
- 1
src/app/api/financialsummary/actions.ts View File

@@ -6,7 +6,8 @@ import { Dayjs } from "dayjs";
import { cache } from "react";
import { FileResponse } from "../reports/actions";
import { revalidateTag } from "next/cache";

import { SumOfByClient } from "@/components/ProjectFinancialSummaryV2/gptFn";
import { FinancialByProject } from ".";

export interface FinancialSummaryByClientResult {
teamId:number;
@@ -144,6 +145,32 @@ export const exportFinancialSummaryByProjectExcel = cache(async (data: ExportFin
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) => {
revalidateTag(tag)
}

+ 1
- 0
src/app/api/financialsummary/index.ts View File

@@ -33,6 +33,7 @@ export type FinancialByProject = {
totalBudget: number,
manhourExpense: number,
invoicedAmount: number,
uninvoicedAmount: number,
paidAmount: number,
projectExpense: number,
}


+ 3
- 1
src/components/CreateProject/CreateProject.tsx View File

@@ -64,6 +64,7 @@ import { deleteDraft, loadDraft, saveToLocalStorage } from "@/app/utils/draftUti

export interface Props {
isEditMode: boolean;
isCopyMode: boolean;
draftId?: number;
isSubProject: boolean;
mainProjects?: MainProject[];
@@ -116,6 +117,7 @@ const hasErrorsInTab = (

const CreateProject: React.FC<Props> = ({
isEditMode,
isCopyMode,
draftId,
isSubProject,
mainProjects,
@@ -547,7 +549,7 @@ const CreateProject: React.FC<Props> = ({
}
}, [totalManhour]);

const loading = isEditMode ? !Boolean(projectName) : false;
const loading = isEditMode || isCopyMode ? !Boolean(projectName) : false;

const submitDisabled =
loading ||


+ 29
- 5
src/components/CreateProject/CreateProjectWrapper.tsx View File

@@ -21,17 +21,26 @@ import { fetchGrades } from "@/app/api/grades";
import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil";

type CreateProjectProps = {
isEditMode: false;
isEditMode?: false;
isCopyMode?: false;
isSubProject?: boolean;
draftId?: number;
};
interface EditProjectProps {
isEditMode: true;
isCopyMode?: false;
projectId?: string;
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 [
@@ -79,7 +88,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => {
(teamLead) => teamLead.teamId === teamId || teamLead.team == "ST",
)
}
const projectInfo = props.isEditMode
const projectInfo = props.isEditMode || props.isCopyMode
? await fetchProjectDetails(props.projectId!)
: undefined;

@@ -87,10 +96,25 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => {
? await fetchMainProjects()
: 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 (
<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)}
defaultInputs={projectInfo}
allTasks={tasks}


+ 2
- 2
src/components/CreateProject/ProjectClientDetails.tsx View File

@@ -345,7 +345,7 @@ const ProjectClientDetails: React.FC<Props> = ({
// Boolean(errors.projectPlanStart)
// ||
new Date(planStart) > new Date(planEnd)
|| !Boolean(planStart)
|| Boolean(errors.projectPlanStart)
,
},
}}
@@ -373,7 +373,7 @@ const ProjectClientDetails: React.FC<Props> = ({
// Boolean(errors.projectPlanEnd)
// ||
new Date(planStart) > new Date(planEnd)
|| !Boolean(planEnd)
|| Boolean(errors.projectPlanEnd)
,
},
}}


+ 10
- 11
src/components/NavigationContent/NavigationContent.tsx View File

@@ -110,10 +110,19 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => {
path: "/home",
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 />,
label: "Financial Summary",
path: "/dashboard/ProjectFinancialSummary",
path: "/dashboard/ProjectFinancialSummaryV2",
isHidden: ![VIEW_DASHBOARD_ALL, VIEW_DASHBOARD_SELF].some((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
// {


+ 96
- 76
src/components/ProjectFinancialSummaryV2/FinancialSummary.tsx View File

@@ -14,36 +14,48 @@ interface Props {
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> = ({
_teamId,
financialSummByProject
}) => {
console.log(financialSummByProject)
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 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 [byTeam, setByTeam] = useState<SumOfByTeam[]>(() => sumUpByTeam(mainData)) // do fetch to set
const [byProject, setByProject] = useState<FinancialByProject[]>(financialSummByProject)
const [byClient, setByClient] = useState<SumOfByClient[]>(() => sumUpByClient(mainData))
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,
team: "All Team",
totalFee: 0,
@@ -51,34 +63,60 @@ const FinancialSummaryPage: React.FC<Props> = ({
manhourExpense: 0,
projectExpense: 0,
invoicedAmount: 0,
uninvoicedAmount: 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])
console.log(allTeam)

const [teamId, setTeamId] = useState(_teamId)
const [isCardClickedIndex, setIsCardClickedIndex] = useState(_teamId || 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) => {
setIsLoading(true)
@@ -95,27 +133,14 @@ const FinancialSummaryPage: React.FC<Props> = ({
setTeamId(teamId)
}, []);

const handleFilter = useCallback((value: number) => {
const handleFilter = useCallback(async (value: number) => {
setPeriod(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(_endDate)
fetchFinancialSummaryByProject(_endDate, _startDate)
await fetchFinancialSummaryByProject(_endDate, _startDate)
}, [isCardClickedIndex])

useEffect(() => {
@@ -128,7 +153,7 @@ const FinancialSummaryPage: React.FC<Props> = ({
setByClient(sumUpByClient(mainData))
}
}, [teamId])
return (
<>
<Card sx={{ display: "block" }}>
@@ -144,18 +169,11 @@ const FinancialSummaryPage: React.FC<Props> = ({
label="Age"
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>
</FormControl>
</Box>
@@ -166,27 +184,28 @@ const FinancialSummaryPage: React.FC<Props> = ({
<CardContent component={Stack} spacing={4}>
<div className="ml-10 mr-10" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'start'}}>
{_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
Title={t("All Team")}
TeamId={0}
Title={t("All Team")}
TeamId={0}
TotalActiveProjectNumber={allTeam.activeProject}
TotalFees={allTeam.totalFee}
TotalBudget={allTeam.totalBudget}
TotalCumulative={allTeam.manhourExpense + allTeam.projectExpense}
TotalProjectExpense={allTeam.projectExpense}
TotalInvoicedAmount={allTeam.invoicedAmount}
TotalInvoicedAmount={allTeam.invoicedAmount}
TotalUnInvoicedAmount={allTeam.totalFee - allTeam.invoicedAmount}
// TotalUnInvoicedAmount={allTeam.uninvoicedAmount}
TotalReceivedAmount={allTeam.paidAmount}
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"}
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}
Index={0}/>
</div>}
{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
Title={record.team}
TeamId={record.id}
@@ -197,11 +216,12 @@ const FinancialSummaryPage: React.FC<Props> = ({
TotalProjectExpense={record.projectExpense}
TotalInvoicedAmount={record.invoicedAmount}
TotalUnInvoicedAmount={Math.abs(record.totalFee - record.invoicedAmount)}
// TotalUnInvoicedAmount={Math.abs(record.uninvoicedAmount)}
TotalReceivedAmount={record.paidAmount}
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"}
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}
Index={record.id}/>
</div>


+ 114
- 65
src/components/ProjectFinancialSummaryV2/FinnancialStatusByProject.tsx View File

@@ -8,8 +8,10 @@ import { useEffect, useMemo, useState } from "react";
import CustomDatagrid from "../CustomDatagrid";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";
import { Box } from "@mui/material";
import { Box, Card, CardHeader } from "@mui/material";
import { SumOfByClient } from "./gptFn";
import { exportFinancialSummaryV2ByClientExcel, exportFinancialSummaryV2ByProjectExcel } from "@/app/api/financialsummary/actions";
import { downloadFile } from "@/app/utils/commonUtil";
// import { summarizeFinancialData } from "./gptFn";

interface Props {
@@ -99,7 +101,8 @@ const FinancialStatusByProject: React.FC<Props> = ({
headerName: t("Cash Flow Status"),
minWidth: 80,
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>;
} else {
return <span className={redColor}>{t("Negative")}</span>;
@@ -112,7 +115,8 @@ const FinancialStatusByProject: React.FC<Props> = ({
headerName: "CPI",
minWidth: 50,
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 (
<span className={cpi >= 1 ? greenColor : redColor}>
{cpi.toLocaleString(undefined, {
@@ -129,7 +133,7 @@ const FinancialStatusByProject: React.FC<Props> = ({
headerName: t("Projected Cash Flow Status"),
minWidth: 100,
renderCell: (params: any) => {
var cumulativeExpenditure = params.row.projectExpense + params.row.invoicedAmount
var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense
if (params.row.totalFee >= cumulativeExpenditure) {
return <span className={greenColor}>{t("Positive")}</span>;
} else {
@@ -143,7 +147,8 @@ const FinancialStatusByProject: React.FC<Props> = ({
headerName: t("Projected CPI"),
minWidth: 50,
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 (
<span
className={(typeof projectedCpi == "number" && projectedCpi >= 1 ? greenColor : redColor)}
@@ -199,7 +204,7 @@ const FinancialStatusByProject: React.FC<Props> = ({
minWidth: 250,
type: "number",
renderCell: (params: any) => {
var cumulativeExpenditure = params.row.projectExpense + params.row.invoicedAmount
var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense
return (
<span>
$
@@ -272,7 +277,9 @@ const FinancialStatusByProject: React.FC<Props> = ({
minWidth: 250,
type: "number",
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 (
<span>
$
@@ -341,7 +348,7 @@ const FinancialStatusByProject: React.FC<Props> = ({
headerName: t("Cash Flow Status"),
minWidth: 100,
renderCell: (params: any) => {
var cumulativeExpenditure = params.row.projectExpense + params.row.invoicedAmount
var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense
return params.row.invoicedAmount >= cumulativeExpenditure ?
<span className={greenColor}>{t("Positive")}</span>
: <span className={redColor}>{t("Negative")}</span>
@@ -353,7 +360,7 @@ const FinancialStatusByProject: React.FC<Props> = ({
headerName: t("CPI"),
minWidth: 50,
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 cpiString = cpi.toLocaleString(undefined, {
minimumFractionDigits: 2,
@@ -370,8 +377,8 @@ const FinancialStatusByProject: React.FC<Props> = ({
headerName: t("Projected Cash Flow Status"),
minWidth: 100,
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 ?
<span className={greenColor}>{t("Positive")}</span>
: <span className={redColor}>{t("Negative")}</span>
@@ -383,7 +390,7 @@ const FinancialStatusByProject: React.FC<Props> = ({
headerName: t("Projected CPI"),
minWidth: 50,
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 projectCpiString = projectCpi.toLocaleString(undefined, {
minimumFractionDigits: 2,
@@ -439,7 +446,7 @@ const FinancialStatusByProject: React.FC<Props> = ({
minWidth: 280,
type: "number",
renderCell: (params: any) => {
var cumulativeExpenditure = params.row.projectExpense + params.row.invoicedAmount
var cumulativeExpenditure = params.row.projectExpense + params.row.manhourExpense
return (
<span>
$
@@ -512,11 +519,13 @@ const FinancialStatusByProject: React.FC<Props> = ({
minWidth: 250,
type: "number",
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 (
<span>
$
{uninvoiced.toLocaleString(undefined, {
{nonInvoiced.toLocaleString(undefined, {
minimumFractionDigits: 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 (
<>
<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} /> */}
</Card>
</Box>
<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>
</>
);


+ 6
- 0
src/components/ProjectFinancialSummaryV2/gptFn.tsx View File

@@ -8,6 +8,7 @@ export type SumOfByTeam = {
manhourExpense: number,
projectExpense: number,
invoicedAmount: number,
uninvoicedAmount: number,
paidAmount: number,
activeProject: number,
}
@@ -21,6 +22,7 @@ export type SumOfByClient = {
manhourExpense: number,
projectExpense: number,
invoicedAmount: number,
uninvoicedAmount: number,
paidAmount: number,
sumOfProjects: number,
}
@@ -36,6 +38,7 @@ export function sumUpByClient(data: FinancialByProject[]): SumOfByClient[] {
manhourExpense: 0,
projectExpense: 0,
invoicedAmount: 0,
uninvoicedAmount: 0,
paidAmount: 0,
sumOfProjects: 0
};
@@ -46,6 +49,7 @@ export function sumUpByClient(data: FinancialByProject[]): SumOfByClient[] {
acc[item.custId].manhourExpense += item.manhourExpense;
acc[item.custId].projectExpense += item.projectExpense;
acc[item.custId].invoicedAmount += item.invoicedAmount;
acc[item.custId].uninvoicedAmount += item.uninvoicedAmount;
acc[item.custId].paidAmount += item.paidAmount;
acc[item.custId].sumOfProjects += 1;

@@ -64,6 +68,7 @@ export function sumUpByTeam(data: FinancialByProject[]): SumOfByTeam[] {
manhourExpense: 0,
projectExpense: 0,
invoicedAmount: 0,
uninvoicedAmount: 0,
paidAmount: 0,
activeProject: 0
};
@@ -75,6 +80,7 @@ export function sumUpByTeam(data: FinancialByProject[]): SumOfByTeam[] {
acc[item.teamId].manhourExpense += item.manhourExpense;
acc[item.teamId].projectExpense += item.projectExpense;
acc[item.teamId].invoicedAmount += item.invoicedAmount;
acc[item.teamId].uninvoicedAmount += item.uninvoicedAmount;
acc[item.teamId].paidAmount += item.paidAmount;
acc[item.teamId].activeProject += 1;



+ 22
- 0
src/components/ProjectSearch/ProjectSearch.tsx View File

@@ -13,6 +13,7 @@ import { reverse, uniqBy } from "lodash";
import { loadDrafts } from "@/app/utils/draftUtils";
import { TeamResult } from "@/app/api/team";
import { Customer } from "@/app/api/customer";
import ContentCopyIcon from '@mui/icons-material/ContentCopy';

type ProjectResultOrDraft = ProjectResult & { isDraft?: boolean };

@@ -129,6 +130,17 @@ const ProjectSearch: React.FC<Props> = ({
[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>[]>(
() => [
{
@@ -138,6 +150,16 @@ const ProjectSearch: React.FC<Props> = ({
buttonIcon: <EditNote />,
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: "name", label: t("Project Name") },
{ name: "category", label: t("Project Category") },


+ 18
- 1
src/components/SearchResults/SearchResults.tsx View File

@@ -35,6 +35,7 @@ interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
onClick: (item: T) => void;
buttonIcon: React.ReactNode;
disabled?: boolean;
disabledRows?: { [columnName in keyof T]: string[] }; // Filter the row which is going to be disabled
}

export type Column<T extends ResultWithId> =
@@ -84,6 +85,22 @@ function SearchResults<T extends ResultWithId>({
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 = (
<>
<TableContainer sx={{ maxHeight: 440 }}>
@@ -112,7 +129,7 @@ function SearchResults<T extends ResultWithId>({
<IconButton
color={column.color ?? "primary"}
onClick={() => column.onClick(item)}
disabled={Boolean(column.disabled)}
disabled={Boolean(column.disabled) || Boolean(disabledRows(column, item))}
>
{column.buttonIcon}
</IconButton>


Loading…
Cancel
Save