Added formatting for jobs lists and jobs detail components

This commit is contained in:
Patrick Fic
2020-10-14 22:37:49 -07:00
parent 76f8a17b92
commit 0456543574
24 changed files with 616 additions and 30 deletions

View File

@@ -10,6 +10,7 @@ import client from "../graphql/GraphQLClient";
import "../ipc/ipc-renderer-handler";
import { checkUserSession } from "../redux/user/user.actions";
import { selectCurrentUser } from "../redux/user/user.selectors";
import "./App.styles.scss";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,

View File

@@ -0,0 +1,52 @@
body {
height: 100%;
overflow: hidden;
}
.imex-flex-row {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
align-items: center;
&__grow {
flex: 1;
}
&__margin {
margin: 0.2rem 0.2rem;
}
&__margin-large {
margin: 0.5rem 0.5rem;
}
&__flex-space-around {
justify-content: space-around;
}
}
.ellipses {
display: inline-block; /* for em, a, span, etc (inline by default) */
text-overflow: ellipsis;
width: calc(95%);
overflow: hidden;
white-space: nowrap;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 0.2rem;
background-color: #f5f5f5;
}
::-webkit-scrollbar {
width: 0.25rem;
max-height: 0.25rem;
background-color: #f5f5f5;
}
::-webkit-scrollbar-thumb {
border-radius: 0.2rem;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
background-color: #188fff;
}

View File

@@ -0,0 +1,6 @@
import React from "react";
import Dinero from "dinero.js";
export default function CurrencyFormatterAtom({ children, ...restProps }) {
const m = Dinero({ amount: Math.round((children || 0) * 100) });
return <div>{m.toFormat()}</div>;
}

View File

@@ -0,0 +1,23 @@
import { Button, Result } from "antd";
import React from "react";
export default function ErrorResultAtom({
title,
errorMessage,
tryAgainCallback,
}) {
return (
<Result
status="500"
title={title}
subTitle={errorMessage}
extra={
tryAgainCallback ? (
<Button type="primary" onClick={() => tryAgainCallback()}>
Try Again
</Button>
) : null
}
/>
);
}

View File

@@ -0,0 +1,12 @@
import { Tooltip } from "antd";
import moment from "moment";
import React from "react";
export default function TimeAgoFormatter(props) {
const m = moment(props.children);
return props.children ? (
<Tooltip placement="top" title={m.format("MM/DD/YYY hh:mm A")}>
{m.fromNow()}
</Tooltip>
) : null;
}

View File

@@ -0,0 +1,31 @@
import { Descriptions, Skeleton } from "antd";
import React from "react";
import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
export default function JobsDetailDescriptionMolecule({ loading, job }) {
if (loading) return <Skeleton active />;
if (!job) return <ErrorResultAtom title="Error displaying job data." />;
return (
<div>
<Descriptions
title={`${job.clm_no}${job.ins_co_nm ? ` | ${job.ins_co_nm}` : ""}`}
bordered
layout="vertical"
column={{ xxl: 5, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }}
>
<Descriptions.Item label="Claim No.">{job.clm_no}</Descriptions.Item>
<Descriptions.Item label="Ins Co. Nm.">
{job.ins_co_nm}
</Descriptions.Item>
<Descriptions.Item label="Owner">{`${job.ownr_fn} ${job.ownr_ln}`}</Descriptions.Item>
<Descriptions.Item label="Vehicle">{`${job.v_model_yr} ${job.v_makedesc} ${job.v_model}`}</Descriptions.Item>
<Descriptions.Item label="Claim Total.">
<CurrencyFormatterAtom>{job.clm_total}</CurrencyFormatterAtom>
</Descriptions.Item>
</Descriptions>
</div>
);
}

View File

