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 { Add } from '@mui/icons-material'; | ||||
| import Link from "next/link"; | import Link from "next/link"; | ||||
| import { t } from 'i18next'; | import { t } from 'i18next'; | ||||
| import { Modal, Typography } from "@mui/material"; | |||||
| import { Card, Modal, Typography } from "@mui/material"; | |||||
| import CustomModal from "../CustomModal/CustomModal"; | import CustomModal from "../CustomModal/CustomModal"; | ||||
| import { PROJECT_MODAL_STYLE } from "@/theme/colorConst"; | import { PROJECT_MODAL_STYLE } from "@/theme/colorConst"; | ||||
| import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; | import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; | ||||
| @@ -51,15 +51,15 @@ const EnterTimesheetModal: React.FC<EnterTimesheetModalProps> = ({ ...props }) = | |||||
| return ( | return ( | ||||
| <Modal open={props.isOpen} onClose={props.onClose}> | <Modal open={props.isOpen} onClose={props.onClose}> | ||||
| <div style={PROJECT_MODAL_STYLE}> | <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%' }}> | <div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}> | ||||
| Timesheet Input | Timesheet Input | ||||
| </div> | </div> | ||||
| </Typography> | |||||
| </Typography> */} | |||||
| <div style={{flex: 10}}> | |||||
| <Card style={{flex: 10, marginBottom:'20px'}}> | |||||
| <TimesheetInputGrid setLockConfirm={setLockConfirm}/> | <TimesheetInputGrid setLockConfirm={setLockConfirm}/> | ||||
| </div> | |||||
| </Card> | |||||
| <div style={{ | <div style={{ | ||||
| display: 'flex', justifyContent: 'space-between', width: '100%', flex: 1 | 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 { Suspense } from "react"; | ||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
| import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
| import { Add, SettingsEthernet } from '@mui/icons-material'; | |||||
| import Link from "next/link"; | import Link from "next/link"; | ||||
| import { t } from 'i18next'; | import { t } from 'i18next'; | ||||
| import { Box, Container, Modal, Select, SelectChangeEvent, Typography } from "@mui/material"; | 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 ArrowBackIcon from '@mui/icons-material/ArrowBack'; | ||||
| import Swal from "sweetalert2"; | import Swal from "sweetalert2"; | ||||
| import { msg } from "../Swal/CustomAlerts"; | import { msg } from "../Swal/CustomAlerts"; | ||||
| import ComboEditor from "../ComboEditor/ComboEditor"; | |||||
| import React from "react"; | import React from "react"; | ||||
| import { DatePicker } from '@mui/x-date-pickers/DatePicker'; | import { DatePicker } from '@mui/x-date-pickers/DatePicker'; | ||||
| import { | import { | ||||
| @@ -72,6 +70,53 @@ interface EditFooterProps { | |||||
| ) => void; | ) => 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 BottomBar = (props: BottomBarProps) => { | ||||
| const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props; | const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props; | ||||
| // const getHoursTotal = props.getHoursTotal; | // 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) => { | const EditFooter = (props: EditFooterProps) => { | ||||
| return ( | return ( | ||||
| @@ -307,7 +309,7 @@ const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({ ...props }) => | |||||
| { | { | ||||
| field: 'actions', | field: 'actions', | ||||
| type: 'actions', | type: 'actions', | ||||
| headerName: '', | |||||
| headerName: 'Actions', | |||||
| width: 100, | width: 100, | ||||
| cellClassName: 'actions', | cellClassName: 'actions', | ||||
| getActions: ({ id }) => { | getActions: ({ id }) => { | ||||
| @@ -455,7 +457,11 @@ const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({ ...props }) => | |||||
| <Box | <Box | ||||
| sx={{ | sx={{ | ||||
| // marginBottom: '-5px', | // marginBottom: '-5px', | ||||
| height: '30rem', | |||||
| display: 'flex', | |||||
| 'flex-direction': 'column', | |||||
| // 'justify-content': 'flex-end', | |||||
| padding: '20px', | |||||
| height: '100%',//'30rem', | |||||
| width: '100%', | width: '100%', | ||||
| '& .actions': { | '& .actions': { | ||||
| color: 'text.secondary', | color: 'text.secondary', | ||||
| @@ -492,9 +498,11 @@ const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({ ...props }) => | |||||
| initialState={{ | initialState={{ | ||||
| pagination: { paginationModel: { pageSize: 100 } }, | 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> | </Box> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -35,7 +35,10 @@ const navigationItems: NavigationItem[] = [ | |||||
| { icon: <Dashboard />, label: "Project Cash Flow", path: "/dashboard/ProjectCashFlow" }, | { icon: <Dashboard />, label: "Project Cash Flow", path: "/dashboard/ProjectCashFlow" }, | ||||
| { icon: <Dashboard />, label: "Project Status by Client", path: "/dashboard/ProjectStatusByClient" }, | { 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: <Assignment />, label: "Project Management", path: "/projects" }, | ||||
| { icon: <Task />, label: "Task Template", path: "/tasks" }, | { icon: <Task />, label: "Task Template", path: "/tasks" }, | ||||
| { icon: <Payments />, label: "Invoice", path: "/invoice" }, | { icon: <Payments />, label: "Invoice", path: "/invoice" }, | ||||
| @@ -38,7 +38,7 @@ const ProjectGrid: React.FC<ProjectGridProps> = (props) => { | |||||
| const cardLayout = (item: Record<string, string>) => { | const cardLayout = (item: Record<string, string>) => { | ||||
| return ( | return ( | ||||
| <Card style={PROJECT_CARD_STYLE}> | <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> | <CardContent> | ||||
| <p>Hours Spent: {item.hr_spent}</p> | <p>Hours Spent: {item.hr_spent}</p> | ||||
| <p>Normal (Others): {item.hr_spent_normal}</p> | <p>Normal (Others): {item.hr_spent_normal}</p> | ||||
| @@ -78,6 +78,18 @@ export const PROJECT_MODAL_STYLE = { | |||||
| flexDirection: 'column', | 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 = { | export const TAB_THEME = { | ||||
| components: { | components: { | ||||
| MuiTab: { | MuiTab: { | ||||