# 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 { 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) | |||
} |
@@ -33,6 +33,7 @@ export type FinancialByProject = { | |||
totalBudget: number, | |||
manhourExpense: number, | |||
invoicedAmount: number, | |||
uninvoicedAmount: number, | |||
paidAmount: number, | |||
projectExpense: number, | |||
} | |||
@@ -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 || | |||
@@ -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} | |||
@@ -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) | |||
, | |||
}, | |||
}} | |||
@@ -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 | |||
// { | |||
@@ -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> | |||
@@ -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> | |||
</> | |||
); | |||
@@ -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; | |||
@@ -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") }, | |||
@@ -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> | |||