@@ -0,0 +1,73 @@
import { Table } from "antd";
import React from "react";
import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom";
export default function JobLinesTableMolecule({ loading, jobLines }) {
const columns = [
{
title: "#",
dataIndex: "unq_seq",
key: "unq_seq",
},
{
title: "S#",
dataIndex: "line_ind",
key: "line_ind",
},
{
title: "Line Description",
dataIndex: "line_desc",
key: "line_desc",
},
{
title: "Part Type",
dataIndex: "part_type",
key: "part_type",
},
{
title: "Part Number",
dataIndex: "oem_partno",
key: "oem_partno",
},
{
title: "Database Price",
dataIndex: "db_price",
key: "db_price",
render: (text, record) => (
<CurrencyFormatterAtom>{record.db_price}</CurrencyFormatterAtom>
),
},
{
title: "Actual Price",
dataIndex: "act_price",
key: "act_price",
render: (text, record) => (
<CurrencyFormatterAtom>{record.act_price}</CurrencyFormatterAtom>
),
},
{
title: "Qty.",
dataIndex: "part_qty",
key: "part_qty",
},
];
return (
<div>
<Table
columns={columns}
rowKey="id"
loading={loading}
size="small"
pagination={false}
dataSource={jobLines}
scroll={{
x: true,
//y: "40rem"
}}
/>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { useQuery } from "@apollo/client";
import { Result } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_BY_PK } from "../../../graphql/jobs.queries";
import { selectSelectedJobId } from "../../../redux/application/application.selectors";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import JobsDetailDescriptionMolecule from "../../molecules/jobs-detail-description/jobs-detail-description.molecule";
import JobsLinesTableMolecule from "../../molecules/jobs-lines-table/jobs-lines-table.molecule";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
selectedJobId: selectSelectedJobId,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function JobsDetailOrganism({ selectedJobId }) {
const { loading, error, data } = useQuery(QUERY_JOB_BY_PK, {
variables: { jobId: selectedJobId },
skip: !selectedJobId,
});
if (!selectedJobId) return <Result title="No job selected." />;
if (error)
return (
<ErrorResultAtom
title="Error fetching Job details.."
errorMessage={JSON.stringify(error)}
/>
);
return (
<div>
<JobsDetailDescriptionMolecule
loading={loading}
job={data ? data.jobs_by_pk : null}
/>
<JobsLinesTableMolecule
loading={loading}
jobLines={data ? data.jobs_by_pk.joblines : []}
/>
{selectedJobId}
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsDetailOrganism);

View File

@@ -0,0 +1,135 @@
import { useQuery } from "@apollo/client";
import { List, Space, Spin, Typography } from "antd";
import React, { useState } from "react";
import InfiniteScroll from "react-infinite-scroller";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_ALL_JOBS_PAGINATED } from "../../../graphql/jobs.queries";
import { setSelectedJobId } from "../../../redux/application/application.actions";
import { selectSelectedJobId } from "../../../redux/application/application.selectors";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import TimeAgoFormatter from "../../atoms/time-ago-formatter/time-ago-formatter.atom";
import "./jobs-table.organism.styles.scss";
const mapStateToProps = createStructuredSelector({
selectedJobId: selectSelectedJobId,
});
const mapDispatchToProps = (dispatch) => ({
setSelectedJobId: (jobId) => dispatch(setSelectedJobId(jobId)),
});
const limit = 20;
export function JobsTableOrganism({ selectedJobId, setSelectedJobId }) {
const [state, setState] = useState({ hasMore: true });
const { loading, error, data, fetchMore } = useQuery(
QUERY_ALL_JOBS_PAGINATED,
{
variables: {
offset: 0,
limit: limit,
order: [{ updated_at: "desc" }],
},
}
);
const handleInfiniteOnLoad = (page) => {
fetchMore({
variables: {
offset: limit * page,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) {
console.log("No more results. Fetch More was empty.");
setState({ ...state, hasMore: false });
return prev;
}
const newCache = Object.assign({}, prev, {
jobs: [...prev.jobs, ...fetchMoreResult.jobs],
});
if (
newCache.jobs.length >= data &&
data.jobs_aggregate.aggregate.count
) {
console.log("No more results.");
setState({ ...state, hasMore: false });
}
return newCache;
},
});
};
const handleSelect = (jobId) => {
setSelectedJobId(jobId);
};
if (error)
return (
<ErrorResultAtom
title="Error fetching Jobs data."
errorMessage={JSON.stringify(error)}
/>
);
return (
<div>
<div className="jobs-list-infinite-container">
<InfiniteScroll
pageStart={0}
loadMore={handleInfiniteOnLoad}
hasMore={!loading && state.hasMore}
useWindow={false}
>
<List
dataSource={data ? data.jobs : []}
renderItem={(item) => (
<List.Item
className="jobs-list-item"
key={item.id}
onClick={() => handleSelect(item.id)}
>
<div
className={`jobs-list-item-content ${
item.id === selectedJobId
? "jobs-list-item-content-selected"
: ""
}`}
>
<div style={{ display: "flex" }}>
<Typography.Title level={4} style={{ flex: 1 }}>
{`${item.clm_no}${
item.ins_co_nm ? ` | ${item.ins_co_nm}` : ""
}`}
</Typography.Title>
<span className="job-list-last-updated-time">
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
</span>
</div>
<Space>
<span>{`${item.ownr_fn} ${item.ownr_ln}`}</span>
<span>
{`${item.v_model_yr} ${item.v_makedesc} ${item.v_model} ${item.v_vin}`}
</span>
</Space>
</div>
</List.Item>
)}
>
{loading && state.hasMore && (
<div>
<Spin />
</div>
)}
</List>
</InfiniteScroll>
</div>
{`${data ? data.jobs.length : 0} jobs loaded. ${
data ? data.jobs_aggregate.aggregate.count : 0
} total jobs.`}
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsTableOrganism);

View File

@@ -0,0 +1,26 @@
.jobs-list-infinite-container {
overflow-y: auto;
overflow-x: hidden;
height: 95vh;
}
.jobs-list-item {
padding: 0;
margin: 0;
.jobs-list-item-content {
&-selected {
border-left: 3px solid #1890ff;
}
display: inline;
margin: 0.5rem;
padding: 0.5rem;
width: 100%;
}
cursor: pointer;
&:hover {
background-color: #e6f7ff;
}
}

View File

@@ -1,33 +1,25 @@
import { Button } from "antd";
import { Col, Row } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const { ipcRenderer } = window;
//const settings = window.require("electron-settings");
import JobsTableOrganism from "../../organisms/jobs-table/jobs-table.organism";
import JobsDetailOrganism from "../../organisms/jobs-detail/jobs-detail.organism";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({});
export function JobsPage() {
// useEffect(() => {
// ipcRenderer.on("test-success", (event, obj) => {
// console.log("Test Success", obj);
// });
// // Cleanup the listener events so that memory leaks are avoided.
// return function cleanup() {
// ipcRenderer.removeAllListeners(
// "test-success",
// ipcTypes.default.filewatcher.startSuccess
// );
// };
// }, []);
return (
<div>
<div>Welcome to your new react app. </div>
<Button onClick={() => ipcRenderer.send("test")}>Send Test IPC</Button>
<Row gutter={[16, 16]}>
<Col span={10}>
<JobsTableOrganism />
</Col>
<Col span={14}>
<JobsDetailOrganism />
</Col>
</Row>
</div>
);
}

View File

@@ -24,3 +24,61 @@ export const INSERT_NEW_JOB = gql`
// v_type
// ]
// }
export const QUERY_ALL_JOBS_PAGINATED = gql`
query QUERY_ALL_JOBS_PAGINATED(
$offset: Int
$limit: Int
$order: [jobs_order_by!]
) {
jobs(offset: $offset, limit: $limit, order_by: $order) {
ownr_fn
ownr_ln
v_vin
v_model_yr
v_model
v_makedesc
id
ins_co_nm
clm_no
clm_total
ro_number
updated_at
}
jobs_aggregate {
aggregate {
count(distinct: true)
}
}
}
`;
export const QUERY_JOB_BY_PK = gql`
query QUERY_ALL_JOBS_PAGINATED($jobId: uuid!) {
jobs_by_pk(id: $jobId) {
ownr_fn
ownr_ln
v_vin
v_model_yr
v_model
v_makedesc
id
ins_co_nm
clm_no
clm_total
ro_number
updated_at
joblines(order_by: { unq_seq: asc }) {
id
act_price
db_price
line_desc
line_ind
oem_partno
part_qty
part_type
unq_seq
}
}
}
`;

View File

@@ -19,7 +19,13 @@ export const setWatcherStatus = (status) => ({
type: ApplicationActionTypes.SET_WATCHER_STATUS,
payload: status,
});
export const setWatcherError = (error) => ({
type: ApplicationActionTypes.SET_WATCHER_ERROR,
payload: error,
});
export const setSelectedJobId = (jobId) => ({
type: ApplicationActionTypes.SET_SELECTED_JOB_ID,
payload: jobId,
});

View File

@@ -3,6 +3,7 @@ const INITIAL_STATE = {
watcherStatus: "Not Started",
watchedPaths: [],
watcherError: null,
selectedJobId: null,
};
const applicationReducer = (state = INITIAL_STATE, action) => {
@@ -32,6 +33,8 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
...state,
watcherError: action.payload,
};
case ApplicationActionTypes.SET_SELECTED_JOB_ID:
return { ...state, selectedJobId: action.payload };
default:
return state;
}

View File

@@ -16,3 +16,7 @@ export const selectWatcherError = createSelector(
[selectApplication],
(application) => application.watcherError
);
export const selectSelectedJobId = createSelector(
[selectApplication],
(application) => application.selectedJobId
);

View File

@@ -4,5 +4,6 @@ const ApplicationActionTypes = {
REMOVE_WATCHED_PATH: "REMOVE_WATCHED_PATH",
SET_WATCHER_STATUS: "SET_WATCHER_STATUS",
SET_WATCHER_ERROR: "SET_WATCHER_ERROR",
SET_SELECTED_JOB_ID: "SET_SELECTED_JOB_ID",
};
export default ApplicationActionTypes;

View File

@@ -7,7 +7,7 @@ import userReducer from "./user/user.reducer";
const persistConfig = {
key: "root",
storage,
blacklist: ["application"],
blacklist: ["application", "user"],
};
const rootReducer = combineReducers({