update create claim minor update on timesheet inputtags/Baseline_30082024_FRONTEND_UAT
@@ -0,0 +1,11 @@ | |||
import { Metadata } from "next"; | |||
export const metadata: Metadata = { | |||
title: "Claim", | |||
}; | |||
const Claim: React.FC = async () => { | |||
return "Claim"; | |||
}; | |||
export default Claim; |
@@ -0,0 +1,21 @@ | |||
import CreateClaim from "@/components/CreateClaim"; | |||
import { getServerI18n } from "@/i18n"; | |||
import Typography from "@mui/material/Typography"; | |||
import { Metadata } from "next"; | |||
export const metadata: Metadata = { | |||
title: "Create Claim", | |||
}; | |||
const CreateClaims: React.FC = async () => { | |||
const { t } = await getServerI18n("claims"); | |||
return ( | |||
<> | |||
<Typography variant="h4">{t("Create Claim")}</Typography> | |||
<CreateClaim /> | |||
</> | |||
); | |||
}; | |||
export default CreateClaims; |
@@ -0,0 +1,47 @@ | |||
import { preloadClaims } from "@/app/api/claims"; | |||
import ClaimSearch from "@/components/ClaimSearch"; | |||
import { getServerI18n } from "@/i18n"; | |||
import Add from "@mui/icons-material/Add"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Typography from "@mui/material/Typography"; | |||
import { Metadata } from "next"; | |||
import Link from "next/link"; | |||
import { Suspense } from "react"; | |||
export const metadata: Metadata = { | |||
title: "Claims", | |||
}; | |||
const StaffReimbursement: React.FC = async () => { | |||
const { t } = await getServerI18n("claims"); | |||
preloadClaims(); | |||
return ( | |||
<> | |||
<Stack | |||
direction="row" | |||
justifyContent="space-between" | |||
flexWrap="wrap" | |||
rowGap={2} | |||
> | |||
<Typography variant="h4" marginInlineEnd={2}> | |||
{t("Staff Reimbursement")} | |||
</Typography> | |||
<Button | |||
variant="contained" | |||
startIcon={<Add />} | |||
LinkComponent={Link} | |||
href="/staffReimbursement/create" | |||
> | |||
{t("Create Claim")} | |||
</Button> | |||
</Stack> | |||
<Suspense fallback={<ClaimSearch.Loading />}> | |||
<ClaimSearch /> | |||
</Suspense> | |||
</> | |||
); | |||
}; | |||
export default StaffReimbursement; |
@@ -0,0 +1,50 @@ | |||
import { cache } from "react"; | |||
import "server-only"; | |||
export interface ClaimResult { | |||
id: number; | |||
created: string; | |||
name: string; | |||
cost: number; | |||
type: "Expense" | "Petty Cash"; | |||
status: "Not Submitted" | "Waiting for Approval" | "Approved" | "Rejected"; | |||
remarks: string; | |||
} | |||
export const preloadClaims = () => { | |||
fetchClaims(); | |||
}; | |||
export const fetchClaims = cache(async () => { | |||
return mockClaims; | |||
}); | |||
const mockClaims: ClaimResult[] = [ | |||
{ | |||
id: 1, | |||
created: "2023-11-22", | |||
name: "Consultancy Project A", | |||
cost: 121.00, | |||
type: "Expense", | |||
status: "Not Submitted", | |||
remarks: "", | |||
}, | |||
{ | |||
id: 2, | |||
created: "2023-11-30", | |||
name: "Consultancy Project A", | |||
cost: 4300.00, | |||
type: "Expense", | |||
status: "Waiting for Approval", | |||
remarks: "", | |||
}, | |||
{ | |||
id: 3, | |||
created: "2023-12-12", | |||
name: "Construction Project C", | |||
cost: 3675.00, | |||
type: "Petty Cash", | |||
status: "Rejected", | |||
remarks: "Duplicate Claim Form", | |||
}, | |||
]; |
@@ -0,0 +1,91 @@ | |||
"use client"; | |||
import { ClaimResult } from "@/app/api/claims"; | |||
import React, { useCallback, useMemo, useState } from "react"; | |||
import SearchBox, { Criterion } from "../SearchBox/index"; | |||
import { useTranslation } from "react-i18next"; | |||
import SearchResults, { Column } from "../SearchResults/index"; | |||
import EditNote from "@mui/icons-material/EditNote"; | |||
interface Props { | |||
claims: ClaimResult[]; | |||
} | |||
type SearchQuery = Partial<Omit<ClaimResult, "id">>; | |||
type SearchParamNames = keyof SearchQuery; | |||
const ClaimSearch: React.FC<Props> = ({ claims }) => { | |||
const { t } = useTranslation("claims"); | |||
// If claim searching is done on the server-side, then no need for this. | |||
const [filteredClaims, setFilteredClaims] = useState(claims); | |||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
() => [ | |||
{ label: t("Creation Date"), paramName: "created", type: "dateRange" }, | |||
{ label: t("Related Project Name"), paramName: "name", type: "text" }, | |||
{ | |||
label: t("Cost (HKD)"), | |||
paramName: "cost", | |||
type: "text", | |||
}, | |||
{ | |||
label: t("Expense Type"), | |||
paramName: "type", | |||
type: "select", | |||
options: ["Expense", "Petty Cash"], | |||
}, | |||
{ | |||
label: t("Status"), | |||
paramName: "status", | |||
type: "select", | |||
options: ["Not Submitted", "Waiting for Approval", "Approved", "Rejected"] | |||
}, | |||
{ | |||
label: t("Remarks"), | |||
paramName: "remarks", | |||
type: "text", | |||
}, | |||
], | |||
[t], | |||
); | |||
const onClaimClick = useCallback((claim: ClaimResult) => { | |||
console.log(claim); | |||
}, []); | |||
const columns = useMemo<Column<ClaimResult>[]>( | |||
() => [ | |||
{ | |||
name: "action", | |||
label: t("Actions"), | |||
onClick: onClaimClick, | |||
buttonIcon: <EditNote />, | |||
}, | |||
{ name: "created", label: t("Creation Date") }, | |||
{ name: "name", label: t("Related Project Name") }, | |||
{ name: "cost", label: t("Cost (HKD)") }, | |||
{ name: "type", label: t("Expense Type") }, | |||
{ name: "status", label: t("Status") }, | |||
{ name: "remarks", label: t("Remarks") }, | |||
], | |||
[t, onClaimClick], | |||
); | |||
return ( | |||
<> | |||
<SearchBox | |||
criteria={searchCriteria} | |||
onSearch={(query) => { | |||
console.log(query); | |||
}} | |||
/> | |||
<SearchResults<ClaimResult> | |||
items={filteredClaims} | |||
columns={columns} | |||
/> | |||
</> | |||
); | |||
}; | |||
export default ClaimSearch; |
@@ -0,0 +1,40 @@ | |||
import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import Skeleton from "@mui/material/Skeleton"; | |||
import Stack from "@mui/material/Stack"; | |||
import React from "react"; | |||
// Can make this nicer | |||
export const ClaimSearchLoading: React.FC = () => { | |||
return ( | |||
<> | |||
<Card> | |||
<CardContent> | |||
<Stack spacing={2}> | |||
<Skeleton variant="rounded" height={60} /> | |||
<Skeleton variant="rounded" height={60} /> | |||
<Skeleton variant="rounded" height={60} /> | |||
<Skeleton | |||
variant="rounded" | |||
height={50} | |||
width={100} | |||
sx={{ alignSelf: "flex-end" }} | |||
/> | |||
</Stack> | |||
</CardContent> | |||
</Card> | |||
<Card> | |||
<CardContent> | |||
<Stack spacing={2}> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
<Skeleton variant="rounded" height={40} /> | |||
</Stack> | |||
</CardContent> | |||
</Card> | |||
</> | |||
); | |||
}; | |||
export default ClaimSearchLoading; |
@@ -0,0 +1,18 @@ | |||
import { fetchClaims } from "@/app/api/claims"; | |||
import React from "react"; | |||
import ClaimSearch from "./ClaimSearch"; | |||
import ClaimSearchLoading from "./ClaimSearchLoading"; | |||
interface SubComponents { | |||
Loading: typeof ClaimSearchLoading; | |||
} | |||
const ClaimSearchWrapper: React.FC & SubComponents = async () => { | |||
const claims = await fetchClaims(); | |||
return <ClaimSearch claims={claims} />; | |||
}; | |||
ClaimSearchWrapper.Loading = ClaimSearchLoading; | |||
export default ClaimSearchWrapper; |
@@ -0,0 +1 @@ | |||
export { default } from "./ClaimSearchWrapper"; |
@@ -0,0 +1,79 @@ | |||
"use client"; | |||
import Stack from "@mui/material/Stack"; | |||
import Box from "@mui/material/Box"; | |||
import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import FormControl from "@mui/material/FormControl"; | |||
import Grid from "@mui/material/Grid"; | |||
import InputLabel from "@mui/material/InputLabel"; | |||
import MenuItem from "@mui/material/MenuItem"; | |||
import Select from "@mui/material/Select"; | |||
import TextField from "@mui/material/TextField"; | |||
import Typography from "@mui/material/Typography"; | |||
import { useTranslation } from "react-i18next"; | |||
import CardActions from "@mui/material/CardActions"; | |||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import Button from "@mui/material/Button"; | |||
import ClaimInputGrid from "./ClaimInputGrid"; | |||
const ClaimDetails: React.FC = () => { | |||
const { t } = useTranslation(); | |||
return ( | |||
<Card> | |||
<CardContent component={Stack} spacing={4}> | |||
<Box> | |||
{/* <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Related Project")} | |||
</Typography> */} | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
<InputLabel>{t("Related Project")}</InputLabel> | |||
<Select | |||
label={t("Project Category")} | |||
> | |||
<MenuItem value={"M1001"}> | |||
{t("M1001")} | |||
</MenuItem> | |||
<MenuItem value={"M1301"}> | |||
{t("M1301")} | |||
</MenuItem> | |||
<MenuItem value={"M1354"}> | |||
{t("M1354")} | |||
</MenuItem> | |||
</Select> | |||
</FormControl> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
<InputLabel>{t("Expense Type")}</InputLabel> | |||
<Select label={t("Team Lead")}> | |||
<MenuItem value={"Petty Cash"}> | |||
{"Petty Cash"} | |||
</MenuItem> | |||
<MenuItem value={"Expense"}> | |||
{"Expense"} | |||
</MenuItem> | |||
</Select> | |||
</FormControl> | |||
</Grid> | |||
</Grid> | |||
</Box> | |||
<Card> | |||
<ClaimInputGrid/> | |||
</Card> | |||
{/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button variant="text" startIcon={<RestartAlt />}> | |||
{t("Reset")} | |||
</Button> | |||
</CardActions> */} | |||
</CardContent> | |||
</Card> | |||
); | |||
}; | |||
export default ClaimDetails; |
@@ -0,0 +1,406 @@ | |||
"use client"; | |||
import Grid from "@mui/material/Grid"; | |||
import Paper from "@mui/material/Paper"; | |||
import { useState, useEffect } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import PageTitle from "../PageTitle/PageTitle"; | |||
import { Suspense } from "react"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Link from "next/link"; | |||
import { t } from 'i18next'; | |||
import { Box, Container, Modal, Select, SelectChangeEvent, Typography } from "@mui/material"; | |||
import { Close } from '@mui/icons-material'; | |||
import AddIcon from '@mui/icons-material/Add'; | |||
import EditIcon from '@mui/icons-material/Edit'; | |||
import DeleteIcon from '@mui/icons-material/DeleteOutlined'; | |||
import SaveIcon from '@mui/icons-material/Save'; | |||
import CancelIcon from '@mui/icons-material/Close'; | |||
import AddPhotoAlternateOutlinedIcon from '@mui/icons-material/AddPhotoAlternateOutlined'; | |||
import ImageNotSupportedOutlinedIcon from '@mui/icons-material/ImageNotSupportedOutlined'; | |||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; | |||
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | |||
import Swal from "sweetalert2"; | |||
import { msg } from "../Swal/CustomAlerts"; | |||
import React from "react"; | |||
import { DatePicker } from '@mui/x-date-pickers/DatePicker'; | |||
import { | |||
GridRowsProp, | |||
GridRowModesModel, | |||
GridRowModes, | |||
DataGrid, | |||
GridColDef, | |||
GridToolbarContainer, | |||
GridFooterContainer, | |||
GridActionsCellItem, | |||
GridEventListener, | |||
GridRowId, | |||
GridRowModel, | |||
GridRowEditStopReasons, | |||
GridEditInputCell, | |||
GridValueSetterParams, | |||
} from '@mui/x-data-grid'; | |||
import { LocalizationProvider } from "@mui/x-date-pickers"; | |||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
import dayjs from "dayjs"; | |||
import { Props } from "react-intl/src/components/relative"; | |||
import palette from "@/theme/devias-material-kit/palette"; | |||
const weekdays = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; | |||
interface BottomBarProps { | |||
getCostTotal: () => number; | |||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||
setRowModesModel: ( | |||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||
) => void; | |||
} | |||
interface EditToolbarProps { | |||
// setDay: (newDay : dayjs.Dayjs) => void; | |||
setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; | |||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||
setRowModesModel: ( | |||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||
) => void; | |||
} | |||
interface EditFooterProps { | |||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||
setRowModesModel: ( | |||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||
) => void; | |||
} | |||
const BottomBar = (props: BottomBarProps) => { | |||
const { setRows, setRowModesModel, getCostTotal } = props; | |||
// const getCostTotal = props.getCostTotal; | |||
const [newId, setNewId] = useState(-1); | |||
const [invalidDays, setInvalidDays] = useState(0); | |||
const handleAddClick = () => { | |||
const id = newId; | |||
setNewId(newId - 1); | |||
setRows((oldRows) => [...oldRows, { id, projectCode: '', task: '', isNew: true }]); | |||
setRowModesModel((oldModel) => ({ | |||
...oldModel, | |||
[id]: { mode: GridRowModes.Edit, fieldToFocus: 'projectCode' }, | |||
})); | |||
}; | |||
const totalColDef = { | |||
flex:1, | |||
// style: {color:getCostTotal('mon')>24?"red":"black"} | |||
}; | |||
const TotalCell = ({value}: Props) => { | |||
const [invalid, setInvalid] = useState(false); | |||
useEffect(()=> { | |||
const newInvalid = (value ?? 0) < 0; | |||
setInvalid(newInvalid); | |||
}, [value]); | |||
return ( | |||
<Box flex={1} style={{color: invalid?"red":"black"}}> | |||
$ {value} | |||
</Box> | |||
); | |||
} | |||
return ( | |||
<div> | |||
<div style={{ display: 'flex', justifyContent: 'flex', width: '100%' }}> | |||
<Box flex={1.5} textAlign={'right'} marginRight='4rem'> | |||
<b>Total:</b> | |||
</Box> | |||
<TotalCell value={getCostTotal()}/> | |||
</div> | |||
<Button variant="outlined" color="primary" startIcon={<AddIcon />} onClick={handleAddClick} sx={{margin:'20px'}}> | |||
Add record | |||
</Button> | |||
</div> | |||
); | |||
} | |||
const EditFooter = (props: EditFooterProps) => { | |||
return ( | |||
<div style={{ display: 'flex', justifyContent: 'flex', width: '100%' }}> | |||
<Box flex={1}> | |||
<b>Total: </b> | |||
</Box> | |||
<Box flex={2}>test</Box> | |||
</div> | |||
); | |||
} | |||
interface ClaimInputGridProps { | |||
onClose?: () => void; | |||
} | |||
const initialRows: GridRowsProp = [ | |||
{ | |||
id: 1, | |||
date: new Date(), | |||
description: "Taxi to client office", | |||
cost: 169.5, | |||
document: 'taxi_receipt.jpg', | |||
}, | |||
{ | |||
id: 2, | |||
date: dayjs().add(-14, 'days').toDate(), | |||
description: "MTR fee to Kowloon Bay Office", | |||
cost: 15.5, | |||
document: 'octopus_invoice.jpg', | |||
}, | |||
{ | |||
id: 3, | |||
date: dayjs().add(-44, 'days').toDate(), | |||
description: "Starbucks", | |||
cost: 504, | |||
}, | |||
]; | |||
const ClaimInputGrid: React.FC<ClaimInputGridProps> = ({ ...props }) => { | |||
const [rows, setRows] = useState(initialRows); | |||
const [day, setDay] = useState(dayjs()); | |||
const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>({}); | |||
const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => { | |||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
event.defaultMuiPrevented = true; | |||
} | |||
}; | |||
const handleEditClick = (id: GridRowId) => () => { | |||
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); | |||
}; | |||
const handleSaveClick = (id: GridRowId) => () => { | |||
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); | |||
}; | |||
const handleDeleteClick = (id: GridRowId) => () => { | |||
setRows(rows.filter((row) => row.id !== id)); | |||
}; | |||
const handleCancelClick = (id: GridRowId) => () => { | |||
setRowModesModel({ | |||
...rowModesModel, | |||
[id]: { mode: GridRowModes.View, ignoreModifications: true }, | |||
}); | |||
const editedRow = rows.find((row) => row.id === id); | |||
if (editedRow!.isNew) { | |||
setRows(rows.filter((row) => row.id !== id)); | |||
} | |||
}; | |||
const processRowUpdate = (newRow: GridRowModel) => { | |||
const updatedRow = { ...newRow, isNew: false }; | |||
setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); | |||
return updatedRow; | |||
}; | |||
const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { | |||
setRowModesModel(newRowModesModel); | |||
}; | |||
const getCostTotal = () => { | |||
let sum = 0; | |||
rows.forEach((row) => { | |||
sum += row['cost']??0; | |||
}); | |||
return sum; | |||
}; | |||
const commonGridColConfig : any = { | |||
type: 'number', | |||
// sortable: false, | |||
//width: 100, | |||
flex: 1, | |||
align: 'left', | |||
headerAlign: 'left', | |||
// headerClassName: 'header', | |||
editable: true, | |||
renderEditCell: (value : any) => ( | |||
<GridEditInputCell | |||
{...value} | |||
inputProps={{ | |||
max: 24, | |||
min: 0, | |||
step: 0.25, | |||
}} | |||
/> | |||
), | |||
}; | |||
const columns: GridColDef[] = [ | |||
{ | |||
field: 'actions', | |||
type: 'actions', | |||
headerName: 'Actions', | |||
width: 100, | |||
cellClassName: 'actions', | |||
getActions: ({ id }) => { | |||
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||
if (isInEditMode) { | |||
return [ | |||
<GridActionsCellItem | |||
icon={<SaveIcon />} | |||
title="Save" | |||
label="Save" | |||
sx={{ | |||
color: 'primary.main', | |||
}} | |||
onClick={handleSaveClick(id)} | |||
/>, | |||
<GridActionsCellItem | |||
icon={<CancelIcon />} | |||
title="Cancel" | |||
label="Cancel" | |||
className="textPrimary" | |||
onClick={handleCancelClick(id)} | |||
color="inherit" | |||
/>, | |||
]; | |||
} | |||
return [ | |||
<GridActionsCellItem | |||
icon={<EditIcon />} | |||
title="Edit" | |||
label="Edit" | |||
className="textPrimary" | |||
onClick={handleEditClick(id)} | |||
color="inherit" | |||
/>, | |||
<GridActionsCellItem | |||
title="Delete" | |||
label="Delete" | |||
icon={<DeleteIcon />} | |||
onClick={handleDeleteClick(id)} | |||
sx={{color:"red"}} | |||
/>, | |||
]; | |||
}, | |||
}, | |||
{ | |||
field: 'date', | |||
headerName: 'Invoice Date', | |||
// width: 220, | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
{ | |||
field: 'description', | |||
headerName: 'Description', | |||
// width: 220, | |||
flex: 2, | |||
editable: true, | |||
type: 'string', | |||
}, | |||
{ | |||
field: 'cost', | |||
headerName: 'Cost (HKD)', | |||
editable: true, | |||
type: 'number', | |||
valueFormatter: (params) => { | |||
return `$ ${params.value??0}`; | |||
}, | |||
}, | |||
{ | |||
field: 'document', | |||
headerName: 'Supporting Document', | |||
type: 'string', | |||
editable: true, | |||
flex: 2, | |||
renderCell: (params) => { | |||
return params.value? | |||
( | |||
<span> | |||
<a href="" target="_blank" rel="noopener noreferrer"> | |||
{params.value} | |||
</a> | |||
</span> | |||
) : | |||
(<span style={{color: palette.text.disabled}}>No Documents</span>) | |||
}, | |||
renderEditCell: (params) => { | |||
return params.value? | |||
( | |||
<span> | |||
<a href="" target="_blank" rel="noopener noreferrer"> | |||
{params.value} | |||
</a> | |||
<Button title='Remove Document' onClick={(event) => console.log(event)}> | |||
<ImageNotSupportedOutlinedIcon sx={{fontSize: '25px', color:"red"}}/> | |||
</Button> | |||
</span> | |||
) : ( | |||
<Button title='Add Document'> | |||
<AddPhotoAlternateOutlinedIcon sx={{fontSize: '25px', color:"green"}}/> | |||
</Button> | |||
) | |||
}, | |||
}, | |||
]; | |||
return ( | |||
<Box | |||
sx={{ | |||
// marginBottom: '-5px', | |||
display: 'flex', | |||
'flex-direction': 'column', | |||
// 'justify-content': 'flex-end', | |||
height: '100%',//'25rem', | |||
width: '100%', | |||
'& .actions': { | |||
color: 'text.secondary', | |||
}, | |||
'& .header': { | |||
backgroundColor: "#F8F9FA", | |||
// border: 1, | |||
// 'border-width': '1px', | |||
// 'border-color': 'grey', | |||
}, | |||
'& .textPrimary': { | |||
color: 'text.primary', | |||
}, | |||
}} | |||
> | |||
<DataGrid | |||
sx={{flex:1}} | |||
rows={rows} | |||
columns={columns} | |||
editMode="row" | |||
rowModesModel={rowModesModel} | |||
onRowModesModelChange={handleRowModesModelChange} | |||
onRowEditStop={handleRowEditStop} | |||
processRowUpdate={processRowUpdate} | |||
disableRowSelectionOnClick={true} | |||
disableColumnMenu={true} | |||
hideFooterPagination={true} | |||
slots={{ | |||
// footer: EditFooter, | |||
}} | |||
slotProps={{ | |||
// footer: { setDay, setRows, setRowModesModel }, | |||
}} | |||
initialState={{ | |||
pagination: { paginationModel: { pageSize: 100 } }, | |||
}} | |||
/> | |||
<BottomBar getCostTotal={getCostTotal} setRows={setRows} setRowModesModel={setRowModesModel} | |||
sx={{flex:2}}/> | |||
</Box> | |||
); | |||
} | |||
export default ClaimInputGrid; |
@@ -0,0 +1,48 @@ | |||
"use client"; | |||
import Check from "@mui/icons-material/Check"; | |||
import Close from "@mui/icons-material/Close"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Tab from "@mui/material/Tab"; | |||
import Tabs, { TabsProps } from "@mui/material/Tabs"; | |||
import { useRouter } from "next/navigation"; | |||
import React, { useCallback, useState } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import ClaimProjectDetails from "./ClaimDetails"; | |||
import TaskSetup from "./TaskSetup"; | |||
import StaffAllocation from "./StaffAllocation"; | |||
import ResourceMilestone from "./ResourceMilestone"; | |||
const CreateProject: React.FC = () => { | |||
const [tabIndex, setTabIndex] = useState(0); | |||
const { t } = useTranslation(); | |||
const router = useRouter(); | |||
const handleCancel = () => { | |||
router.back(); | |||
}; | |||
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
(_e, newValue) => { | |||
setTabIndex(newValue); | |||
}, | |||
[], | |||
); | |||
return ( | |||
<> | |||
<ClaimProjectDetails /> | |||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||
<Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | |||
{t("Cancel")} | |||
</Button> | |||
<Button variant="contained" startIcon={<Check />}> | |||
{t("Confirm")} | |||
</Button> | |||
</Stack> | |||
</> | |||
); | |||
}; | |||
export default CreateProject; |
@@ -0,0 +1 @@ | |||
export { default } from "./CreateClaim"; |
@@ -11,7 +11,7 @@ import Stack from "@mui/material/Stack"; | |||
import { Add } from '@mui/icons-material'; | |||
import Link from "next/link"; | |||
import { t } from 'i18next'; | |||
import { Modal, Typography } from "@mui/material"; | |||
import { Card, Modal, Typography } from "@mui/material"; | |||
import CustomModal from "../CustomModal/CustomModal"; | |||
import { PROJECT_MODAL_STYLE } from "@/theme/colorConst"; | |||
import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; | |||
@@ -51,15 +51,15 @@ const EnterTimesheetModal: React.FC<EnterTimesheetModalProps> = ({ ...props }) = | |||
return ( | |||
<Modal open={props.isOpen} onClose={props.onClose}> | |||
<div style={PROJECT_MODAL_STYLE}> | |||
<Typography variant="h6" id="modal-title" sx={{flex:1}}> | |||
{/* <Typography variant="h5" id="modal-title" sx={{flex:1}}> | |||
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}> | |||
Timesheet Input | |||
</div> | |||
</Typography> | |||
</Typography> */} | |||
<div style={{flex: 10}}> | |||
<Card style={{flex: 10, marginBottom:'20px'}}> | |||
<TimesheetInputGrid setLockConfirm={setLockConfirm}/> | |||
</div> | |||
</Card> | |||
<div style={{ | |||
display: 'flex', justifyContent: 'space-between', width: '100%', flex: 1 | |||
@@ -1,62 +0,0 @@ | |||
"use client"; | |||
import * as React from "react"; | |||
import Grid from "@mui/material/Grid"; | |||
import { useEffect, useState } from 'react' | |||
import { TFunction } from "i18next"; | |||
import { useTranslation } from "react-i18next"; | |||
import {Card,CardContent,CardHeader} from '@mui/material'; | |||
import CustomCardGrid from '../CustomCardGrid/CustomCardGrid'; | |||
import '../../app/global.css'; | |||
import { PROJECT_CARD_STYLE } from "@/theme/colorConst"; | |||
interface ProjectGridProps { | |||
tab: number; | |||
} | |||
const ProjectGrid: React.FC<ProjectGridProps> = (props) => { | |||
const [items, setItems] = React.useState<Object[]>([]) | |||
useEffect(() => { | |||
if (props.tab == 0) { | |||
setItems(cards) | |||
} | |||
else { | |||
const filteredItems = cards;//cards.filter(item => (item.track == props.tab)); | |||
setItems(filteredItems); | |||
} | |||
}, [props.tab]); | |||
const cards = [ | |||
{code: 'M1001 (C)', name: 'Consultancy Project A', hr_spent: 12.75, hr_spent_normal: 0.00, hr_alloc: 150.00, hr_alloc_normal: 30.00}, | |||
{code: 'M1301 (C)', name: 'Consultancy Project AAA', hr_spent: 4.25, hr_spent_normal: 0.25, hr_alloc: 30.00, hr_alloc_normal: 0.00}, | |||
{code: 'M1354 (C)', name: 'Consultancy Project BBB', hr_spent: 57.00, hr_spent_normal: 6.50, hr_alloc: 100.00, hr_alloc_normal: 20.00}, | |||
{code: 'M1973 (C)', name: 'Construction Project CCC', hr_spent: 12.75, hr_spent_normal: 0.00, hr_alloc: 150.00, hr_alloc_normal: 30.00}, | |||
{code: 'M2014 (T)', name: 'Consultancy Project DDD', hr_spent: 1.00, hr_spent_normal: 0.00, hr_alloc: 10.00, hr_alloc_normal: 0.00}, | |||
]; | |||
const cardLayout = (item: Record<string, string>) => { | |||
return ( | |||
<Card style={PROJECT_CARD_STYLE}> | |||
<CardHeader style={{backgroundColor:'red'}} title={item.code + '\u000A' + item.name}/> | |||
<CardContent> | |||
<p>Hours Spent: {item.hr_spent}</p> | |||
<p>Normal (Others): {item.hr_spent_normal}</p> | |||
<p>Hours Allocated: {item.hr_alloc}</p> | |||
<p>Normal (Others): {item.hr_alloc_normal}</p> | |||
</CardContent> | |||
</Card> | |||
); | |||
} | |||
// Apply the preset style to the cards in child, if not specified // | |||
return ( | |||
<Grid container md={12} style={{backgroundColor:"yellow"}}> | |||
{/* <CustomSearchForm applySearch={applySearch} fields={InputFields}/> */} | |||
{/* item count = {items?.length??"idk"} , track/tab = {props.tab} */} | |||
<CustomCardGrid Title={props.tab.toString()} items={items} cardStyle={cardLayout}/> | |||
{/* <CustomCardGrid Title={props.tab.toString()} rows={rows} columns={columns} columnWidth={200} items={items}/> */} | |||
</Grid> | |||
); | |||
}; | |||
export default ProjectGrid; |
@@ -7,7 +7,6 @@ import PageTitle from "../PageTitle/PageTitle"; | |||
import { Suspense } from "react"; | |||
import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import { Add, SettingsEthernet } from '@mui/icons-material'; | |||
import Link from "next/link"; | |||
import { t } from 'i18next'; | |||
import { Box, Container, Modal, Select, SelectChangeEvent, Typography } from "@mui/material"; | |||
@@ -21,7 +20,6 @@ import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; | |||
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | |||
import Swal from "sweetalert2"; | |||
import { msg } from "../Swal/CustomAlerts"; | |||
import ComboEditor from "../ComboEditor/ComboEditor"; | |||
import React from "react"; | |||
import { DatePicker } from '@mui/x-date-pickers/DatePicker'; | |||
import { | |||
@@ -72,6 +70,53 @@ interface EditFooterProps { | |||
) => void; | |||
} | |||
const EditToolbar = (props: EditToolbarProps) => { | |||
const { setDay } = props; | |||
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs()); | |||
const handleClickLeft = () => { | |||
if (selectedDate) { | |||
const newDate = selectedDate.add(-7, 'day'); | |||
setSelectedDate(newDate); | |||
} | |||
}; | |||
const handleClickRight = () => { | |||
if (selectedDate) { | |||
const newDate = selectedDate.add(7, 'day') > dayjs()? dayjs(): selectedDate.add(7, 'day'); | |||
setSelectedDate(newDate); | |||
} | |||
}; | |||
const handleDateChange = (date: dayjs.Dayjs | Date | null) => { | |||
const newDate = dayjs(date); | |||
setSelectedDate(newDate); | |||
}; | |||
useEffect(() => { | |||
setDay((oldDay) => selectedDate); | |||
}, [selectedDate]); | |||
return ( | |||
<LocalizationProvider dateAdapter={AdapterDayjs}> | |||
<div style={{ display: 'flex', justifyContent: 'flex-end', width: '100%', paddingBottom:'20px'}}> | |||
<Typography variant="h5" id="modal-title" sx={{flex:1}}> | |||
Timesheet Input | |||
</Typography> | |||
<Button sx={{"border-radius":"30%", marginRight:'20px'}} variant="contained" onClick={handleClickLeft}> | |||
<ArrowBackIcon/> | |||
</Button> | |||
<DatePicker | |||
value={selectedDate} | |||
onChange={handleDateChange} | |||
disableFuture={true}/> | |||
<Button sx={{"border-radius":"30%", margin:'0px 20px 0px 20px'}} variant="contained" onClick={handleClickRight}> | |||
<ArrowForwardIcon/> | |||
</Button> | |||
</div> | |||
</LocalizationProvider> | |||
); | |||
} | |||
const BottomBar = (props: BottomBarProps) => { | |||
const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props; | |||
// const getHoursTotal = props.getHoursTotal; | |||
@@ -136,49 +181,6 @@ const BottomBar = (props: BottomBarProps) => { | |||
); | |||
} | |||
const EditToolbar = (props: EditToolbarProps) => { | |||
const { setDay } = props; | |||
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs()); | |||
const handleClickLeft = () => { | |||
if (selectedDate) { | |||
const newDate = selectedDate.add(-7, 'day'); | |||
setSelectedDate(newDate); | |||
} | |||
}; | |||
const handleClickRight = () => { | |||
if (selectedDate) { | |||
const newDate = selectedDate.add(7, 'day') > dayjs()? dayjs(): selectedDate.add(7, 'day'); | |||
setSelectedDate(newDate); | |||
} | |||
}; | |||
const handleDateChange = (date: dayjs.Dayjs | Date | null) => { | |||
const newDate = dayjs(date); | |||
setSelectedDate(newDate); | |||
}; | |||
useEffect(() => { | |||
setDay((oldDay) => selectedDate); | |||
}, [selectedDate]); | |||
return ( | |||
<LocalizationProvider dateAdapter={AdapterDayjs}> | |||
<div style={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}> | |||
<Button sx={{"border-radius":"30%", marginRight:'20px'}} variant="contained" onClick={handleClickLeft}> | |||
<ArrowBackIcon/> | |||
</Button> | |||
<DatePicker | |||
value={selectedDate} | |||
onChange={handleDateChange} | |||
disableFuture={true}/> | |||
<Button sx={{"border-radius":"30%", margin:'0px 20px 0px 20px'}} variant="contained" onClick={handleClickRight}> | |||
<ArrowForwardIcon/> | |||
</Button> | |||
</div> | |||
</LocalizationProvider> | |||
); | |||
} | |||
const EditFooter = (props: EditFooterProps) => { | |||
return ( | |||
@@ -307,7 +309,7 @@ const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({ ...props }) => | |||
{ | |||
field: 'actions', | |||
type: 'actions', | |||
headerName: '', | |||
headerName: 'Actions', | |||
width: 100, | |||
cellClassName: 'actions', | |||
getActions: ({ id }) => { | |||
@@ -455,7 +457,11 @@ const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({ ...props }) => | |||
<Box | |||
sx={{ | |||
// marginBottom: '-5px', | |||
height: '30rem', | |||
display: 'flex', | |||
'flex-direction': 'column', | |||
// 'justify-content': 'flex-end', | |||
padding: '20px', | |||
height: '100%',//'30rem', | |||
width: '100%', | |||
'& .actions': { | |||
color: 'text.secondary', | |||
@@ -492,9 +498,11 @@ const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({ ...props }) => | |||
initialState={{ | |||
pagination: { paginationModel: { pageSize: 100 } }, | |||
}} | |||
sx={{flex:1}} | |||
/> | |||
<BottomBar getHoursTotal={getHoursTotal} setRows={setRows} setRowModesModel={setRowModesModel} setLockConfirm={setLockConfirm}/> | |||
<BottomBar getHoursTotal={getHoursTotal} setRows={setRows} setRowModesModel={setRowModesModel} setLockConfirm={setLockConfirm} | |||
sx={{flex:3}}/> | |||
</Box> | |||
); | |||
} | |||
@@ -35,7 +35,10 @@ const navigationItems: NavigationItem[] = [ | |||
{ icon: <Dashboard />, label: "Project Cash Flow", path: "/dashboard/ProjectCashFlow" }, | |||
{ icon: <Dashboard />, label: "Project Status by Client", path: "/dashboard/ProjectStatusByClient" }, | |||
]}, | |||
{ icon: <RequestQuote />, label: "Expense Claim", path: "/claim" }, | |||
{ icon: <RequestQuote />, label: "Staff Reimbursement", path: "/staffReimbursement", children: [ | |||
{ icon: <RequestQuote />, label: "ClaimApproval", path: "/staffReimbursement/ClaimApproval"}, | |||
{ icon: <RequestQuote />, label: "ClaimSummary", path: "/staffReimbursement/ClaimSummary"} | |||
] }, | |||
{ icon: <Assignment />, label: "Project Management", path: "/projects" }, | |||
{ icon: <Task />, label: "Task Template", path: "/tasks" }, | |||
{ icon: <Payments />, label: "Invoice", path: "/invoice" }, | |||
@@ -38,7 +38,7 @@ const ProjectGrid: React.FC<ProjectGridProps> = (props) => { | |||
const cardLayout = (item: Record<string, string>) => { | |||
return ( | |||
<Card style={PROJECT_CARD_STYLE}> | |||
<CardHeader style={{backgroundColor:'red'}} title={item.code + '\u000A' + item.name}/> | |||
<CardHeader style={{backgroundColor:'pink'}} title={item.code + '\u000A' + item.name}/> | |||
<CardContent> | |||
<p>Hours Spent: {item.hr_spent}</p> | |||
<p>Normal (Others): {item.hr_spent_normal}</p> | |||
@@ -78,6 +78,18 @@ export const PROJECT_MODAL_STYLE = { | |||
flexDirection: 'column', | |||
}; | |||
export const DATAGRID_STYLE = { | |||
boxShadow: 2, | |||
border: 2, | |||
borderColor: 'primary.light', | |||
'& .MuiDataGrid-cell:hover': { | |||
color: 'primary.main' | |||
}, | |||
'& .MuiDataGrid-root': { | |||
overflow: 'auto', | |||
} | |||
}; | |||
export const TAB_THEME = { | |||
components: { | |||
MuiTab: { | |||