Merged in dev-patrick (pull request #3)

Dev patrick
This commit is contained in:
Snapt Software
2020-04-02 00:51:37 +00:00
411 changed files with 23579 additions and 4578 deletions

View File

View File

@@ -0,0 +1,92 @@
CREATE SCHEMA audit;
CREATE TABLE audit_trail (
id serial PRIMARY KEY,
tstamp timestamp DEFAULT now(),
schemaname text,
tabname text,
operation text,
recordid uuid,
-- who text DEFAULT current_user,
new_val json,
old_val json,
useremail text,
bodyshopid uuid
);
-- More as an example than anything else, I wanted a function that would take two JSONB objects in PostgreSQL, and return how the left-hand side differs from the right-hand side. This means any key that is in the left but not in the right would be returned, along with any key whose value on the left is different from the right.
-- Heres a quick example of how to do this in a single SELECT. In real life, you probably want more error checking, but it shows how nice the built-in primitives are:
CREATE OR REPLACE FUNCTION json_diff(l JSONB, r JSONB) RETURNS JSONB AS
$json_diff$
SELECT jsonb_object_agg(a.key, a.value) FROM
( SELECT key, value FROM jsonb_each(l) ) a LEFT OUTER JOIN
( SELECT key, value FROM jsonb_each(r) ) b ON a.key = b.key
WHERE a.value != b.value OR b.key IS NULL;
$json_diff$
LANGUAGE sql;
CREATE OR REPLACE FUNCTION audit_trigger() RETURNS trigger AS $$
DECLARE
shopid text ;
email text;
BEGIN
select b.id, u.email INTO shopid, email from users u join associations a on u.email = a.useremail join bodyshops b on b.id = a.shopid where u.authid = current_setting('hasura.user', 't')::jsonb->>'x-hasura-user-id' and a.active = true;
IF TG_OP = 'INSERT'
THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, new_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(NEW), NEW.id, shopid, email);
RETURN NEW;
ELSIF TG_OP = 'UPDATE'
THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, new_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP,
json_diff(to_jsonb(OLD), to_jsonb(NEW)) , json_diff(to_jsonb(NEW), to_jsonb(OLD)), OLD.id, shopid, email);
RETURN NEW;
ELSIF TG_OP = 'DELETE'
THEN
INSERT INTO public.audit_trail (tabname, schemaname, operation, old_val, recordid, bodyshopid, useremail)
VALUES (TG_RELNAME, TG_TABLE_SCHEMA, TG_OP, row_to_json(OLD), OLD.ID, shopid, email);
RETURN OLD;
END IF;
END;
$$ LANGUAGE 'plpgsql' SECURITY DEFINER;
CREATE TRIGGER audit_trigger_users AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE PROCEDURE audit_trigger();

File diff suppressed because it is too large Load Diff

3
client/debug.log Normal file
View File

@@ -0,0 +1,3 @@
[0309/123120.472:ERROR:process_reader_win.cc(108)] process 40916 not found
[0309/123120.472:ERROR:exception_snapshot_win.cc(98)] thread ID 50448 not found in process
[0309/123120.472:ERROR:scoped_process_suspend.cc(40)] NtResumeProcess: An attempt was made to access an exiting process. (0xc000010a)

View File

@@ -4,46 +4,51 @@
"private": true,
"proxy": "https://localhost:5000",
"dependencies": {
"@ckeditor/ckeditor5-build-classic": "^16.0.0",
"@ckeditor/ckeditor5-build-classic": "^18.0.0",
"@ckeditor/ckeditor5-react": "^2.1.0",
"antd": "^3.26.8",
"@nivo/pie": "^0.61.1",
"@tanem/react-nprogress": "^3.0.20",
"aamva": "^1.2.0",
"antd": "^4.1.0",
"apollo-boost": "^0.4.4",
"apollo-link-context": "^1.0.19",
"apollo-link-error": "^1.1.12",
"apollo-link-logger": "^1.2.3",
"apollo-link-retry": "^2.2.15",
"apollo-link-ws": "^1.0.19",
"axios": "^0.19.2",
"chart.js": "^2.9.3",
"dotenv": "^8.2.0",
"firebase": "^7.8.1",
"firebase": "^7.13.1",
"graphql": "^14.6.0",
"i18next": "^19.1.0",
"i18next": "^19.3.4",
"node-sass": "^4.13.1",
"react": "^16.12.0",
"react": "^16.13.1",
"react-apollo": "^3.1.3",
"react-barcode": "^1.4.0",
"react-big-calendar": "^0.23.0",
"react-chartjs-2": "^2.9.0",
"react-dom": "^16.12.0",
"react-big-calendar": "^0.24.1",
"react-dom": "^16.13.1",
"react-grid-gallery": "^0.5.5",
"react-grid-layout": "^0.18.3",
"react-html-email": "^3.0.0",
"react-i18next": "^11.3.1",
"react-i18next": "^11.3.4",
"react-icons": "^3.9.0",
"react-image-file-resizer": "^0.2.1",
"react-moment": "^0.9.7",
"react-number-format": "^4.3.1",
"react-redux": "^7.1.3",
"react-number-format": "^4.4.1",
"react-pdf": "^4.1.0",
"react-redux": "^7.2.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.3.1",
"react-scripts": "3.4.1",
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"reselect": "^4.0.0",
"styled-components": "^5.0.1",
"subscriptions-transport-ws": "^0.9.16",
"twilio": "^3.39.5"
"subscriptions-transport-ws": "^0.9.16"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
@@ -67,6 +72,7 @@
"devDependencies": {
"@apollo/react-testing": "^3.1.3",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2"
"enzyme-adapter-react-16": "^1.15.2",
"source-map-explorer": "^2.4.2"
}
}

View File

@@ -1,3 +1,4 @@
import { ApolloProvider } from "@apollo/react-common";
import { ApolloLink } from "apollo-boost";
import { InMemoryCache } from "apollo-cache-inmemory";
import ApolloClient from "apollo-client";
@@ -5,21 +6,17 @@ import { split } from "apollo-link";
import { setContext } from "apollo-link-context";
import { HttpLink } from "apollo-link-http";
import apolloLogger from "apollo-link-logger";
import { RetryLink } from "apollo-link-retry";
import { WebSocketLink } from "apollo-link-ws";
import { getMainDefinition } from "apollo-utilities";
import React, { Component } from "react";
import { ApolloProvider } from "react-apollo";
import SpinnerComponent from "../components/loading-spinner/loading-spinner.component";
//import { shouldRefreshToken, refreshToken } from "../graphql/middleware";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import { auth } from "../firebase/firebase.utils";
import errorLink from "../graphql/apollo-error-handling";
import App from "./App";
class AppContainer extends Component {
state = {
client: null,
loaded: false
};
async componentDidMount() {
export default class AppContainer extends Component {
constructor() {
super();
const httpLink = new HttpLink({
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT
});
@@ -29,8 +26,9 @@ class AppContainer extends Component {
options: {
lazy: true,
reconnect: true,
connectionParams: () => {
const token = localStorage.getItem("token");
connectionParams: async () => {
//const token = localStorage.getItem("token");
const token = await auth.currentUser.getIdToken(true);
if (token) {
return {
headers: {
@@ -41,6 +39,13 @@ class AppContainer extends Component {
}
}
});
const subscriptionMiddleware = {
applyMiddleware: async (options, next) => {
options.authToken = await auth.currentUser.getIdToken(true);
next();
}
};
wsLink.subscriptionClient.use([subscriptionMiddleware]);
const link = split(
// split based on operation type
@@ -64,16 +69,29 @@ class AppContainer extends Component {
);
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("token");
if (token) {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ""
}
};
} else {
return { headers };
return auth.currentUser.getIdToken().then(token => {
if (token) {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ""
}
};
} else {
return { headers };
}
});
});
const retryLink = new RetryLink({
delay: {
initial: 300,
max: 5,
jitter: true
},
attempts: {
max: 5,
retryIf: (error, _operation) => !!error
}
});
@@ -81,37 +99,27 @@ class AppContainer extends Component {
if (process.env.NODE_ENV === "development") {
middlewares.push(apolloLogger);
}
middlewares.push(errorLink.concat(authLink.concat(link)));
middlewares.push(retryLink.concat(errorLink.concat(authLink.concat(link))));
const cache = new InMemoryCache();
const client = new ApolloClient({
link: ApolloLink.from(middlewares),
cache,
connectToDevTools: true
});
this.setState({
client,
loaded: true
});
this.state = { client };
}
componentWillUnmount() {}
render() {
const { client, loaded } = this.state;
if (!loaded) {
return <SpinnerComponent />;
}
const { client } = this.state;
return (
<ApolloProvider client={client}>
<GlobalLoadingBar />
<App />
</ApolloProvider>
);
}
}
export default AppContainer;

View File

@@ -1,27 +1 @@
@import "~antd/dist/antd.css";
/* .ant-layout-header {
position: absolute;
top: 0px;
left: 0px;
height: 5vh;
right: 0px;
overflow: hidden;
}
.ant-layout-content {
position: absolute;
top: 5vh;
bottom: 3vh;
left: 0px;
right: 0px;
overflow: auto;
}
.ant-layout-footer {
position: absolute;
bottom: 0px;
height: 3vh;
left: 0px;
right: 0px;
overflow: hidden;
} */
@import "~antd/dist/antd.css";

View File

@@ -26,6 +26,7 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = dispatch => ({
checkUserSession: () => dispatch(checkUserSession())
});
export default connect(
mapStateToProps,
mapDispatchToProps
@@ -34,9 +35,10 @@ export default connect(
checkUserSession();
return () => {};
}, [checkUserSession]);
const { t } = useTranslation();
if (currentUser && currentUser.language)
i18next.changeLanguage(currentUser.language, (err, t) => {
i18next.changeLanguage(currentUser.language, err => {
if (err)
return console.log("Error encountered when changing languages.", err);
});
@@ -49,14 +51,13 @@ export default connect(
<div>
<Switch>
<ErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
<Suspense fallback={<LoadingSpinner message='App.Js Suspense' />}>
<Route exact path='/' component={LandingPage} />
<Route exact path='/unauthorized' component={Unauthorized} />
<Route exact path='/signin' component={SignInPage} />
<PrivateRoute
//isAuthorized={HookCurrentUser.data.currentUser ? true : false}
isAuthorized={currentUser.authorized}
path='/manage'
component={ManagePage}

View File

@@ -1,18 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App.container";
import { MockedProvider } from "@apollo/react-testing";
const div = document.createElement("div");
it("renders without crashing", () => {
ReactDOM.render(
<MockedProvider>
<App />
</MockedProvider>,
div
);
});
it("unmounts without crashing", () => {
ReactDOM.unmountComponentAtNode(div);
});

View File

@@ -1,37 +1,51 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
startLoading,
endLoading
} from "../../redux/application/application.actions";
import { setEmailOptions } from "../../redux/email/email.actions";
import T from "../../emails/parts-order/parts-order.email";
import { REPORT_QUERY_PARTS_ORDER_BY_PK } from "../../emails/parts-order/parts-order.query";
import T, {
Subject
} from "../../emails/templates/appointment-confirmation/appointment-confirmation.template";
import { EMAIL_APPOINTMENT_CONFIRMATION } from "../../emails/templates/appointment-confirmation/appointment-confirmation.query";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
setEmailOptions: e => dispatch(setEmailOptions(e))
setEmailOptions: e => dispatch(setEmailOptions(e)),
load: () => dispatch(startLoading()),
endload: () => dispatch(endLoading())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function Test({ setEmailOptions }) {
)(function Test({ setEmailOptions, load, endload }) {
return (
<button
onClick={() =>
setEmailOptions({
messageOptions: {
from: { name: "Kavia Autobdoy", address: "noreply@bodyshop.app" },
to: "patrickwf@gmail.com",
replyTo: "snaptsoft@gmail.com"
},
template: T,
queryConfig: [
REPORT_QUERY_PARTS_ORDER_BY_PK,
{ variables: { id: "46f3aa34-c3bd-46c8-83fc-c93b7ce84f46" } }
]
})
}>
Set email config.
</button>
<div>
<button
onClick={() =>
setEmailOptions({
messageOptions: {
from: { name: "Kavia Autobody", address: "noreply@bodyshop.app" },
to: "patrickwf@gmail.com",
replyTo: "snaptsoft@gmail.com",
subject: Subject
},
template: T,
queryConfig: [
EMAIL_APPOINTMENT_CONFIRMATION,
{ variables: { id: "91bb31dd-ea87-4cfc-bbe2-2ec754dcb861" } }
]
})
}
>
Set email config.
</button>
<button onClick={() => load()}>Load</button>
<button onClick={() => endload()}>Stop</button>
</div>
);
});

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Alert component should render Alert component 1`] = `ShallowWrapper {}`;

View File

@@ -6,8 +6,18 @@ import { shallow, mount } from "enzyme";
const div = document.createElement("div");
it("renders without crashing", () => {
const wrapper = mount(<Alert type="error" message="Test Error" />);
console.log("wrapper", wrapper);
// expect(wrapper.children()).to.have.lengthOf(1);
describe("Alert component", () => {
let wrapper;
beforeEach(() => {
const mockProps = {
type: "error",
message: "Test error message."
};
wrapper = shallow(<Alert {...mockProps} />);
});
it("should render Alert component", () => {
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AllocationsAssignmentComponent component should create an allocation on save 1`] = `ReactWrapper {}`;
exports[`AllocationsAssignmentComponent component should render AllocationsAssignmentComponent component 1`] = `ReactWrapper {}`;

View File

@@ -9,10 +9,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function AllocationsAssignmentComponent({
export function AllocationsAssignmentComponent({
bodyshop,
handleAssignment,
assignment,
@@ -23,7 +20,6 @@ export default connect(
const { t } = useTranslation();
const onChange = e => {
console.log("e", e);
setAssignment({ ...assignment, employeeid: e });
};
@@ -31,16 +27,15 @@ export default connect(
const popContent = (
<div>
<Select
<Select id="employeeSelector"
showSearch
style={{ width: 200 }}
placeholder="Select a person"
optionFilterProp="children"
placeholder='Select a person'
optionFilterProp='children'
onChange={onChange}
filterOption={(input, option) =>
option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
}>
{bodyshop.employees.map(emp => (
<Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`}
@@ -56,10 +51,9 @@ export default connect(
/>
<Button
type="primary"
type='primary'
disabled={!assignment.employeeid}
onClick={handleAssignment}
>
onClick={handleAssignment}>
Assign
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
@@ -73,4 +67,6 @@ export default connect(
</Button>
</Popover>
);
});
}
export default connect(mapStateToProps, null)(AllocationsAssignmentComponent);

View File

@@ -0,0 +1,38 @@
import { mount, shallow } from "enzyme";
import React from "react";
import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
import { MockBodyshop } from "../../utils/TestingHelpers";
import { Select } from "antd";
const div = document.createElement("div");
describe("AllocationsAssignmentComponent component", () => {
let wrapper;
beforeEach(() => {
const mockProps = {
bodyshop: MockBodyshop,
handleAssignment: jest.fn(),
assignment: {},
setAssignment: jest.fn(),
visibilityState: [false, jest.fn()],
maxHours: 4
};
wrapper = mount(<AllocationsAssignmentComponent {...mockProps} />);
});
it("should render AllocationsAssignmentComponent component", () => {
expect(wrapper).toMatchSnapshot();
});
it("should render a list of employees", () => {
const empList = wrapper.find("#employeeSelector");
console.log(empList.debug());
expect(empList.children()).to.have.lengthOf(2);
});
it("should create an allocation on save", () => {
wrapper.find("Button").simulate("click");
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react";
import AllocationsAssignmentComponent from "./allocations-assignment.component";
import { useMutation } from "react-apollo";
import { useMutation } from "@apollo/react-hooks";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next";
import { notification } from "antd";

View File

@@ -1,20 +0,0 @@
import React from "react";
import { shallow } from "enzyme";
import AllocationsAssignmentContainer from "./allocations-assignment.container";
describe("LineAllocationsContainer", () => {
let mockRefetch;
let jobLineId;
let wrapper;
beforeEach(() => {
mockRefetch = jest.fn;
jobLineId = "b76e44a8-943f-4c67-b8f4-38d14db8b4b8";
const mockProps = {
refetch: mockRefetch,
jobLineId,
hours: 5
};
shallow(<AllocationsAssignmentContainer {...mockProps} />);
});
});

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react";
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
import { useMutation } from "react-apollo";
import { useMutation } from "@apollo/react-hooks";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next";
import { notification } from "antd";

View File

@@ -1,4 +1,4 @@
import { Icon } from "antd";
import Icon from "@ant-design/icons";
import React from "react";
import { MdRemoveCircleOutline } from "react-icons/md";

View File

@@ -1,5 +1,5 @@
import React from "react";
import { useMutation } from "react-apollo";
import { useMutation } from "@apollo/react-hooks";
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
import AllocationsLabelComponent from "./allocations-employee-label.component";
import { notification } from "antd";

View File

@@ -0,0 +1,85 @@
import React, { useState } from "react";
import { Table } from "antd";
import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { useTranslation } from "react-i18next";
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
export default function AuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {}
});
const { t } = useTranslation();
const columns = [
{
title: t("audit.fields.created"),
dataIndex: " created",
key: " created",
width: "10%",
render: (text, record) => (
<DateTimeFormatter>{record.created}</DateTimeFormatter>
),
sorter: (a, b) => a.created - b.created,
sortOrder:
state.sortedInfo.columnKey === "created" && state.sortedInfo.order
},
{
title: t("audit.fields.operation"),
dataIndex: "operation",
key: "operation",
width: "10%",
sorter: (a, b) => alphaSort(a.operation, b.operation),
sortOrder:
state.sortedInfo.columnKey === "operation" && state.sortedInfo.order
},
{
title: t("audit.fields.values"),
dataIndex: " old_val",
key: " old_val",
width: "10%",
render: (text, record) => (
<AuditTrailValuesComponent
oldV={record.old_val}
newV={record.new_val}
/>
)
},
{
title: t("audit.fields.useremail"),
dataIndex: "useremail",
key: "useremail",
width: "10%",
sorter: (a, b) => alphaSort(a.useremail, b.useremail),
sortOrder:
state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
}
];
const formItemLayout = {
labelCol: {
xs: { span: 12 },
sm: { span: 5 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 }
}
};
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
{...formItemLayout}
loading={loading}
size="small"
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={data}
onChange={handleTableChange}
/>
);
}

View File

@@ -0,0 +1,24 @@
import React from "react";
import AuditTrailListComponent from "./audit-trail-list.component";
import { useQuery } from "@apollo/react-hooks";
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import AlertComponent from "../alert/alert.component";
export default function AuditTrailListContainer({ recordId }) {
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
variables: { id: recordId },
fetchPolicy: "network-only"
});
return (
<div>
{error ? (
<AlertComponent type="error" message={error.message} />
) : (
<AuditTrailListComponent
loading={loading}
data={data ? data.audit_trail : null}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,31 @@
import React from "react";
import { List } from "antd";
import Icon from "@ant-design/icons";
import { FaArrowRight } from "react-icons/fa";
export default function AuditTrailValuesComponent({ oldV, newV }) {
console.log("(!oldV & !newV)", !oldV && !newV);
console.log("(!oldV & newV)", !oldV && newV);
if (!oldV && !newV) return <div></div>;
if (!oldV && newV)
return (
<List style={{ width: "800px" }} bordered size="small">
{Object.keys(newV).map((key, idx) => (
<List.Item key={idx} value={key}>
{key}: {JSON.stringify(newV[key])}
</List.Item>
))}
</List>
);
return (
<List style={{ width: "800px" }} bordered size="small">
{Object.keys(oldV).map((key, idx) => (
<List.Item key={idx}>
{key}: {oldV[key]} <Icon component={FaArrowRight} />
{JSON.stringify(newV[key])}
</List.Item>
))}
</List>
);
}

View File

@@ -0,0 +1,41 @@
import { ShrinkOutlined } from "@ant-design/icons";
import { Badge } from "antd";
import React from "react";
import { connect } from "react-redux";
import {
openConversation,
toggleChatVisible
} from "../../redux/messaging/messaging.actions";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
const mapDispatchToProps = dispatch => ({
toggleChatVisible: () => dispatch(toggleChatVisible()),
openConversation: number => dispatch(openConversation(number))
});
export function ChatConversationListComponent({
toggleChatVisible,
conversationList,
openConversation
}) {
return (
<div className='chat-overlay-open'>
<ShrinkOutlined onClick={() => toggleChatVisible()} />
{conversationList.map(item => (
<Badge count={item.messages_aggregate.aggregate.count || 0}>
<div
key={item.id}
style={{ cursor: "pointer", display: "block" }}
onClick={() =>
openConversation({ phone_num: item.phone_num, id: item.id })
}>
<div>
<PhoneNumberFormatter>{item.phone_num}</PhoneNumberFormatter>
</div>
</div>
</Badge>
))}
</div>
);
}
export default connect(null, mapDispatchToProps)(ChatConversationListComponent);

View File

@@ -0,0 +1,38 @@
import { CloseCircleFilled } from "@ant-design/icons";
import React from "react";
import { connect } from "react-redux";
import {
closeConversation,
sendMessage,
toggleConversationVisible
} from "../../redux/messaging/messaging.actions";
import PhoneFormatter from "../../utils/PhoneFormatter";
const mapDispatchToProps = dispatch => ({
toggleConversationVisible: conversationId =>
dispatch(toggleConversationVisible(conversationId)),
closeConversation: phone => dispatch(closeConversation(phone)),
sendMessage: message => dispatch(sendMessage(message))
});
function ChatConversationClosedComponent({
conversation,
toggleConversationVisible,
closeConversation
}) {
return (
<div
className='chat-conversation-closed'
onClick={() => toggleConversationVisible(conversation.id)}>
<PhoneFormatter>{conversation.phone_num}</PhoneFormatter>
<CloseCircleFilled
onClick={() => closeConversation(conversation.phone_num)}
/>
</div>
);
}
export default connect(
null,
mapDispatchToProps
)(ChatConversationClosedComponent);

View File

@@ -1,120 +1,29 @@
import { Button, Card, Input, Icon } from "antd";
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import twilio from "twilio";
import {
closeConversation,
toggleConversationVisible
} from "../../redux/messaging/messaging.actions";
import PhoneFormatter from "../../utils/PhoneFormatter";
import "./chat-conversation.styles.scss"; //https://bootsnipp.com/snippets/exR5v
import { MdSend } from "react-icons/md";
import { useTranslation } from "react-i18next";
import { Badge, Card } from "antd";
import React from "react";
import ChatConversationClosedComponent from "./chat-conversation.closed.component";
import ChatConversationOpenComponent from "./chat-conversation.open.component";
const client = twilio(
"ACf1b1aaf0e04740828b49b6e58467d787",
"0bea5e29a6d77593183ab1caa01d23de"
);
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
toggleConversationVisible: conversationId =>
dispatch(toggleConversationVisible(conversationId)),
closeConversation: phone => dispatch(closeConversation(phone))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function ChatConversationComponent({
export default function ChatConversationComponent({
conversation,
toggleConversationVisible,
closeConversation
messages,
subState,
unreadCount
}) {
const { t } = useTranslation();
const [messages, setMessages] = useState([]);
useEffect(() => {
client.messages.list({ limit: 20 }, (error, items) => {
setMessages(
items.reduce((acc, value) => {
acc.push({
sid: value.sid,
direction: value.direction,
body: value.body
});
return acc;
}, [])
);
});
return () => {};
}, [setMessages]);
return (
<div>
<Card
title={
conversation.open ? (
<div style={{ display: "flex" }}>
<div
onClick={() => toggleConversationVisible(conversation.phone)}
>
<PhoneFormatter>{conversation.phone}</PhoneFormatter>
</div>
<Button
type="danger"
shape="circle-outline"
onClick={() => closeConversation(conversation.phone)}
>
X
</Button>
</div>
) : null
}
style={{
width: conversation.open ? "400px" : "175px",
margin: "0px 10px"
}}
size="small"
>
{conversation.open ? (
<div>
<div className="messages" style={{ height: "400px" }}>
<ul>
{messages.map(item => (
<li
key={item.sid}
className={`${
item.direction === "inbound" ? "sent" : "replies"
}`}
>
<p> {item.body}</p>
</li>
))}
</ul>
</div>
<Input.Search
placeholder={t("messaging.labels.typeamessage")}
enterButton={<Icon component={MdSend} />}
<div className='chat-conversation'>
<Badge count={unreadCount}>
<Card size='small'>
{conversation.open ? (
<ChatConversationOpenComponent
messages={messages}
conversation={conversation}
subState={subState}
/>
</div>
) : (
<div style={{ display: "flex" }}>
<div onClick={() => toggleConversationVisible(conversation.phone)}>
<PhoneFormatter>{conversation.phone}</PhoneFormatter>
</div>
<Button
type="dashed"
shape="circle-outline"
onClick={() => closeConversation(conversation.phone)}
>
X
</Button>
</div>
)}
</Card>
) : (
<ChatConversationClosedComponent conversation={conversation} />
)}
</Card>
</Badge>
</div>
);
});
}

View File

@@ -1,17 +1,34 @@
import { useSubscription } from "@apollo/react-hooks";
import React from "react";
import { CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries";
import ChatConversationComponent from "./chat-conversation.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function ChatConversationContainer({ conversation }) {
return <ChatConversationComponent conversation={conversation} />;
});
export default function ChatConversationContainer({ conversation }) {
const { loading, error, data } = useSubscription(
CONVERSATION_SUBSCRIPTION_BY_PK,
{
variables: { conversationId: conversation.id }
}
);
return (
<ChatConversationComponent
subState={[loading, error]}
conversation={conversation}
unreadCount={
(data &&
data.conversations_by_pk &&
data.conversations_by_pk.messages_aggregate &&
data.conversations_by_pk.messages_aggregate.aggregate &&
data.conversations_by_pk.messages_aggregate.aggregate.count) ||
0
}
messages={
(data &&
data.conversations_by_pk &&
data.conversations_by_pk.messages) ||
[]
}
/>
);
}

View File

@@ -0,0 +1,36 @@
import React from "react";
import { connect } from "react-redux";
import { toggleConversationVisible } from "../../redux/messaging/messaging.actions";
import AlertComponent from "../alert/alert.component";
import ChatMessageListComponent from "../chat-messages-list/chat-message-list.component";
import ChatSendMessage from "../chat-send-message/chat-send-message.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import { ShrinkOutlined } from "@ant-design/icons";
const mapDispatchToProps = dispatch => ({
toggleConversationVisible: conversation =>
dispatch(toggleConversationVisible(conversation))
});
export function ChatConversationOpenComponent({
conversation,
messages,
subState,
toggleConversationVisible
}) {
const [loading, error] = subState;
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type='error' />;
return (
<div className='chat-conversation-open'>
<ShrinkOutlined
onClick={() => toggleConversationVisible(conversation.id)}
/>
<ChatMessageListComponent messages={messages} />
<ChatSendMessage conversation={conversation} />
</div>
);
}
export default connect(null, mapDispatchToProps)(ChatConversationOpenComponent);

View File

@@ -0,0 +1,32 @@
import { Affix } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectConversations } from "../../redux/messaging/messaging.selectors";
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
import ChatMessagesButtonContainer from "../chat-messages-button/chat-messages-button.container";
import "./chat-dock.styles.scss";
const mapStateToProps = createStructuredSelector({
activeConversations: selectConversations
});
export function ChatOverlayContainer({ activeConversations }) {
return (
<Affix offsetBottom={0}>
<div className='chat-dock'>
<ChatMessagesButtonContainer />
{activeConversations
? activeConversations.map(conversation => (
<ChatConversationContainer
conversation={conversation}
key={conversation.id}
/>
))
: null}
</div>
</Affix>
);
}
export default connect(mapStateToProps, null)(ChatOverlayContainer);

View File

@@ -1,6 +1,67 @@
.chat-dock {
z-index: 5;
//overflow-x: scroll;
// overflow-y: hidden;
width: 100%;
display: flex;
align-items: baseline;
}
.chat-conversation {
margin: 2em 1em 0em 1em;
}
.chat-conversation-open {
height: 500px;
}
// .chat-messages {
// height: 80%;
// overflow-x: hidden;
// overflow-y: scroll;
// flex-grow: 1;
// ul {
// list-style: none;
// margin: 0;
// padding: 0;
// }
// ul li {
// display: inline-block;
// clear: both;
// padding: 3px 10px;
// border-radius: 30px;
// margin-bottom: 2px;
// }
// .inbound {
// background: #eee;
// float: left;
// }
// .outbound {
// float: right;
// background: #0084ff;
// color: #fff;
// }
// .inbound + .outbound {
// border-bottom-right-radius: 5px;
// }
// .outbound + .outbound {
// border-top-right-radius: 5px;
// border-bottom-right-radius: 5px;
// }
// .outbound:last-of-type {
// border-bottom-right-radius: 30px;
// }
// }
.messages {
height: auto;
min-height: calc(100% - 93px);
min-height: calc(100% - 10px);
max-height: calc(100% - 93px);
overflow-y: scroll;
overflow-x: hidden;
@@ -21,7 +82,7 @@
display: inline-block;
clear: both;
//float: left;
margin: 5px 15px 5px 15px;
margin: 5px;
width: calc(100% - 25px);
font-size: 0.9em;
}

View File

@@ -0,0 +1,47 @@
import { MessageFilled } from "@ant-design/icons";
import { Badge, Card } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
const mapStateToProps = createStructuredSelector({
chatVisible: selectChatVisible
});
const mapDispatchToProps = dispatch => ({
toggleChatVisible: () => dispatch(toggleChatVisible())
});
export function ChatWindowComponent({
chatVisible,
toggleChatVisible,
conversationList,
unreadCount
}) {
const { t } = useTranslation();
return (
<div className='chat-conversation'>
<Badge count={unreadCount}>
<Card size='small'>
{chatVisible ? (
<ChatConversationListComponent
conversationList={conversationList}
/>
) : (
<div onClick={() => toggleChatVisible()}>
<MessageFilled />
<strong>{t("messaging.labels.messaging")}</strong>
</div>
)}
</Card>
</Badge>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ChatWindowComponent);

View File

@@ -0,0 +1,27 @@
import { useSubscription } from "@apollo/react-hooks";
import React from "react";
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import ChatMessagesButtonComponent from "./chat-messages-button.component";
export default function ChatMessagesButtonContainer() {
const { loading, error, data } = useSubscription(
CONVERSATION_LIST_SUBSCRIPTION
);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type='error' />;
return (
<ChatMessagesButtonComponent
conversationList={(data && data.conversations) || []}
unreadCount={
(data &&
data.conversations.reduce((acc, val) => {
return (acc = acc + val.messages_aggregate.aggregate.count);
}, 0)) ||
0
}
/>
);
}

View File

@@ -0,0 +1,44 @@
import { CheckCircleOutlined, CheckOutlined } from "@ant-design/icons";
import React, { useEffect, useRef } from "react";
export default function ChatMessageListComponent({ messages }) {
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
console.log("use");
!!messagesEndRef.current &&
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
};
useEffect(scrollToBottom, [messages]);
const StatusRender = status => {
switch (status) {
case "sent":
return <CheckOutlined style={{ margin: "2px", float: "right" }} />;
case "delivered":
return (
<CheckCircleOutlined style={{ margin: "2px", float: "right" }} />
);
default:
return null;
}
};
return (
<div className='messages'>
<ul>
{messages.map(item => (
<li
key={item.id}
className={`${item.isoutbound ? "replies" : "sent"}`}>
<p>
{item.text}
{StatusRender(item.status)}
</p>
</li>
))}
<li ref={messagesEndRef} />
</ul>
</div>
);
}

View File

@@ -2,7 +2,7 @@ import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { openConversation } from "../../redux/messaging/messaging.actions";
import { Icon } from "antd";
import { MessageFilled } from "@ant-design/icons";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
@@ -14,9 +14,8 @@ export default connect(
mapDispatchToProps
)(function ChatOpenButton({ openConversation, phone }) {
return (
<Icon
<MessageFilled
style={{ margin: 4 }}
type="message"
onClick={() => openConversation(phone)}
/>
);

View File

@@ -1,36 +0,0 @@
import { Badge, Card, Icon } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
export default function ChatWindowComponent({
chatVisible,
toggleChatVisible
}) {
const { t } = useTranslation();
return (
<div>
<Badge count={5}>
<Card
onClick={() => toggleChatVisible()}
style={{
width: chatVisible ? "300px" : "125px",
margin: "0px 10px"
}}
size="small"
>
{chatVisible ? (
<div className="messages" style={{ height: "400px" }}>
List of chats here.
</div>
) : (
<div>
<Icon type="message" />
<strong style={{ paddingLeft: "10px" }}>
{t("messaging.labels.messaging")}
</strong>
</div>
)}
</Card>
</Badge>
</div>
);
}

View File

@@ -1,48 +0,0 @@
import { Affix, Badge } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
import {
selectChatVisible,
selectConversations
} from "../../redux/messaging/messaging.selectors";
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
import ChatOverlayComponent from "./chat-overlay.component";
const mapStateToProps = createStructuredSelector({
chatVisible: selectChatVisible,
conversations: selectConversations
});
const mapDispatchToProps = dispatch => ({
toggleChatVisible: () => dispatch(toggleChatVisible())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function ChatWindowContainer({
chatVisible,
toggleChatVisible,
conversations
}) {
return (
<Affix offsetBottom={0}>
<div>
<Badge count={10}>
<ChatOverlayComponent
chatVisible={chatVisible}
toggleChatVisible={toggleChatVisible}
/>
</Badge>
{conversations
? conversations.map((conversation, idx) => (
<Badge key={idx} count={5}>
<ChatConversationContainer conversation={conversation} />
</Badge>
))
: null}
</div>
</Affix>
);
});

View File

@@ -0,0 +1,69 @@
import { Input, Spin } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { sendMessage } from "../../redux/messaging/messaging.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = dispatch => ({
sendMessage: message => dispatch(sendMessage(message))
});
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage }) {
const [message, setMessage] = useState("");
useEffect(() => {
if (conversation.isSending === false) {
setMessage("");
}
}, [conversation, setMessage]);
const { t } = useTranslation();
const handleEnter = () => {
sendMessage({
to: conversation.phone_num,
body: message,
messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id
});
};
return (
<div style={{ display: "flex " }}>
<Input.TextArea
allowClear
autoFocus
suffix={<span>a</span>}
autoSize={{ minRows: 1, maxRows: 4 }}
value={message}
disabled={conversation.isSending}
placeholder={t("messaging.labels.typeamessage")}
onChange={e => setMessage(e.target.value)}
onPressEnter={event => {
event.preventDefault();
if (!!!event.shiftKey) handleEnter();
}}
/>
<Spin
style={{ display: `${conversation.isSending ? "" : "none"}` }}
indicator={
<LoadingOutlined
style={{
fontSize: 24
}}
spin
/>
}
/>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ChatSendMessageComponent);

View File

@@ -0,0 +1,119 @@
import { Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
export default function ContractsCarsComponent({
loading,
data,
selectedCar,
handleSelect
}) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
search: ""
});
const { t } = useTranslation();
const columns = [
{
title: t("courtesycars.fields.fleetnumber"),
dataIndex: "fleetnumber",
key: "fleetnumber",
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
sortOrder:
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.year"),
dataIndex: "year",
key: "year",
sorter: (a, b) => alphaSort(a.year, b.year),
sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.make"),
dataIndex: "make",
key: "make",
sorter: (a, b) => alphaSort(a.make, b.make),
sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.model"),
dataIndex: "model",
key: "model",
sorter: (a, b) => alphaSort(a.model, b.model),
sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.plate"),
dataIndex: "plate",
key: "plate",
sorter: (a, b) => alphaSort(a.plate, b.plate),
sortOrder:
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const filteredData =
state.search === ""
? data
: data.filter(
cc =>
(cc.fleetnumber || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.status || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.year || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.make || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.model || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
);
return (
<Table
loading={loading}
title={() => (
<Input.Search
placeholder={t("general.labels.search")}
value={state.search}
onChange={e => setState({ ...state, search: e.target.value })}
/>
)}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={filteredData}
onChange={handleTableChange}
rowSelection={{
onSelect: handleSelect,
type: "radio",
selectedRowKeys: [selectedCar]
}}
/>
);
}

View File

@@ -0,0 +1,29 @@
import { useQuery } from "@apollo/react-hooks";
import React from "react";
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
import AlertComponent from "../alert/alert.component";
import ContractCarsComponent from "./contract-cars.component";
export default function ContractCarsContainer({ selectedCarState, bodyshop }) {
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC);
const [selectedCar, setSelectedCar] = selectedCarState;
const handleSelect = record => {
setSelectedCar(record.id);
};
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
<ContractCarsComponent
handleSelect={handleSelect}
selectedCar={selectedCar}
loading={loading}
data={data ? data.courtesycars : []}
/>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Descriptions, Card } from "antd";
import { Link } from "react-router-dom";
export default function ContractCourtesyCarBlock({ courtesyCar }) {
const { t } = useTranslation();
return (
<Link to={`/manage/courtesycars/${courtesyCar && courtesyCar.id}`}>
<Card title={t("courtesycars.labels.courtesycar")}>
<Descriptions size="small" column={1}>
<Descriptions.Item label={t("courtesycars.fields.fleetnumber")}>
{(courtesyCar && courtesyCar.fleetnumber) || ""}
</Descriptions.Item>
<Descriptions.Item label={t("courtesycars.fields.plate")}>
{(courtesyCar && courtesyCar.plate) || ""}
</Descriptions.Item>
<Descriptions.Item label={t("courtesycars.labels.vehicle")}>
{`${(courtesyCar && courtesyCar.year) || ""} ${(courtesyCar &&
courtesyCar.make) ||
""} ${(courtesyCar && courtesyCar.model) || ""}`}
</Descriptions.Item>
</Descriptions>
</Card>
</Link>
);
}

View File

@@ -0,0 +1,264 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Form, Input, DatePicker, InputNumber, Button } from "antd";
import aamva from "aamva";
import InputPhone from "../form-items-formatted/phone-form-item.component";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
export default function ContractFormComponent() {
const [state, setState] = useState("");
const { t } = useTranslation();
return (
<div>
<div style={{ background: "#f00" }}>
TEST AREA
<Input value={state} onChange={e => setState(e.target.value)} />
<Button
onClick={() => {
console.log("state", state);
//let data = state;
var data =
"%FLDELRAY BEACH^DOE$JOHN$^4818 S FEDERAL BLVD^ ? ;6360100462172082009=2101198299090=? #! 33435 I 1600 ECCECC00000?";
data = data.replace(/\n/, "");
// replace spaces with regular space
data = data.replace(/\s/g, " ");
var track = data.match(/(.*?\?)(.*?\?)(.*?\?)/);
console.log("data", data);
console.log("track", track);
const a = aamva.stripe(data);
console.log(JSON.stringify(a));
}}
>
Decode
</Button>
</div>
<Form.Item
label={t("contracts.fields.status")}
name="status"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<ContractStatusSelector />
</Form.Item>
<Form.Item
label={t("contracts.fields.start")}
name="start"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.scheduledreturn")}
name="scheduledreturn"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item label={t("contracts.fields.actualreturn")} name="actualreturn">
<DatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.kmstart")}
name="kmstart"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<InputNumber />
</Form.Item>
<Form.Item label={t("contracts.fields.kmend")} name="kmend">
<InputNumber />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_dlnumber")}
name="driver_dlnumber"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_dlexpiry")}
name="driver_dlexpiry"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_dlst")}
name="driver_dlst"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_fn")}
name="driver_fn"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_ln")}
name="driver_ln"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_addr1")}
name="driver_addr1"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item label={t("contracts.fields.driver_addr2")} name="driver_addr2">
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_city")}
name="driver_city"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_state")}
name="driver_state"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_zip")}
name="driver_zip"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_ph1")}
name="driver_ph1"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<InputPhone />
</Form.Item>
<Form.Item
label={t("contracts.fields.driver_dob")}
name="driver_dob"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.cc_num")}
name="cc_num"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.cc_expiry")}
name="cc_expiry"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("contracts.fields.cc_cardholder")}
name="cc_cardholder"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,27 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Descriptions, Card } from "antd";
import { Link } from "react-router-dom";
export default function ContractJobBlock({ job }) {
const { t } = useTranslation();
return (
<Link to={`/manage/jobs/${job && job.id}`}>
<Card title={t("jobs.labels.job")}>
<Descriptions size="small" column={1}>
<Descriptions.Item label={t("jobs.fields.ro_number")}>
{(job && job.ro_number) || ""}
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.vehicle")}>
{`${(job && job.v_model_yr) || ""} ${(job && job.v_make_desc) ||
""} ${(job && job.v_model_desc) || ""}`}
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.owner")}>
{`${(job && job.ownr_fn) || ""} ${(job && job.ownr_ln) ||
""} ${(job && job.ownr_co_nm) || ""}`}
</Descriptions.Item>
</Descriptions>
</Card>
</Link>
);
}

View File

@@ -0,0 +1,187 @@
import { Table, Input } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
export default function ContractsJobsComponent({
loading,
data,
selectedJob,
handleSelect
}) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
search: ""
});
const { t } = useTranslation();
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
width: "8%",
sorter: (a, b) =>
alphaSort(
a.ro_number ? a.ro_number : "EST-" + a.est_number,
b.ro_number ? b.ro_number : "EST-" + b.est_number
),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<span>
{record.ro_number ? record.ro_number : "EST-" + record.est_number}
</span>
)
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
width: "25%",
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.owner ? (
<span>
{record.ownr_fn} {record.ownr_ln}
</span>
) : (
<span>{`${record.ownr_fn} ${record.ownr_ln}`}</span>
);
}
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
width: "10%",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => {
return record.status || t("general.labels.na");
}
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
width: "15%",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<span>
{`${record.v_model_yr || ""} ${record.v_make_desc ||
""} ${record.v_model_desc || ""}`}
</span>
) : (
t("jobs.errors.novehicle")
);
}
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
width: "8%",
ellipsis: true,
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
render: (text, record) => {
return record.plate_no ? (
<span>{record.plate_no}</span>
) : (
t("general.labels.unknown")
);
}
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
width: "12%",
ellipsis: true,
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) => {
return record.clm_no ? (
<span>{record.clm_no}</span>
) : (
t("general.labels.unknown")
);
}
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const filteredData =
state.search === ""
? data
: data.filter(
j =>
(j.est_number || "")
.toString()
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.ro_number || "")
.toString()
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.ownr_ln || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.clm_no || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.v_model_desc || "")
.toLowerCase()
.includes(state.search.toLowerCase()) ||
(j.plate_no || "")
.toLowerCase()
.includes(state.search.toLowerCase())
);
return (
<Table
loading={loading}
title={() => (
<Input.Search
placeholder={t("general.labels.search")}
value={state.search}
onChange={e => setState({ ...state, search: e.target.value })}
/>
)}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={filteredData}
onChange={handleTableChange}
rowSelection={{
onSelect: handleSelect,
type: "radio",
selectedRowKeys: [selectedJob]
}}
/>
);
}

View File

@@ -0,0 +1,38 @@
import { useQuery } from "@apollo/react-hooks";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import ContractJobsComponent from "./contract-jobs.component";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop
});
export function ContractJobsContainer({ selectedJobState, bodyshop }) {
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"]
}
});
const [selectedJob, setSelectedJob] = selectedJobState;
const handleSelect = record => {
setSelectedJob(record.id);
};
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
<ContractJobsComponent
handleSelect={handleSelect}
selectedJob={selectedJob}
loading={loading}
data={data ? data.jobs : []}
/>
</div>
);
}
export default connect(mapStateToProps, null)(ContractJobsContainer);

View File

@@ -0,0 +1,35 @@
import React, { useState, useEffect } from "react";
import { Select } from "antd";
import { useTranslation } from "react-i18next";
const { Option } = Select;
const ContractStatusComponent = ({
value = "contracts.status.new",
onChange
}) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
useEffect(() => {
if (onChange) {
onChange(option);
}
}, [option, onChange]);
return (
<Select
value={option}
style={{
width: 100
}}
onChange={setOption}
>
<Option value="contracts.status.new">{t("contracts.status.new")}</Option>
<Option value="contracts.status.out">{t("contracts.status.out")}</Option>
<Option value="contracts.status.returned">
{t("contracts.status.returned")}
</Option>
</Select>
);
};
export default ContractStatusComponent;

View File

@@ -0,0 +1,102 @@
import { Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
import { DateFormatter } from "../../utils/DateFormatter";
export default function ContractsList({ loading, contracts }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const { t } = useTranslation();
const columns = [
{
title: t("contracts.fields.agreementnumber"),
dataIndex: "agreementnumber",
key: "agreementnumber",
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
sortOrder:
state.sortedInfo.columnKey === "agreementnumber" &&
state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
{record.agreementnumber || ""}
</Link>
)
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "job.ro_number",
key: "job.ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder:
state.sortedInfo.columnKey === "job.ro_number" &&
state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/jobs/${record.job.id}`}>
{record.job.ro_number || ""}
</Link>
)
},
{
title: t("contracts.fields.driver"),
dataIndex: "driver_ln",
key: "driver_ln",
sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln),
sortOrder:
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
render: (text, record) =>
`${record.driver_fn || ""} ${record.driver_ln || ""}`
},
{
title: t("contracts.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => t(record.status)
},
{
title: t("contracts.fields.start"),
dataIndex: "start",
key: "start",
sorter: (a, b) => alphaSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>
},
{
title: t("contracts.fields.scheduledreturn"),
dataIndex: "scheduledreturn",
key: "scheduledreturn",
sorter: (a, b) => alphaSort(a.scheduledreturn, b.scheduledreturn),
sortOrder:
state.sortedInfo.columnKey === "scheduledreturn" &&
state.sortedInfo.order,
render: (text, record) => (
<DateFormatter>{record.scheduledreturn}</DateFormatter>
)
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
loading={loading}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={contracts}
onChange={handleTableChange}
/>
);
}

View File

@@ -0,0 +1,100 @@
import { Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
import { DateFormatter } from "../../utils/DateFormatter";
export default function CourtesyCarContractListComponent({ contracts }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const { t } = useTranslation();
const columns = [
{
title: t("contracts.fields.agreementnumber"),
dataIndex: "agreementnumber",
key: "agreementnumber",
sorter: (a, b) => a.agreementnumber - b.agreementnumber,
sortOrder:
state.sortedInfo.columnKey === "agreementnumber" &&
state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/courtesycars/contracts/${record.id}`}>
{record.agreementnumber || ""}
</Link>
)
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "job.ro_number",
key: "job.ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder:
state.sortedInfo.columnKey === "job.ro_number" &&
state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/jobs/${record.job.id}`}>
{record.job.ro_number || ""}
</Link>
)
},
{
title: t("contracts.fields.driver"),
dataIndex: "driver_ln",
key: "driver_ln",
sorter: (a, b) => alphaSort(a.driver_ln, b.driver_ln),
sortOrder:
state.sortedInfo.columnKey === "driver_ln" && state.sortedInfo.order,
render: (text, record) =>
`${record.driver_fn || ""} ${record.driver_ln || ""}`
},
{
title: t("contracts.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => t(record.status)
},
{
title: t("contracts.fields.start"),
dataIndex: "start",
key: "start",
sorter: (a, b) => alphaSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.start}</DateFormatter>
},
{
title: t("contracts.fields.scheduledreturn"),
dataIndex: "scheduledreturn",
key: "scheduledreturn",
sorter: (a, b) => a.scheduledreturn - b.scheduledreturn,
sortOrder:
state.sortedInfo.columnKey === "scheduledreturn" &&
state.sortedInfo.order,
render: (text, record) => (
<DateFormatter>{record.scheduledreturn}</DateFormatter>
)
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={contracts}
onChange={handleTableChange}
/>
);
}

View File

@@ -0,0 +1,200 @@
import React from "react";
import { Form, Input, InputNumber, DatePicker, Button } from "antd";
import { useTranslation } from "react-i18next";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
export default function CourtesyCarCreateFormComponent() {
const { t } = useTranslation();
return (
<div>
<Button type="primary" htmlType="submit">
{t("general.actions.save")}
</Button>
<Form.Item
label={t("courtesycars.fields.make")}
name="make"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.model")}
name="model"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.year")}
name="year"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.plate")}
name="plate"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.color")}
name="color"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.vin")}
name="vin"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.fleetnumber")}
name="fleetnumber"
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.purchasedate")}
name="purchasedate"
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.servicestartdate")}
name="servicestartdate"
>
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.serviceenddate")}
name="serviceenddate"
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.leaseenddate")}
name="leaseenddate"
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.status")}
name="status"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<CourtesyCarStatus />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.nextservicekm")}
name="nextservicekm"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.nextservicedate")}
name="nextservicedate"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.damage")} name="damage">
<Input />
</Form.Item>
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
<Input />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.fuel")}
name="fuel"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<CourtesyCarFuelSlider />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.registrationexpires")}
name="registrationexpires"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.insuranceexpires")}
name="insuranceexpires"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
<CurrencyInput />
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import { Slider } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
const CourtesyCarFuelComponent = ({ value = 100, onChange }) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
useEffect(() => {
if (onChange) {
onChange(option);
}
}, [option, onChange]);
const marks = {
0: {
style: {
color: "#f50"
},
label: t("courtesycars.labels.fuel.empty")
},
13: t("courtesycars.labels.fuel.18"),
25: t("courtesycars.labels.fuel.14"),
38: t("courtesycars.labels.fuel.38"),
50: t("courtesycars.labels.fuel.12"),
63: t("courtesycars.labels.fuel.58"),
75: t("courtesycars.labels.fuel.34"),
88: t("courtesycars.labels.fuel.78"),
100: {
style: {
color: "#008000"
},
label: <strong>{t("courtesycars.labels.fuel.full")}</strong>
}
};
return (
<Slider
marks={marks}
defaultValue={value}
onChange={setOption}
step={null}
/>
);
};
export default CourtesyCarFuelComponent;

View File

@@ -0,0 +1,49 @@
import { Form, DatePicker, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
export default function CourtesyCarReturnModalComponent() {
const { t } = useTranslation();
return (
<div>
<Form.Item
label={t("contracts.fields.actualreturn")}
name="actualreturn"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.kmend")}
name="kmend"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.fuel")}
name={["courtesycar", "data", "fuel"]}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<CourtesyCarFuelSlider />
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,86 @@
import { Form, Modal, notification } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCourtesyCarReturn } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component";
import moment from "moment";
import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries";
import { useMutation } from "@apollo/react-hooks";
const mapStateToProps = createStructuredSelector({
courtesyCarReturnModal: selectCourtesyCarReturn,
bodyshop: selectBodyshop
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("courtesyCarReturn"))
});
export function InvoiceEnterModalContainer({
courtesyCarReturnModal,
toggleModalVisible,
bodyshop
}) {
const { visible, context, actions } = courtesyCarReturnModal;
const { t } = useTranslation();
const [form] = Form.useForm();
const [updateContract] = useMutation(RETURN_CONTRACT);
const handleFinish = values => {
console.log("Finish", values);
updateContract({
variables: {
contractId: context.contractId,
cccontract: {
kmend: values.kmend,
actualreturn: values.actualreturn,
status: "contracts.status.returned"
},
courtesycarid: context.courtesyCarId,
courtesycar: {
status: "courtesycars.status.in",
fuel: values.fuel,
mileage: values.kmend
}
}
})
.then(r => {
if (actions.refetch) actions.refetch();
toggleModalVisible();
})
.catch(error => {
notification["error"]({
message: t("contracts.errors.returning", { error: error })
});
});
};
return (
<Modal
title={t("courtesycars.labels.return")}
visible={visible}
onCancel={() => toggleModalVisible()}
width={"90%"}
okText={t("general.actions.save")}
onOk={() => form.submit()}
okButtonProps={{ htmlType: "submit" }}
>
<Form
form={form}
onFinish={handleFinish}
initialValues={{ fuel: 100, actualreturn: moment(new Date()) }}
>
<CourtesyCarReturnModalComponent />
</Form>
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(InvoiceEnterModalContainer);

View File

@@ -0,0 +1,39 @@
import React, { useState, useEffect } from "react";
import { Select } from "antd";
import { useTranslation } from "react-i18next";
const { Option } = Select;
const CourtesyCarStatusComponent = ({
value = "courtesycars.status.in",
onChange
}) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
useEffect(() => {
if (onChange) {
onChange(option);
}
}, [option, onChange]);
return (
<Select
value={option}
style={{
width: 100
}}
onChange={setOption}
>
<Option value="courtesycars.status.in">
{t("courtesycars.status.in")}
</Option>
<Option value="courtesycars.status.inservice">
{t("courtesycars.status.inservice")}
</Option>
<Option value="courtesycars.status.out">
{t("courtesycars.status.out")}
</Option>
</Select>
);
};
export default CourtesyCarStatusComponent;

View File

@@ -0,0 +1,82 @@
import { Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
export default function CourtesyCarsList({ loading, courtesycars }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const { t } = useTranslation();
const columns = [
{
title: t("courtesycars.fields.fleetnumber"),
dataIndex: "fleetnumber",
key: "fleetnumber",
sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
sortOrder:
state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.vin"),
dataIndex: "vin",
key: "vin",
sorter: (a, b) => alphaSort(a.vin, b.vin),
sortOrder: state.sortedInfo.columnKey === "vin" && state.sortedInfo.order,
render: (text, record) => (
<Link to={`/manage/courtesycars/${record.id}`}>{record.vin}</Link>
)
},
{
title: t("courtesycars.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => t(record.status)
},
{
title: t("courtesycars.fields.year"),
dataIndex: "year",
key: "year",
sorter: (a, b) => alphaSort(a.year, b.year),
sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.make"),
dataIndex: "make",
key: "make",
sorter: (a, b) => alphaSort(a.make, b.make),
sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
},
{
title: t("courtesycars.fields.model"),
dataIndex: "model",
key: "model",
sorter: (a, b) => alphaSort(a.model, b.model),
sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<Table
loading={loading}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={courtesycars}
onChange={handleTableChange}
/>
);
}

View File

@@ -0,0 +1,64 @@
import { Card } from "antd";
import React, { useState } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import styled from "styled-components";
//Combination of the following:
// /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css
import "./dashboard-grid.styles.css";
const Sdiv = styled.div`
position: absolute;
height: 80%;
width: 80%;
top: 10%;
left: 10%;
// background-color: #ffcc00;
`;
const ResponsiveReactGridLayout = WidthProvider(Responsive);
export default function DashboardGridComponent() {
const [state, setState] = useState({
layout: [
{ i: "1", x: 0, y: 0, w: 2, h: 2 },
{ i: "2", x: 2, y: 0, w: 2, h: 2 },
{ i: "3", x: 4, y: 0, w: 2, h: 2 }
]
});
const defaultProps = {
className: "layout",
breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }
// cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
// rowHeight: 100
};
// We're using the cols coming back from this to calculate where to add new items.
const onBreakpointChange = (breakpoint, cols) => {
console.log("breakpoint, cols", breakpoint, cols);
// setState({ ...state, breakpoint: breakpoint, cols: cols });
};
if (true) return null;
return (
<Sdiv>
The Grid.
<ResponsiveReactGridLayout
{...defaultProps}
onBreakpointChange={onBreakpointChange}
width='100%'
onLayoutChange={layout => {
console.log("layout", layout);
setState({ ...state, layout });
}}>
{state.layout.map((item, index) => {
return (
<Card style={{ width: "100px" }} key={item.i} data-grid={item}>
A Card {index}
</Card>
);
})}
</ResponsiveReactGridLayout>
</Sdiv>
);
}

View File

@@ -0,0 +1,126 @@
.react-resizable {
position: relative;
}
.react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");
background-position: bottom right;
padding: 0 3px 3px 0;
}
.react-resizable-handle-sw {
bottom: 0;
left: 0;
cursor: sw-resize;
transform: rotate(90deg);
}
.react-resizable-handle-se {
bottom: 0;
right: 0;
cursor: se-resize;
}
.react-resizable-handle-nw {
top: 0;
left: 0;
cursor: nw-resize;
transform: rotate(180deg);
}
.react-resizable-handle-ne {
top: 0;
right: 0;
cursor: ne-resize;
transform: rotate(270deg);
}
.react-resizable-handle-w,
.react-resizable-handle-e {
top: 50%;
margin-top: -10px;
cursor: ew-resize;
}
.react-resizable-handle-w {
left: 0;
transform: rotate(135deg);
}
.react-resizable-handle-e {
right: 0;
transform: rotate(315deg);
}
.react-resizable-handle-n,
.react-resizable-handle-s {
left: 50%;
margin-left: -10px;
cursor: ns-resize;
}
.react-resizable-handle-n {
top: 0;
transform: rotate(225deg);
}
.react-resizable-handle-s {
bottom: 0;
transform: rotate(45deg);
}
.react-grid-layout {
position: relative;
transition: height 200ms ease;
}
.react-grid-item {
transition: all 200ms ease;
transition-property: left, top;
}
.react-grid-item.cssTransforms {
transition-property: transform;
}
.react-grid-item.resizing {
z-index: 1;
will-change: width, height;
}
.react-grid-item.react-draggable-dragging {
transition: none;
z-index: 3;
will-change: transform;
}
.react-grid-item.dropping {
visibility: hidden;
}
.react-grid-item.react-grid-placeholder {
background: red;
opacity: 0.2;
transition-duration: 100ms;
z-index: 2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.react-grid-item > .react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
cursor: se-resize;
}
.react-grid-item > .react-resizable-handle::after {
content: "";
position: absolute;
right: 3px;
bottom: 3px;
width: 5px;
height: 5px;
border-right: 2px solid rgba(0, 0, 0, 0.4);
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
}
.react-resizable-hide > .react-resizable-handle {
display: none;
}

View File

@@ -0,0 +1,19 @@
import { UploadOutlined } from "@ant-design/icons";
import { Button, Upload } from "antd";
import React from "react";
export default function DocumentsUploadComponent({ handleUpload }) {
return (
<div>
<Upload
multiple={true}
customRequest={handleUpload}
accept="audio/*,video/*,image/*"
>
<Button>
<UploadOutlined /> Click to Upload
</Button>
</Upload>
</div>
);
}

View File

@@ -0,0 +1,143 @@
import { notification } from "antd";
import axios from "axios";
import React from "react";
import { useMutation } from "@apollo/react-hooks";
import { useTranslation } from "react-i18next";
import Resizer from "react-image-file-resizer";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
import {
selectBodyshop,
selectCurrentUser
} from "../../redux/user/user.selectors";
import { generateCdnThumb } from "../../utils/DocHelpers";
import DocumentsUploadComponent from "./documents-upload.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function DocumentsUploadContainer({
jobId,
invoiceId,
currentUser,
bodyshop,
callbackAfterUpload
}) {
const { t } = useTranslation();
const [insertNewDocument] = useMutation(INSERT_NEW_DOCUMENT);
const handleUpload = ev => {
const { onError, onSuccess, onProgress } = ev;
//If PDF, upload directly.
//If JPEG, resize and upload.
//TODO If this is just an invoice job? Where to put it?
let key = `${bodyshop.id}/${jobId}/${ev.file.name}`;
if (ev.file.type === "application/pdf") {
console.log("It's a PDF.");
uploadToS3(key, ev.file.type, ev.file, onError, onSuccess, onProgress);
} else {
Resizer.imageFileResizer(
ev.file,
3000,
3000,
"PNG",
75,
0,
uri => {
let file = new File([uri], ev.file.name, {});
file.uid = ev.file.uid;
uploadToS3(key, file.type, file, onError, onSuccess, onProgress);
},
"blob"
);
}
};
const uploadToS3 = (
fileName,
fileType,
file,
onError,
onSuccess,
onProgress
) => {
axios
.post("/sign_s3", {
fileName,
fileType
})
.then(response => {
var returnData = response.data.data.returnData;
var signedRequest = returnData.signedRequest;
var url = returnData.url;
// setState({ ...state, url: url });
// Put the fileType in the headers for the upload
var options = {
headers: {
"Content-Type": fileType
},
onUploadProgress: e => {
onProgress({ percent: (e.loaded / e.total) * 100 });
}
};
axios
.put(signedRequest, file, options)
.then(response => {
insertNewDocument({
variables: {
docInput: [
{
jobid: jobId,
uploaded_by: currentUser.email,
url,
thumb_url:
fileType === "application/pdf"
? "application/pdf"
: generateCdnThumb(fileName),
key: fileName,
invoiceid: invoiceId
}
]
}
}).then(r => {
onSuccess({
uid: r.data.insert_documents.returning[0].id,
url: r.data.insert_documents.returning[0].thumb_url,
name: r.data.insert_documents.returning[0].name,
status: "done",
full_url: r.data.insert_documents.returning[0].url,
key: r.data.insert_documents.returning[0].key
});
notification["success"]({
message: t("documents.successes.insert")
});
if (callbackAfterUpload) {
callbackAfterUpload();
}
});
})
.catch(error => {
onError(error);
notification["error"]({
message: t("documents.errors.insert", {
message: JSON.stringify(error)
})
});
});
})
.catch(error => {
notification["error"]({
message: t("documents.errors.getpresignurl", {
message: JSON.stringify(error)
})
});
});
};
return <DocumentsUploadComponent handleUpload={handleUpload} />;
});

View File

@@ -3,7 +3,7 @@ import { Input } from "antd";
import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
export default function SendEmailButtonComponent({
export default function EmailOverlayComponent({
messageOptions,
handleConfigChange,
handleHtmlChange
@@ -13,24 +13,29 @@ export default function SendEmailButtonComponent({
<Input
defaultValue={messageOptions.to}
onChange={handleConfigChange}
name='to'
name="to"
/>
CC
<Input
defaultValue={messageOptions.cc}
onChange={handleConfigChange}
name='cc'
name="cc"
/>
Subject
<Input
defaultValue={messageOptions.subject}
onChange={handleConfigChange}
name='subject'
name="subject"
/>
<CKEditor
editor={ClassicEditor}
data={messageOptions.html}
onChange={(event, editor) => {
// handleHtmlChange(editor.getData());
//TODO Ensure that removing onchange never introduces a race condition
}}
onBlur={(event, editor) => {
console.log("Blur.");
handleHtmlChange(editor.getData());
}}
/>

View File

@@ -1,13 +1,16 @@
import { Button, Modal, notification } from "antd";
import { Modal, notification } from "antd";
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useLazyQuery } from "react-apollo";
import { useLazyQuery } from "@apollo/react-hooks";
import ReactDOMServer from "react-dom/server";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { toggleEmailOverlayVisible } from "../../redux/email/email.actions";
import { selectEmailConfig, selectEmailVisible } from "../../redux/email/email.selectors.js";
import {
selectEmailConfig,
selectEmailVisible
} from "../../redux/email/email.selectors.js";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import EmailOverlayComponent from "./email-overlay.component";
@@ -21,11 +24,17 @@ const mapDispatchToProps = dispatch => ({
export default connect(
mapStateToProps,
mapDispatchToProps
)(function SendEmail({ emailConfig, modalVisible, toggleEmailOverlayVisible }) {
)(function EmailOverlayContainer({
emailConfig,
modalVisible,
toggleEmailOverlayVisible
}) {
const { t } = useTranslation();
const [messageOptions, setMessageOptions] = useState(
emailConfig.messageOptions
);
useEffect(() => {
setMessageOptions(emailConfig.messageOptions);
}, [setMessageOptions, emailConfig.messageOptions]);
@@ -53,7 +62,6 @@ export default connect(
html: ReactDOMServer.renderToStaticMarkup(
<emailConfig.template data={data} />
)
//html: renderEmail(<emailConfig.template data={data} />)
});
}
@@ -73,6 +81,7 @@ export default connect(
});
});
};
const handleConfigChange = event => {
const { name, value } = event.target;
setMessageOptions({ ...messageOptions, [name]: value });
@@ -82,23 +91,28 @@ export default connect(
};
return (
<div>
<Modal
destroyOnClose={true}
visible={modalVisible}
width={"80%"}
onOk={handleOk}
onCancel={() => toggleEmailOverlayVisible()}>
<LoadingSpinner loading={loading}>
<EmailOverlayComponent
handleConfigChange={handleConfigChange}
messageOptions={messageOptions}
handleHtmlChange={handleHtmlChange}
/>
</LoadingSpinner>
</Modal>
<Button onClick={() => toggleEmailOverlayVisible()}>Show</Button>
</div>
<Modal
destroyOnClose={true}
visible={modalVisible}
width={"80%"}
onOk={handleOk}
onCancel={() => toggleEmailOverlayVisible()}
>
<LoadingSpinner loading={loading}>
<EmailOverlayComponent
handleConfigChange={handleConfigChange}
messageOptions={messageOptions}
handleHtmlChange={handleHtmlChange}
/>
<button
onClick={() => {
console.log(messageOptions.html);
navigator.clipboard.writeText(messageOptions.html);
}}
>
Get HTML
</button>
</LoadingSpinner>
</Modal>
);
});

View File

@@ -0,0 +1,14 @@
import { InputNumber } from "antd";
import React, { forwardRef } from "react";
function FormItemCurrency(props, ref) {
return (
<InputNumber
{...props}
//formatter={value => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
// parser={value => value.replace(/\$\s?|(,*)/g, "")}
precision={2}
/>
);
}
export default forwardRef(FormItemCurrency);

View File

@@ -1,4 +1,5 @@
import { Icon, Input } from "antd";
import { Input } from "antd";
import { MailFilled } from "@ant-design/icons";
import React, { forwardRef } from "react";
function FormItemEmail(props, ref) {
return (
@@ -7,10 +8,10 @@ function FormItemEmail(props, ref) {
addonAfter={
props.email ? (
<a href={`mailto:${props.email}`}>
<Icon type="mail" />
<MailFilled />
</a>
) : (
<Icon type="mail" />
<MailFilled />
)
}
/>

View File

@@ -0,0 +1,52 @@
import { useNProgress } from "@tanem/react-nprogress";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectLoading } from "../../redux/application/application.selectors";
const mapStateToProps = createStructuredSelector({
loading: selectLoading
});
export default connect(mapStateToProps, null)(GlobalLoadingHeader);
function GlobalLoadingHeader({ loading }) {
const { animationDuration, isFinished, progress } = useNProgress({
isAnimating: loading
});
return (
<div
style={{
opacity: isFinished ? 0 : 1,
pointerEvents: "none",
transition: `opacity ${animationDuration}ms linear`
}}>
<div
style={{
background: "#29d",
height: 4,
left: 0,
marginLeft: `${(-1 + progress) * 100}%`,
position: "fixed",
top: 0,
transition: `margin-left ${animationDuration}ms linear`,
width: "100%",
zIndex: 1031
}}>
<div
style={{
boxShadow: "0 0 10px #29d, 0 0 5px #29d",
display: "block",
height: "100%",
opacity: 1,
position: "absolute",
right: 0,
transform: "rotate(3deg) translate(0px, -4px)",
width: 100
}}
/>
</div>
</div>
);
}

View File

@@ -1,9 +1,10 @@
import { Avatar, Col, Icon, Menu, Row } from "antd";
import Icon, { CarFilled, FileAddFilled, FileFilled, GlobalOutlined, HomeFilled, TeamOutlined } from "@ant-design/icons";
import { Avatar, Col, Menu, Row } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { FaCalendarAlt, FaCarCrash } from "react-icons/fa";
import { Link } from "react-router-dom";
import UserImage from "../../assets/User.svg";
import { signOutStart } from "../../redux/user/user.actions";
import ManageSignInButton from "../manage-sign-in-button/manage-sign-in-button.component";
export default ({
@@ -11,7 +12,8 @@ export default ({
selectedNavItem,
logo,
handleMenuClick,
currentUser
currentUser,
signOutStart
}) => {
const { t } = useTranslation();
//TODO Add
@@ -23,7 +25,7 @@ export default ({
<img alt="Shop Logo" src={logo} style={{ height: "40px" }} />
</Col>
) : null}
<Col span={14}>
<Col span={21}>
{landingHeader ? (
<Menu
theme="dark"
@@ -49,7 +51,7 @@ export default ({
</div>
}
>
<Menu.Item onClick={signOutStart()}>
<Menu.Item onClick={() => signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item>
@@ -60,7 +62,7 @@ export default ({
<Menu.SubMenu
title={
<span>
<Icon type="global" />
<GlobalOutlined />
<span>{t("menus.currentuser.languageselector")}</span>
</span>
}
@@ -87,14 +89,21 @@ export default ({
>
<Menu.Item key="home">
<Link to="/manage">
<Icon type="home" />
<HomeFilled />
{t("menus.header.home")}
</Link>
</Menu.Item>
<Menu.SubMenu title={t("menus.header.jobs")}>
<Menu.SubMenu
title={
<span>
<Icon component={FaCarCrash} />
<span>{t("menus.header.jobs")}</span>
</span>
}
>
<Menu.Item key="schedule">
<Link to="/manage/schedule">
<Icon type="calendar" />
<Icon component={FaCalendarAlt} />
{t("menus.header.schedule")}
</Link>
</Menu.Item>
@@ -110,17 +119,46 @@ export default ({
<Menu.SubMenu title={t("menus.header.customers")}>
<Menu.Item key="owners">
<Link to="/manage/owners">
<Icon type="team" />
<TeamOutlined />
{t("menus.header.owners")}
</Link>
</Menu.Item>
<Menu.Item key="vehicles">
<Link to="/manage/vehicles">
<Icon type="car" />
<CarFilled />
{t("menus.header.vehicles")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
title={
<span>
<CarFilled />
<span>{t("menus.header.courtesycars")}</span>
</span>
}
>
<Menu.Item key="courtesycarsall">
<Link to="/manage/courtesycars">
<CarFilled />
{t("menus.header.courtesycars-all")}
</Link>
</Menu.Item>
<Menu.Item key="contracts">
<Link to="/manage/courtesycars/contracts">
<FileFilled />
{t("menus.header.courtesycars-contracts")}
</Link>
</Menu.Item>
<Menu.Item key="newcontract">
<Link to="/manage/courtesycars/contracts/new">
<FileAddFilled />
{t("menus.header.courtesycars-newcontract")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={t("menus.header.shop")}>
<Menu.Item key="shop">
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
@@ -147,7 +185,7 @@ export default ({
</div>
}
>
<Menu.Item onClick={signOutStart()}>
<Menu.Item onClick={() => signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item>
@@ -158,7 +196,7 @@ export default ({
<Menu.SubMenu
title={
<span>
<Icon type="global" />
<GlobalOutlined />
<span>{t("menus.currentuser.languageselector")}</span>
</span>
}

View File

@@ -0,0 +1,36 @@
import React, { useState } from "react";
import { Button, Popover, Input, InputNumber, Form } from "antd";
import { SelectOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
export default function InvoiceAddLineButton({ jobLine, discount, disabled }) {
const [visibility, setVisibility] = useState(false);
const { t } = useTranslation();
const popContent = (
<div style={{ display: "flex" }}>
<Form.Item name="line_desc" label={t("joblines.fields.line_desc")}>
<Input />
</Form.Item>
<Form.Item name="oem_partno" label={t("joblines.fields.oem_partno")}>
<Input />
</Form.Item>
<Form.Item name="retail" label={t("invoicelines.fields.retail")}>
<InputNumber precision={2} />
</Form.Item>
<Form.Item name="actual" label={t("invoicelines.fields.actual")}>
<InputNumber precision={2} />
</Form.Item>
DISC: {discount}
<Button onClick={() => setVisibility(false)}>X</Button>
</div>
);
return (
<Popover content={popContent} visible={visibility}>
<Button onClick={() => setVisibility(true)} disabled={!disabled}>
<SelectOutlined />
</Button>
</Popover>
);
}

View File

@@ -1,71 +1,176 @@
import { Modal, Form, Input, InputNumber } from "antd";
import {
Button,
DatePicker,
Form,
Input,
Modal,
Select,
Switch,
Tag
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import ResetForm from "../form-items-formatted/reset-form-item.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import InvoiceEnterModalLinesComponent from "./invoice-enter-modal.lines.component";
export default function InvoiceEnterModalComponent({
visible,
invoice,
handleCancel,
handleSubmit,
form
handleFinish,
handleRoSelect,
roAutoCompleteOptions,
handleVendorSelect,
vendorAutoCompleteOptions,
lineData,
vendor,
job,
responsibilityCenters
}) {
const { t } = useTranslation();
const { getFieldDecorator, isFieldsTouched, resetFields } = form;
const [form] = Form.useForm();
const { resetFields } = form;
return (
<Modal
title={
invoice && invoice.id
? t("invoice.labels.edit")
: t("invoice.labels.new")
}
visible={visible}
okText={t("general.labels.save")}
onOk={handleSubmit}
onCancel={handleCancel}
>
{isFieldsTouched() ? <ResetForm resetFields={resetFields} /> : null}
<Form onSubmit={handleSubmit} autoComplete={"off"}>
{JSON.stringify(invoice)}
{
// <Form.Item label={t("joblines.fields.line_desc")}>
// {getFieldDecorator("line_desc", {
// initialValue: jobLine.line_desc
// })(<Input name="line_desc" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.oem_partno")}>
// {getFieldDecorator("oem_partno", {
// initialValue: jobLine.oem_partno
// })(<Input name="oem_partno" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.part_type")}>
// {getFieldDecorator("part_type", {
// initialValue: jobLine.part_type
// })(<Input name="part_type" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.mod_lbr_ty")}>
// {getFieldDecorator("mod_lbr_ty", {
// initialValue: jobLine.mod_lbr_ty
// })(<Input name="mod_lbr_ty" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.op_code_desc")}>
// {getFieldDecorator("op_code_desc", {
// initialValue: jobLine.op_code_desc
// })(<Input name="op_code_desc" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.mod_lb_hrs")}>
// {getFieldDecorator("mod_lb_hrs", {
// initialValue: jobLine.mod_lb_hrs
// })(<InputNumber name="mod_lb_hrs" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.act_price")}>
// {getFieldDecorator("act_price", {
// initialValue: jobLine.act_price
// })(<InputNumber name="act_price" />)}
// </Form.Item>
<Form onFinish={handleFinish} autoComplete={"off"} form={form}>
<Modal
title={
invoice && invoice.id
? t("invoices.labels.edit")
: t("invoices.labels.new")
}
</Form>
</Modal>
width={"90%"}
visible={visible}
okText={t("general.actions.save")}
onOk={() => form.submit()}
okButtonProps={{ htmlType: "submit" }}
onCancel={handleCancel}
>
<div style={{ display: "flex" }}>
<Form.Item
name="jobid"
label={t("invoices.fields.ro_number")}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Select
showSearch
defaultValue={
job ? (job.ro_number ? job.ro_number : job.est_number) : null
}
autoFocus
defaultOpen
style={{ width: "300px" }}
onSelect={handleRoSelect}
>
{roAutoCompleteOptions
? roAutoCompleteOptions.map(o => (
<Select.Option
key={o.id}
value={o.ro_number ? o.ro_number : o.est_number}
>
{`${
o.ro_number ? o.ro_number : o.est_number
} | ${o.ownr_ln || ""} ${o.ownr_fn ||
""} | ${o.v_model_yr || ""} ${o.v_make_desc ||
""} ${o.v_model_desc || ""}`}
</Select.Option>
))
: null}
</Select>
</Form.Item>
<Form.Item
label={t("invoices.fields.vendor")}
name="vendorid"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Select
showSearch
onSelect={handleVendorSelect}
style={{ width: "300px" }}
>
{vendorAutoCompleteOptions
? vendorAutoCompleteOptions.map(o => (
<Select.Option key={o.id} value={o.name}>
<div style={{ display: "flex" }}>
{o.name}
<Tag color="green">{`${o.discount * 100}%`}</Tag>
</div>
</Select.Option>
))
: null}
</Select>
</Form.Item>
<Button onClick={() => resetFields()}>
{t("general.actions.reset")}
</Button>
</div>
<div style={{ display: "flex" }}>
<Form.Item
label={t("invoices.fields.invoice_number")}
name="invoice_number"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("invoices.fields.date")}
name="date"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("invoices.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label={t("invoices.fields.total")}
name="total"
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<CurrencyInput />
</Form.Item>
</div>
<InvoiceEnterModalLinesComponent
lineData={lineData}
discount={vendor && vendor.discount}
form={form}
responsibilityCenters={responsibilityCenters}
/>
<Button onClick={() => console.log(form.getFieldsValue())}>
Field Values
</Button>
</Modal>
</Form>
);
}

View File

@@ -1,19 +1,21 @@
import { Form, notification } from "antd";
import React from "react";
import { useMutation } from "react-apollo";
import { useTranslation } from "react-i18next";
import React, { useState } from "react";
import { notification } from "antd";
import { useLazyQuery, useQuery, useMutation } from "@apollo/react-hooks";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
INSERT_NEW_JOB_LINE,
UPDATE_JOB_LINE
} from "../../graphql/jobs-lines.queries";
import { GET_JOB_LINES_TO_ENTER_INVOICE } from "../../graphql/jobs-lines.queries";
import { ACTIVE_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries";
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectInvoiceEnterModal } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import InvoiceEnterModalComponent from "./invoice-enter-modal.component";
import { INSERT_NEW_INVOICE } from "../../graphql/invoices.queries";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
invoiceEnterModal: selectInvoiceEnterModal
invoiceEnterModal: selectInvoiceEnterModal,
bodyshop: selectBodyshop
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter"))
@@ -22,71 +24,84 @@ const mapDispatchToProps = dispatch => ({
function InvoiceEnterModalContainer({
invoiceEnterModal,
toggleModalVisible,
form
bodyshop
}) {
const { t } = useTranslation();
// const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
// const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
const linesState = useState([]);
const roSearchState = useState({ text: "", selectedId: null });
const [roSearch, setRoSearch] = roSearchState;
const handleSubmit = e => {
e.preventDefault();
const [insertInvoice] = useMutation(INSERT_NEW_INVOICE);
form.validateFieldsAndScroll((err, values) => {
if (err) {
notification["error"]({
message: t("invoices.errors.validation"),
description: err.message
const { data: RoAutoCompleteData } = useQuery(ACTIVE_JOBS_FOR_AUTOCOMPLETE, {
fetchPolicy: "network-only",
variables: { statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"] },
skip: !invoiceEnterModal.visible
});
const vendorSearchState = useState({
text: "",
selectedId: null
});
const [vendorSearch, setVendorSearch] = vendorSearchState;
const { data: VendorAutoCompleteData } = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE,
{
fetchPolicy: "network-only",
skip: !invoiceEnterModal.visible
}
);
const [loadLines, { called, data: lineData }] = useLazyQuery(
GET_JOB_LINES_TO_ENTER_INVOICE,
{
fetchPolicy: "network-only",
variables: { id: roSearch.selectedId }
}
);
if (roSearch.selectedId) {
if (!called) loadLines();
}
const handleRoSelect = (value, obj) => {
setRoSearch({ ...roSearch, selectedId: obj.key });
};
const handleVendorSelect = (value, obj) => {
setVendorSearch({ ...vendorSearch, selectedId: obj.key });
};
const handleFinish = values => {
insertInvoice({
variables: {
invoice: [
Object.assign(
{},
values,
{ jobid: roSearch.selectedId },
{ vendorid: vendorSearch.selectedId },
{ invoicelines: { data: values.invoicelines } }
)
]
}
})
.then(r => {
// if (jobLineEditModal.actions.refetch)
// jobLineEditModal.actions.refetch();
// toggleModalVisible();
notification["success"]({
message: t("invoices.successes.created")
});
}
if (!err) {
alert("Closing this modal.");
toggleModalVisible();
// if (!jobLineEditModal.context.id) {
// insertJobLine({
// variables: {
// lineInput: [{ jobid: jobLineEditModal.context.jobid, ...values }]
// }
// })
// .then(r => {
// if (jobLineEditModal.actions.refetch)
// jobLineEditModal.actions.refetch();
// toggleModalVisible();
// notification["success"]({
// message: t("joblines.successes.created")
// });
// })
// .catch(error => {
// notification["error"]({
// message: t("joblines.errors.creating", {
// message: error.message
// })
// });
// });
// } else {
// updateJobLine({
// variables: {
// lineId: jobLineEditModal.context.id,
// line: values
// }
// })
// .then(r => {
// notification["success"]({
// message: t("joblines.successes.updated")
// });
// })
// .catch(error => {
// notification["success"]({
// message: t("joblines.errors.updating", {
// message: error.message
// })
// });
// });
// if (jobLineEditModal.actions.refetch)
// jobLineEditModal.actions.refetch();
// toggleModalVisible();
// }
}
});
})
.catch(error => {
console.log("error", error);
notification["error"]({
message: t("invoices.errors.creating", {
message: error.message
})
});
});
};
const handleCancel = () => {
@@ -97,9 +112,26 @@ function InvoiceEnterModalContainer({
<InvoiceEnterModalComponent
visible={invoiceEnterModal.visible}
invoice={invoiceEnterModal.context}
handleSubmit={handleSubmit}
handleFinish={handleFinish}
handleCancel={handleCancel}
form={form}
handleRoSelect={handleRoSelect}
roAutoCompleteOptions={RoAutoCompleteData && RoAutoCompleteData.jobs}
handleVendorSelect={handleVendorSelect}
vendorAutoCompleteOptions={
VendorAutoCompleteData && VendorAutoCompleteData.vendors
}
linesState={linesState}
lineData={lineData ? lineData.joblines : null}
vendor={
vendorSearch.selectedId
? VendorAutoCompleteData &&
VendorAutoCompleteData.vendors.filter(
v => v.id === vendorSearch.selectedId
)[0]
: null
}
job={invoiceEnterModal.context.job || null}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
/>
);
}
@@ -107,8 +139,4 @@ function InvoiceEnterModalContainer({
export default connect(
mapStateToProps,
mapDispatchToProps
)(
Form.create({ name: "InvoiceEnterModalContainer" })(
InvoiceEnterModalContainer
)
);
)(InvoiceEnterModalContainer);

View File

@@ -0,0 +1,242 @@
import { DeleteFilled } from "@ant-design/icons";
import { Button, Col, Form, Input, Row, Select, Tag } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
export default function InvoiceEnterModalLinesComponent({
lineData,
discount,
form,
responsibilityCenters
}) {
const { t } = useTranslation();
const { setFieldsValue, getFieldsValue } = form;
const [amounts, setAmounts] = useState({ invoiceTotal: 0, enteredAmount: 0 });
return (
<div>
<Form.List name="invoicelines">
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item required={false} key={field.key}>
<div style={{ display: "flex" }}>
<Form.Item
label={t("invoicelines.fields.line_desc")}
key={`${index}joblinename`}
name={[field.name, "joblinename"]}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Select
autoFocus
name={`le${index}`}
style={{ width: "450px" }}
onSelect={(value, opt) => {
setFieldsValue({
invoicelines: getFieldsValue([
"invoicelines"
]).invoicelines.map((item, idx) => {
if (idx === index) {
return {
...item,
joblineid: opt.key.includes("noline")
? null
: opt.key,
line_desc: opt.key.includes("noline")
? ""
: opt.value,
actual_price: opt.cost ? opt.cost : 0,
cost_center: opt.part_type
? responsibilityCenters.defaults[
opt.part_type
] || null
: null
};
}
return item;
})
});
}}
showSearch
>
<Select.Option
key={`${index}noline`}
value={t("invoicelines.labels.other")}
cost={0}
>
{t("invoicelines.labels.other")}
</Select.Option>
{lineData
? lineData.map(item => (
<Select.Option
key={item.id}
value={item.line_desc}
cost={item.act_price ? item.act_price : 0}
part_type={item.part_type}
>
<Row justify="center" align="middle">
<Col span={12}> {item.line_desc}</Col>
<Col span={8}>
<Tag color="blue">{item.oem_partno}</Tag>
</Col>
<Col span={4}>
<Tag color="green">
<CurrencyFormatter>
{item.act_price}
</CurrencyFormatter>
</Tag>
</Col>
</Row>
</Select.Option>
))
: null}
</Select>
</Form.Item>
{getFieldsValue("invoicelines").invoicelines[index] &&
getFieldsValue("invoicelines").invoicelines[index]
.joblinename &&
!getFieldsValue("invoicelines").invoicelines[index]
.joblineid ? (
<Form.Item
label={t("invoicelines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<Input />
</Form.Item>
) : null}
<Form.Item
label={t("invoicelines.fields.actual")}
key={`${index}actual_price`}
name={[field.name, "actual_price"]}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<CurrencyInput
onBlur={e => {
setFieldsValue({
invoicelines: getFieldsValue(
"invoicelines"
).invoicelines.map((item, idx) => {
if (idx === index) {
return {
...item,
actual_cost:
parseFloat(e.target.value) * (1 - discount)
};
}
return item;
})
});
}}
/>
</Form.Item>
<Form.Item
label={t("invoicelines.fields.actual_cost")}
key={`${index}actual_cost`}
name={[field.name, "actual_cost"]}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<CurrencyInput
onBlur={() =>
setAmounts({
invoiceTotal: getFieldsValue().total,
enteredTotal: getFieldsValue("invoicelines")
.invoicelines
? getFieldsValue(
"invoicelines"
).invoicelines.reduce(
(acc, value) =>
acc +
(value && value.actual_cost
? value.actual_cost
: 0),
0
)
: 0
})
}
/>
</Form.Item>
<Form.Item
label={t("invoicelines.fields.cost_center")}
key={`${index}cost_center`}
name={[field.name, "cost_center"]}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
>
<Select style={{ width: "150px" }}>
{responsibilityCenters.costs.map(item => (
<Select.Option key={item}>{item}</Select.Option>
))}
</Select>
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
</div>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("invoicelines.actions.newline")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Row>
<Col span={4}>
{t("invoicelines.labels.entered")}
<CurrencyFormatter>{amounts.enteredTotal || 0}</CurrencyFormatter>
</Col>
<Col span={4}>
{amounts.invoiceTotal - amounts.enteredTotal === 0 ? (
<Tag color="green">{t("invoicelines.labels.reconciled")}</Tag>
) : (
<Tag color="red">
{t("invoicelines.labels.unreconciled")}:
<CurrencyFormatter>
{amounts.invoiceTotal - amounts.enteredTotal}
</CurrencyFormatter>
</Tag>
)}
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,71 @@
import { Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
import { DateFormatter } from "../../utils/DateFormatter";
export default function InvoicesListTableComponent({ loading, invoices }) {
const [state, setState] = useState({
sortedInfo: {}
});
const { t } = useTranslation();
const columns = [
{
title: t("invoices.fields.vendorname"),
dataIndex: "vendorname",
key: "vendorname",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder:
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
//ellipsis: true,
render: (text, record) => <span>{record.vendor.name}</span>
},
{
title: t("invoices.fields.invoice_number"),
dataIndex: "invoice_number",
key: "invoice_number",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder:
state.sortedInfo.columnKey === "invoice_number" &&
state.sortedInfo.order
//ellipsis: true,
},
{
title: t("invoices.fields.date"),
dataIndex: "date",
key: "date",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => a.date - b.date,
sortOrder:
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
//ellipsis: true,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const rowExpander = record => (
<div style={{ margin: 0 }}>Invoice details</div>
);
return (
<Table
loading={loading}
size="small"
expandedRowRender={rowExpander}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={invoices}
onChange={handleTableChange}
/>
);
}

View File

@@ -1,12 +1,21 @@
import {
EditFilled,
FileImageFilled,
PrinterFilled,
ShoppingFilled
} from "@ant-design/icons";
import { useQuery } from "@apollo/react-hooks";
import { Button, Icon, PageHeader, Tag } from "antd";
import { Button, PageHeader, Tag } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
import ScheduleJobModalContainer from "../schedule-job-modal/schedule-job-modal.container";
//import JobDetailCardsHeaderComponent from "./job-detail-cards.header.component";
import JobDetailCardsCustomerComponent from "./job-detail-cards.customer.component";
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
@@ -17,9 +26,13 @@ import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
import "./job-detail-cards.styles.scss";
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
import ScheduleJobModalContainer from "../schedule-job-modal/schedule-job-modal.container";
export default function JobDetailCards({ selectedJob }) {
const mapDispatchToProps = dispatch => ({
setInvoiceEnterContext: context =>
dispatch(setModalContext({ context: context, modal: "invoiceEnter" }))
});
function JobDetailCards({ selectedJob, setInvoiceEnterContext }) {
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
fetchPolicy: "network-only",
variables: { id: selectedJob },
@@ -33,10 +46,10 @@ export default function JobDetailCards({ selectedJob }) {
return <div>{t("jobs.errors.nojobselected")}</div>;
}
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type='error' />;
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div className='job-cards-container'>
<div className="job-cards-container">
<NoteUpsertModal
jobId={data.jobs_by_pk.id}
visible={noteModalVisible}
@@ -52,9 +65,9 @@ export default function JobDetailCards({ selectedJob }) {
ghost={false}
onBack={() => window.history.back()}
tags={
<span key='job-status'>
<span key="job-status">
{data.jobs_by_pk.status ? (
<Tag color='blue'>{data.jobs_by_pk.status}</Tag>
<Tag color="blue">{data.jobs_by_pk.status}</Tag>
) : null}
</span>
}
@@ -73,39 +86,53 @@ export default function JobDetailCards({ selectedJob }) {
}
extra={[
<Button
key='schedule'
key="schedule"
//TODO Enabled logic based on status.
onClick={() => {
scheduleModalState[1](true);
}}>
}}
>
{t("jobs.actions.schedule")}
</Button>,
<Link
key='documents'
to={`/manage/jobs/${data.jobs_by_pk.id}#documents`}>
key="documents"
to={`/manage/jobs/${data.jobs_by_pk.id}#documents`}
>
<Button>
<Icon type='file-image' />
<FileImageFilled />
{t("jobs.actions.addDocuments")}
</Button>
</Link>,
<Button key='printing'>
<Icon type='printer' />
<Button key="printing">
<PrinterFilled />
{t("jobs.actions.printCenter")}
</Button>,
<Button
key='notes'
actiontype='addNote'
key="notes"
actiontype="addNote"
onClick={() => {
setNoteModalVisible(!noteModalVisible);
}}>
<Icon type='edit' />
}}
>
<EditFilled />
{t("jobs.actions.addNote")}
</Button>,
<Button key='postinvoices'>
<Icon type='shopping-cart' />
<Button
key="postinvoices"
onClick={() => {
setInvoiceEnterContext({
actions: { refetch: null },
context: {
job: data.jobs_by_pk
}
});
}}
>
<ShoppingFilled />
{t("jobs.actions.postInvoices")}
</Button>
]}>
]}
>
{
// loading ? (
// <LoadingSkeleton />
@@ -126,7 +153,7 @@ export default function JobDetailCards({ selectedJob }) {
// )
}
<section className='job-cards'>
<section className="job-cards">
<JobDetailCardsCustomerComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
@@ -171,3 +198,4 @@ export default function JobDetailCards({ selectedJob }) {
</div>
);
}
export default connect(null, mapDispatchToProps)(JobDetailCards);

View File

@@ -11,7 +11,8 @@ export default function JobDetailCardsCustomerComponent({ loading, data }) {
<CardTemplate
loading={loading}
title={t("jobs.labels.cards.customer")}
extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}>
extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}
>
{data ? (
<span>
<div>
@@ -34,14 +35,22 @@ export default function JobDetailCardsCustomerComponent({ loading, data }) {
)}
</div>
<div>{`${(data.owner && data.owner.preferred_contact) || ""}`}</div>
{data.vehicle ? (
<Link to={`/manage/vehicles/${data.vehicle.id}`}>
{`${data.vehicle.v_model_yr || ""} ${data.vehicle.v_make_desc ||
""} ${data.vehicle.v_model_desc || ""}`}
</Link>
) : (
<span>{t("jobs.errors.novehicle")}</span>
)}
<div>
{data.vehicle ? (
<Link to={`/manage/vehicles/${data.vehicleid}`}>
{`${data.v_model_yr || ""} ${data.v_make_desc ||
""} ${data.v_model_desc || ""}`}
a
</Link>
) : (
<span>
{`${data.v_model_yr || ""} ${data.v_make_desc ||
""} ${data.v_model_desc || ""}`}
b
</span>
)}
e
</div>
</span>
) : null}
</CardTemplate>

View File

@@ -8,7 +8,12 @@ export default function JobDetailCardsDamageComponent({ loading, data }) {
const { area_of_damage } = data;
return (
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
<Car dmg1={area_of_damage.impact1} dmg2={area_of_damage.impact2} />
{area_of_damage ? (
<Car
dmg1={area_of_damage.impact1 || null}
dmg2={area_of_damage.impact2 || null}
/>
) : t("jobs.errors.nodamage")}
</CardTemplate>
);
}

View File

@@ -18,7 +18,7 @@ export default function JobDetailCardsDocumentsComponent({ loading, data }) {
<CardTemplate
loading={loading}
title={t("jobs.labels.cards.documents")}
extraLink={`/manage/jobs/${data.id}#documents`}
extraLink={`/manage/jobs/${data.id}?documents`}
>
{data.documents.length > 0 ? (
<Carousel autoplay>

View File

@@ -1,4 +1,5 @@
import { List, Icon } from "antd";
import { List } from "antd";
import { WarningFilled, EyeInvisibleFilled } from "@ant-design/icons";
import React from "react";
import { useTranslation } from "react-i18next";
import CardTemplate from "./job-detail-cards.template.component";
@@ -13,24 +14,23 @@ export default function JobDetailCardsNotesComponent({ loading, data }) {
const { t } = useTranslation();
return (
<CardTemplate
<CardTemplate
loading={loading}
title={t("jobs.labels.cards.notes")}
extraLink={`/manage/jobs/${data.id}#notes`}>
extraLink={`/manage/jobs/${data.id}#notes`}
>
{data ? (
<Container>
<List
size='small'
size="small"
bordered
dataSource={data.notes}
renderItem={item => (
<List.Item>
{item.critical ? (
<Icon style={{ margin: 4, color: "red" }} type='warning' />
) : null}
{item.private ? (
<Icon style={{ margin: 4 }} type='eye-invisible' />
<EyeInvisibleFilled style={{ margin: 4, color: "red" }} />
) : null}
{item.private ? <WarningFilled style={{ margin: 4 }} /> : null}
{item.text}
</List.Item>
)}

View File

@@ -1,41 +1,61 @@
import React from "react";
import { useTranslation } from "react-i18next";
import CardTemplate from "./job-detail-cards.template.component";
import { Pie } from "react-chartjs-2";
import { Pie } from "@nivo/pie";
export default function JobDetailCardsPartsComponent({ loading, data }) {
const { t } = useTranslation();
const p = {
labels: ["Not Ordered", "Ordered", "Received", "Backordered"],
datasets: [
{
data: [5, 15, 10, 2],
backgroundColor: [
"rgba(255, 99, 132, 0.2)",
"rgba(54, 162, 235, 0.2)",
"rgba(255, 206, 86, 0.2)",
"rgba(75, 192, 192, 0.2)",
"rgba(153, 102, 255, 0.2)",
"rgba(255, 159, 64, 0.2)"
],
borderColor: [
"rgba(255, 99, 132, 1)",
"rgba(54, 162, 235, 1)",
"rgba(255, 206, 86, 1)",
"rgba(75, 192, 192, 1)",
"rgba(153, 102, 255, 1)",
"rgba(255, 159, 64, 1)"
],
borderWidth: 1
}
]
const commonProperties = {
width: 225,
height: 225,
margin: { top: 20, right: 30, bottom: 20, left: 30 },
animate: true
};
const cdata = [
{
id: "elixir",
label: "elixir",
value: 558,
color: "hsl(21, 70%, 50%)"
},
{
id: "erlang",
label: "erlang",
value: 443,
color: "hsl(91, 70%, 50%)"
},
{
id: "css",
label: "css",
value: 161,
color: "hsl(271, 70%, 50%)"
},
{
id: "python",
label: "python",
value: 305,
color: "hsl(33, 70%, 50%)"
},
{
id: "php",
label: "php",
value: 360,
color: "hsl(296, 70%, 50%)"
}
];
return (
<div>
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
{data ? <Pie data={p} /> : null}
<Pie
{...commonProperties}
data={cdata}
innerRadius={0.5}
padAngle={2}
cornerRadius={5}
enableRadialLabels={false}
/>
</CardTemplate>
</div>
);

View File

@@ -1,69 +1,73 @@
import { Modal, Form, Input, InputNumber } from "antd";
import React from "react";
import { Form, Input, Modal } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ResetForm from "../form-items-formatted/reset-form-item.component";
import InputCurrency from "../form-items-formatted/currency-form-item.component";
export default function JobLinesUpsertModalComponent({
visible,
jobLine,
handleOk,
handleCancel,
handleSubmit,
form
handleFinish
}) {
const { t } = useTranslation();
const { getFieldDecorator, isFieldsTouched, resetFields } = form;
const [form] = Form.useForm();
useEffect(() => {
form.resetFields();
}, [visible, form]);
return (
<Modal
title={
jobLine && jobLine.id
? t("joblines.labels.edit")
: t("joblines.labels.new")
}
visible={visible}
okText={t("general.labels.save")}
onOk={handleSubmit}
onCancel={handleCancel}
<Form
onFinish={handleFinish}
initialValues={jobLine}
autoComplete="off"
form={form}
>
{isFieldsTouched() ? <ResetForm resetFields={resetFields} /> : null}
<Form onSubmit={handleSubmit} autoComplete={"off"}>
<Form.Item label={t("joblines.fields.line_desc")}>
{getFieldDecorator("line_desc", {
initialValue: jobLine.line_desc
})(<Input name="line_desc" />)}
<Modal
title={
jobLine && jobLine.id
? t("joblines.labels.edit")
: t("joblines.labels.new")
}
visible={visible}
okText={t("general.actions.save")}
onOk={() => form.submit()}
onCancel={handleCancel}
>
<Form.Item
label={t("joblines.fields.line_desc")}
rules={[
{
required: true,
message: t("general.validation.required")
}
]}
name="line_desc"
>
<Input />
</Form.Item>
<Form.Item label={t("joblines.fields.oem_partno")}>
{getFieldDecorator("oem_partno", {
initialValue: jobLine.oem_partno
})(<Input name="oem_partno" />)}
<Form.Item label={t("joblines.fields.oem_partno")} name="oem_partno">
<Input />
</Form.Item>
<Form.Item label={t("joblines.fields.part_type")}>
{getFieldDecorator("part_type", {
initialValue: jobLine.part_type
})(<Input name="part_type" />)}
<Form.Item label={t("joblines.fields.part_type")} name="part_type">
<Input />
</Form.Item>
<Form.Item label={t("joblines.fields.mod_lbr_ty")}>
{getFieldDecorator("mod_lbr_ty", {
initialValue: jobLine.mod_lbr_ty
})(<Input name="mod_lbr_ty" />)}
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
<Input />
</Form.Item>
<Form.Item label={t("joblines.fields.op_code_desc")}>
{getFieldDecorator("op_code_desc", {
initialValue: jobLine.op_code_desc
})(<Input name="op_code_desc" />)}
<Form.Item
label={t("joblines.fields.op_code_desc")}
name="op_code_desc"
>
<Input />
</Form.Item>
<Form.Item label={t("joblines.fields.mod_lb_hrs")}>
{getFieldDecorator("mod_lb_hrs", {
initialValue: jobLine.mod_lb_hrs
})(<InputNumber name="mod_lb_hrs" />)}
<Form.Item label={t("joblines.fields.mod_lb_hrs")} name="mod_lb_hrs">
<InputCurrency />
</Form.Item>
<Form.Item label={t("joblines.fields.act_price")}>
{getFieldDecorator("act_price", {
initialValue: jobLine.act_price
})(<InputNumber name="act_price" />)}
<Form.Item label={t("joblines.fields.act_price")} name="act_price">
<InputCurrency />
</Form.Item>
</Form>
</Modal>
</Modal>
</Form>
);
}

View File

@@ -1,13 +1,10 @@
import { Form, notification } from "antd";
import { notification } from "antd";
import React from "react";
import { useMutation } from "react-apollo";
import { useMutation } from "@apollo/react-hooks";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
INSERT_NEW_JOB_LINE,
UPDATE_JOB_LINE
} from "../../graphql/jobs-lines.queries";
import { INSERT_NEW_JOB_LINE, UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
@@ -21,70 +18,56 @@ const mapDispatchToProps = dispatch => ({
function JobLinesUpsertModalContainer({
jobLineEditModal,
toggleModalVisible,
form
toggleModalVisible
}) {
const { t } = useTranslation();
const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
const handleSubmit = e => {
e.preventDefault();
form.validateFieldsAndScroll((err, values) => {
if (err) {
notification["error"]({
message: t("joblines.errors.validation"),
description: err.message
});
}
if (!err) {
if (!jobLineEditModal.context.id) {
insertJobLine({
variables: {
lineInput: [{ jobid: jobLineEditModal.context.jobid, ...values }]
}
})
.then(r => {
if (jobLineEditModal.actions.refetch)
jobLineEditModal.actions.refetch();
toggleModalVisible();
notification["success"]({
message: t("joblines.successes.created")
});
})
.catch(error => {
notification["error"]({
message: t("joblines.errors.creating", {
message: error.message
})
});
});
} else {
updateJobLine({
variables: {
lineId: jobLineEditModal.context.id,
line: values
}
})
.then(r => {
notification["success"]({
message: t("joblines.successes.updated")
});
})
.catch(error => {
notification["success"]({
message: t("joblines.errors.updating", {
message: error.message
})
});
});
const handleFinish = values => {
if (!jobLineEditModal.context.id) {
insertJobLine({
variables: {
lineInput: [{ jobid: jobLineEditModal.context.jobid, ...values }]
}
})
.then(r => {
if (jobLineEditModal.actions.refetch)
jobLineEditModal.actions.refetch();
toggleModalVisible();
notification["success"]({
message: t("joblines.successes.created")
});
})
.catch(error => {
notification["error"]({
message: t("joblines.errors.creating", {
message: error.message
})
});
});
} else {
updateJobLine({
variables: {
lineId: jobLineEditModal.context.id,
line: values
}
}
});
})
.then(r => {
notification["success"]({
message: t("joblines.successes.updated")
});
})
.catch(error => {
notification["success"]({
message: t("joblines.errors.updating", {
message: error.message
})
});
});
if (jobLineEditModal.actions.refetch) jobLineEditModal.actions.refetch();
toggleModalVisible();
}
};
const handleCancel = () => {
@@ -95,9 +78,8 @@ function JobLinesUpsertModalContainer({
<JobLinesUpdsertModal
visible={jobLineEditModal.visible}
jobLine={jobLineEditModal.context}
handleSubmit={handleSubmit}
handleFinish={handleFinish}
handleCancel={handleCancel}
form={form}
/>
);
}
@@ -105,6 +87,4 @@ function JobLinesUpsertModalContainer({
export default connect(
mapStateToProps,
mapDispatchToProps
)(
Form.create({ name: "JobsDetailPageContainer" })(JobLinesUpsertModalContainer)
);
)(JobLinesUpsertModalContainer);

View File

@@ -1,10 +1,11 @@
import { Button, Icon, Input, notification, Table } from "antd";
import { DeleteFilled, PlusCircleFilled, SyncOutlined } from "@ant-design/icons";
import { Button, notification, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.container";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
export default function JobsAvailableComponent({
loading,
@@ -124,7 +125,7 @@ export default function JobsAvailableComponent({
});
}}
>
<Icon type="delete" />
<DeleteFilled />
</Button>
<Button
onClick={() => {
@@ -132,7 +133,7 @@ export default function JobsAvailableComponent({
setModalVisible(true);
}}
>
<Icon type="plus" />
<PlusCircleFilled />
</Button>
</span>
)
@@ -169,19 +170,13 @@ export default function JobsAvailableComponent({
title={() => {
return (
<div>
<Input.Search
placeholder="Search...//TODO Implement Search"
onSearch={value => {
console.log(value);
}}
enterButton
/>
<strong>{t("jobs.labels.availablenew")}</strong>
<Button
onClick={() => {
refetch();
}}
>
<Icon type="sync" />
<SyncOutlined />
</Button>
<Button
onClick={() => {

View File

@@ -1,6 +1,6 @@
import { notification } from "antd";
import React, { useState } from "react";
import { useMutation, useQuery } from "react-apollo";
import { useMutation, useQuery } from "@apollo/react-hooks";
import { useTranslation } from "react-i18next";
import { withRouter } from "react-router-dom";
import { DELETE_ALL_AVAILABLE_NEW_JOBS, QUERY_AVAILABLE_NEW_JOBS } from "../../graphql/available-jobs.queries";

View File

@@ -1,10 +1,11 @@
import { Input, Table, Button, Icon, notification } from "antd";
import { DeleteFilled, PlusCircleFilled, SyncOutlined } from "@ant-design/icons";
import { Button, notification, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
export default function JobsAvailableSupplementComponent({
loading,
@@ -138,7 +139,7 @@ export default function JobsAvailableSupplementComponent({
});
}}
>
<Icon type="delete" />
<DeleteFilled />
</Button>
<Button
onClick={() => {
@@ -146,7 +147,7 @@ export default function JobsAvailableSupplementComponent({
setModalVisible(true);
}}
>
<Icon type="plus" />
<PlusCircleFilled />
</Button>
</span>
)
@@ -172,19 +173,13 @@ export default function JobsAvailableSupplementComponent({
title={() => {
return (
<div>
<Input.Search
placeholder="Search..."
onSearch={value => {
console.log(value);
}}
enterButton
/>
<strong>{t("jobs.labels.availablesupplements")}</strong>
<Button
onClick={() => {
refetch();
}}
>
<Icon type="sync" />
<SyncOutlined />
</Button>
<Button
onClick={() => {

View File

@@ -1,6 +1,6 @@
import { notification } from "antd";
import React, { useState } from "react";
import { useMutation, useQuery } from "react-apollo";
import { useMutation, useQuery } from "@apollo/react-hooks";
import { useTranslation } from "react-i18next";
import { withRouter } from "react-router-dom";
import {
@@ -9,8 +9,9 @@ import {
} from "../../graphql/available-jobs.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component";
import JobsAvailableSupplementComponent from "./jobs-available-supplement.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobsAvailableSupplementComponent from "./jobs-available-supplement.component";
import HeaderFields from "./jobs-available-supplement.headerfields";
export default withRouter(function JobsAvailableSupplementContainer({
deleteJob,
@@ -53,14 +54,14 @@ export default withRouter(function JobsAvailableSupplementContainer({
} else {
//create upsert job
let supp = estData.data.available_jobs_by_pk.est_data;
console.log("supp before", supp);
delete supp.joblines;
//TODO How to update the estimate lines.
delete supp.owner;
delete supp.vehicle;
if (!importOptions.overrideHeaders) {
delete supp["ins_ea"];
//TODO Remove all required fields.
if (importOptions.overrideHeaders) {
HeaderFields.forEach(item => delete supp[item]);
}
updateJob({
@@ -102,11 +103,12 @@ export default withRouter(function JobsAvailableSupplementContainer({
setSelectedJob(null);
};
if (error) return <AlertComponent type='error' message={error.message} />;
if (error) return <AlertComponent type="error" message={error.message} />;
return (
<LoadingSpinner
loading={insertLoading}
message={t("jobs.labels.creating_new_job")}>
message={t("jobs.labels.creating_new_job")}
>
<JobsAvailableSupplementComponent
loading={loading}
data={data}

View File

@@ -0,0 +1,225 @@
const headerFields = [
//AD1
"ins_co_id",
"ins_co_nm",
"ins_addr1",
"ins_addr2",
"ins_city",
"ins_st",
"ins_zip",
"ins_ctry",
"ins_ea",
"policy_no",
"ded_amt",
"ded_status",
"asgn_no",
"asgn_date",
"asgn_type",
"clm_no",
"clm_ofc_id",
"clm_ofc_nm",
"clm_addr1",
"clm_addr2",
"clm_city",
"clm_st",
"clm_zip",
"clm_ctry",
"clm_ph1",
"clm_ph1x",
"clm_ph2",
"clm_ph2x",
"clm_fax",
"clm_faxx",
"clm_ct_ln",
"clm_ct_fn",
"clm_title",
"clm_ct_ph",
"clm_ct_phx",
"clm_ea",
"payee_nms",
"pay_type",
"pay_date",
"pay_chknm",
"pay_amt",
"agt_co_id",
"agt_co_nm",
"agt_addr1",
"agt_addr2",
"agt_city",
"agt_st",
"agt_zip",
"agt_ctry",
"agt_ph1",
"agt_ph1x",
"agt_ph2",
"agt_ph2x",
"agt_fax",
"agt_faxx",
"agt_ct_ln",
"agt_ct_fn",
"agt_ct_ph",
"agt_ct_phx",
"agt_ea",
"agt_lic_no",
"loss_date",
"loss_type",
"loss_desc",
"theft_ind",
"cat_no",
"tlos_ind",
"cust_pr",
"insd_ln",
"insd_fn",
"insd_title",
"insd_co_nm",
"insd_addr1",
"insd_addr2",
"insd_city",
"insd_st",
"insd_zip",
"insd_ctry",
"insd_ph1",
"insd_ph1x",
"insd_ph2",
"insd_ph2x",
"insd_fax",
"insd_faxx",
"insd_ea",
"ownr_ln",
"ownr_fn",
"ownr_title",
"ownr_co_nm",
"ownr_addr1",
"ownr_addr2",
"ownr_city",
"ownr_st",
"ownr_zip",
"ownr_ctry",
"ownr_ph1",
"ownr_ph1x",
"ownr_ph2",
"ownr_ph2x",
"ownr_fax",
"ownr_faxx",
"ownr_ea",
"ins_ph1",
"ins_ph1x",
"ins_ph2",
"ins_ph2x",
"ins_fax",
"ins_faxx",
"ins_ct_ln",
"ins_ct_fn",
"ins_title",
"ins_ct_ph",
"ins_ct_phx",
"loss_cat",
//ad2
"clmt_ln",
"clmt_fn",
"clmt_title",
"clmt_co_nm",
"clmt_addr1",
"clmt_addr2",
"clmt_city",
"clmt_st",
"clmt_zip",
"clmt_ctry",
"clmt_ph1",
"clmt_ph1x",
"clmt_ph2",
"clmt_ph2x",
"clmt_fax",
"clmt_faxx",
"clmt_ea",
"est_co_id",
"est_co_nm",
"est_addr1",
"est_addr2",
"est_city",
"est_st",
"est_zip",
"est_ctry",
"est_ph1",
"est_ph1x",
"est_ph2",
"est_ph2x",
"est_fax",
"est_faxx",
"est_ct_ln",
"est_ct_fn",
"est_ea",
"est_lic_no",
"est_fileno",
"insp_ct_ln",
"insp_ct_fn",
"insp_addr1",
"insp_addr2",
"insp_city",
"insp_st",
"insp_zip",
"insp_ctry",
"insp_ph1",
"insp_ph1x",
"insp_ph2",
"insp_ph2x",
"insp_fax",
"insp_faxx",
"insp_ea",
"insp_code",
"insp_desc",
"insp_date",
"insp_time",
"rf_co_id",
"rf_co_nm",
"rf_addr1",
"rf_addr2",
"rf_city",
"rf_st",
"rf_zip",
"rf_ctry",
"rf_ph1",
"rf_ph1x",
"rf_ph2",
"rf_ph2x",
"rf_fax",
"rf_faxx",
"rf_ct_ln",
"rf_ct_fn",
"rf_ea",
"rf_tax_id",
"rf_lic_no",
"rf_bar_no",
"ro_in_date",
"ro_in_time",
"tar_date",
"tar_time",
"ro_cmpdate",
"ro_cmptime",
"date_out",
"time_out",
"rf_estimtr",
"mktg_type",
"mktg_src",
"loc_nm",
"loc_addr1",
"loc_addr2",
"loc_city",
"loc_st",
"loc_zip",
"loc_ctry",
"loc_ph1",
"loc_ph1x",
"loc_ph2",
"loc_ph2x",
"loc_fax",
"loc_faxx",
"loc_ct_ln",
"loc_ct_fn",
"loc_title",
"loc_ph",
"loc_phx",
"loc_ea"
];
export default headerFields;

View File

@@ -0,0 +1,290 @@
import { Collapse, Form, Input, InputNumber, Switch, DatePicker } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone from "../form-items-formatted/phone-form-item.component";
export default function JobsCreateJobsInfo({ form }) {
const { t } = useTranslation();
const { getFieldValue } = form;
return (
<div>
<Collapse defaultActiveKey="insurance">
<Collapse.Panel
key="insurance"
header={t("menus.jobsdetail.insurance")}
>
<Form.Item label={t("jobs.fields.ins_co_id")} name="ins_co_id">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.clm_no")} name="clm_no">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.regie_number")} name="regie_number">
<Input />
</Form.Item>
TODO: missing KOL field???
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<DatePicker />
</Form.Item>
CAA # seems not correct based on field mapping Class seems not correct
based on field mapping
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_city")} name="ins_city">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ct_ln")} name="ins_ct_ln">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ct_fn")} name="ins_ct_fn">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ph1")} name="ins_ph1">
<FormItemPhone customInput={Input} />
</Form.Item>
<Form.Item
label={t("jobs.fields.ins_ea")}
name="ins_ea"
rules={[
{
type: "email",
message: "This is not a valid email address."
}
]}
>
<FormItemEmail email={getFieldValue("ins_ea")} />
</Form.Item>
Appraiser Info
<Form.Item label={t("jobs.fields.est_co_nm")} name="est_co_nm">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_ct_fn")} name="est_ct_fn">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_ct_ln")} name="est_ct_ln">
<Input />
</Form.Item>
TODO: Field is pay date but title is inspection date. Likely
incorrect?
<Form.Item label={t("jobs.fields.pay_date")} name="pay_date">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_ph1")} name="est_ph1">
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.est_ea")}
name="est_ea"
rules={[
{
type: "email",
message: "This is not a valid email address."
}
]}
>
<FormItemEmail email={getFieldValue("est_ea")} />
</Form.Item>
<Form.Item
label={t("jobs.fields.selling_dealer")}
name="selling_dealer"
>
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.servicing_dealer")}
name="servicing_dealer"
>
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.selling_dealer_contact")}
name="selling_dealer_contact"
>
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.servicing_dealer_contact")}
name="servicing_dealer_contact"
>
<Input />
</Form.Item>
</Collapse.Panel>
<Collapse.Panel key="claim" header={t("menus.jobsdetail.claimdetail")}>
<Form.Item label={t("jobs.fields.csr")} name="csr">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
<Input />
</Form.Item>
TODO How to handle different taxes and marking them as exempt?
{
// <Form.Item label={t("jobs.fields.exempt")}>
// {getFieldDecorator("exempt", {
// initialValue: job.exempt
// })(<Input name='exempt' />)}
// </Form.Item>
}
<Form.Item label={t("jobs.fields.ponumber")} name="po_number">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.unitnumber")} name="unit_number">
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.specialcoveragepolicy")}
valuePropName="checked"
name="special_coverage_policy"
>
<Switch />
</Form.Item>
<Form.Item label={t("jobs.fields.kmin")} name="kmin">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.kmout")} name="kmout">
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.referralsource")}
name="referral_source"
>
<Input />
</Form.Item>
</Collapse.Panel>
<Collapse.Panel
key="financial"
header={t("menus.jobsdetail.financials")}
>
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.depreciation_taxes")}
name="depreciation_taxes"
>
<InputNumber />
</Form.Item>
TODO This is equivalent of GST payable.
<Form.Item
label={t("jobs.fields.federal_tax_payable")}
name="federal_tax_payable"
>
<InputNumber />
</Form.Item>
TODO equivalent of other customer amount
<Form.Item
label={t("jobs.fields.other_amount_payable")}
name="other_amount_payable"
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("jobs.fields.towing_payable")}
name="towing_payable"
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("jobs.fields.storage_payable")}
name="storage_payable"
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("jobs.fields.adjustment_bottom_line")}
name="adjustment_bottom_line"
>
<InputNumber />
</Form.Item>
Totals Table
<Form.Item
label={t("jobs.fields.labor_rate_desc")}
name="labor_rate_desc"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lad")} name="rate_lad">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lae")} name="rate_lae">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lar")} name="rate_lar">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_las")} name="rate_las">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_laf")} name="rate_laf">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lam")} name="rate_lam">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lag")} name="rate_lag">
<InputNumber />
</Form.Item>
Note //TODO Remove ATP rate?
<Form.Item label={t("jobs.fields.rate_atp")} name="rate_atp">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lau")} name="rate_lau">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la1")} name="rate_la1">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la2")} name="rate_la2">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la3")} name="rate_la3">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la4")} name="rate_la4">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mapa")} name="rate_mapa">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mash")} name="rate_mash">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mahw")} name="rate_mahw">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_ma2s")} name="rate_ma2s">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_ma3s")} name="rate_ma3s">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mabl")} name="rate_mabl">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_macs")} name="rate_macs">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_matd")} name="rate_matd">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_laa")} name="rate_laa">
<InputNumber />
</Form.Item>
</Collapse.Panel>
</Collapse>
</div>
);
}

View File

@@ -0,0 +1,28 @@
import React from "react";
import { Row, Col, Typography } from "antd";
import JobsCreateOwnerInfoNewComponent from "./jobs-create-owner-info.new.component";
import JobsCreateOwnerInfoSearchComponent from "./jobs-create-owner-info.search.component";
import { useTranslation } from "react-i18next";
export default function JobsCreateOwnerInfoComponent({ loading, owners }) {
const { t } = useTranslation();
return (
<div>
<Row>
<Typography.Title>{t("jobs.labels.create.ownerinfo")}</Typography.Title>
</Row>
<Row gutter={4}>
<Col span={16}>
<JobsCreateOwnerInfoSearchComponent
loading={loading}
owners={owners}
/>
</Col>
<Col span={8}>
<JobsCreateOwnerInfoNewComponent />
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,22 @@
import React, { useContext } from "react";
import JobsCreateOwnerInfoComponent from "./jobs-create-owner-info.component";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import { QUERY_SEARCH_OWNER_BY_IDX } from "../../graphql/owners.queries";
import { useQuery } from "@apollo/react-hooks";
import AlertComponent from "../alert/alert.component";
export default function JobsCreateOwnerContainer() {
const [state] = useContext(JobCreateContext);
const { loading, error, data } = useQuery(QUERY_SEARCH_OWNER_BY_IDX, {
variables: { search: `%${state.owner.search}%` },
skip: !state.owner.search
});
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<JobsCreateOwnerInfoComponent
loading={loading}
owners={data ? data.search_owner : null}
/>
);
}

View File

@@ -0,0 +1,122 @@
import { Form, Input, Checkbox, Switch } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone from "../form-items-formatted/phone-form-item.component";
export default function JobsCreateOwnerInfoNewComponent() {
const [state, setState] = useContext(JobCreateContext);
const { t } = useTranslation();
return (
<div>
<Checkbox
defaultChecked={state.owner.new}
checked={state.owner.new}
onChange={() => {
setState({
...state,
owner: {
...state.owner,
new: !state.owner.new,
selectedid: null
}
});
}}
>
{t("jobs.labels.create.newowner")}
</Checkbox>
<Form.Item
label={t("owners.fields.ownr_ln")}
name={["owner", "data", "ownr_ln"]}
>
<Input disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.ownr_fn")}
name={["owner", "data", "ownr_fn"]}
>
<Input disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.allow_text_message")}
valuePropName="checked"
name={["owner", "data", "allow_text_message"]}
>
<Switch disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.ownr_addr1")}
name={["owner", "data", "ownr_addr1"]}
>
<Input disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.ownr_addr2")}
name={["owner", "data", "ownr_addr2"]}
>
<Input disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.ownr_city")}
name={["owner", "data", "ownr_city"]}
>
<Input disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.ownr_ctry")}
name={["owner", "data", "ownr_ctry"]}
>
<Input disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.ownr_ea")}
rules={[
{
type: "email",
message: "This is not a valid email address."
}
]}
name={["owner", "data", "ownr_ea"]}
>
<FormItemEmail
//TODO Fix this email={getFieldValue("ownr_ea")}
disabled={!state.owner.new}
/>
</Form.Item>
<Form.Item
label={t("owners.fields.ownr_ph1")}
name={["owner", "data", "ownr_ph1"]}
>
<FormItemPhone customInput={Input} disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.ownr_st")}
name={["owner", "data", "ownr_st"]}
>
<Input disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.ownr_zip")}
name={["owner", "data", "ownr_zip"]}
>
<Input disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.preferred_contact")}
name={["owner", "data", "preferred_contact"]}
>
<Input disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.ownr_title")}
name={["owner", "data", "ownr_title"]}
>
<Input disabled={!state.owner.new} />
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,144 @@
import { Input, Table } from "antd";
import React, { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { alphaSort } from "../../utils/sorters";
export default function JobsCreateOwnerInfoSearchComponent({
loading,
owners
}) {
const [state, setState] = useContext(JobCreateContext);
const [tableState, setTableState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const { t } = useTranslation();
const columns = [
{
title: t("owners.fields.ownr_ln"),
dataIndex: "ownr_ln",
key: "ownr_ln",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
tableState.sortedInfo.columnKey === "ownr_ln" &&
tableState.sortedInfo.order
},
{
title: t("owners.fields.ownr_fn"),
dataIndex: "ownr_fn",
key: "ownr_fn",
sorter: (a, b) => alphaSort(a.ownr_fn, b.ownr_fn),
sortOrder:
tableState.sortedInfo.columnKey === "ownr_fn" &&
tableState.sortedInfo.order
},
{
title: t("owners.fields.ownr_addr1"),
dataIndex: "ownr_addr1",
key: "ownr_addr1",
sorter: (a, b) => alphaSort(a.ownr_addr1, b.ownr_addr1),
sortOrder:
tableState.sortedInfo.columnKey === "ownr_addr1" &&
tableState.sortedInfo.order
},
{
title: t("owners.fields.ownr_city"),
dataIndex: "ownr_city",
key: "ownr_city",
sorter: (a, b) => alphaSort(a.ownr_city, b.ownr_city),
sortOrder:
tableState.sortedInfo.columnKey === "ownr_city" &&
tableState.sortedInfo.order
},
{
title: t("owners.fields.ownr_ea"),
dataIndex: "ownr_ea",
key: "ownr_ea",
sorter: (a, b) => alphaSort(a.ownr_ea, b.ownr_ea),
sortOrder:
tableState.sortedInfo.columnKey === "ownr_ea" &&
tableState.sortedInfo.order
},
{
title: t("owners.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
render: (text, record) => (
<PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
),
sorter: (a, b) => alphaSort(a.ownr_ph1, b.ownr_ph1),
sortOrder:
tableState.sortedInfo.columnKey === "ownr_ph1" &&
tableState.sortedInfo.order
}
];
const handleTableChange = (pagination, filters, sorter) => {
setTableState({ ...tableState, filteredInfo: filters, sortedInfo: sorter });
};
//TODO Implement searching & pagination
return (
<Table
loading={loading}
title={() => {
return (
<Input.Search
placeholder="Search..."
onSearch={value => {
setState({
...state,
owner: { ...state.owner, search: value }
});
}}
enterButton
/>
);
}}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={owners}
onChange={handleTableChange}
rowSelection={{
onSelect: props => {
setState({
...state,
owner: { ...state.owner, new: false, selectedid: props.id }
});
},
type: "radio",
selectedRowKeys: [state.owner.selectedid]
}}
onRow={(record, rowIndex) => {
return {
onClick: event => {
if (record) {
if (record.id) {
setState({
...state,
owner: {
...state.owner,
new: false,
selectedid: record.id
}
});
return;
}
}
setState({
...state,
owner: { ...state.owner, selectedid: null }
});
}
};
}}
/>
);
}

View File

@@ -0,0 +1,25 @@
import { Col, Row, Typography } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import JobsCreateVehicleInfoNewComponent from "./jobs-create-vehicle-info.new.component";
import JobsCreateVehicleInfoSearchComponent from "./jobs-create-vehicle-info.search.component";
export default function JobsCreateVehicleInfoComponent({ loading, vehicles }) {
const { t } = useTranslation();
return (
<div>
<Typography.Title>{t("jobs.labels.create.vehicleinfo")}</Typography.Title>
<Row>
<Col span={16}>
<JobsCreateVehicleInfoSearchComponent
loading={loading}
vehicles={vehicles}
/>
</Col>
<Col span={8}>
<JobsCreateVehicleInfoNewComponent />
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,21 @@
import React, { useContext } from "react";
import JobsCreateVehicleInfoComponent from "./jobs-create-vehicle-info.component";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import AlertComponent from "../alert/alert.component";
import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
import { useQuery } from "@apollo/react-hooks";
export default function JobsCreateVehicleInfoContainer({ form }) {
const [state] = useContext(JobCreateContext);
const { loading, error, data } = useQuery(SEARCH_VEHICLE_BY_VIN, {
variables: { vin: `%${state.vehicle.search}%` },
skip: !state.vehicle.search
});
if (error) return <AlertComponent message={error.message} type='error' />;
return (
<JobsCreateVehicleInfoComponent
loading={loading}
vehicles={data ? data.vehicles : null}
/>
);
}

View File

@@ -0,0 +1,195 @@
import { DatePicker, Form, Input, Checkbox } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
export default function JobsCreateVehicleInfoNewComponent() {
const [state, setState] = useContext(JobCreateContext);
const { t } = useTranslation();
return (
<div>
<Checkbox
defaultChecked={state.vehicle.new}
checked={state.vehicle.new}
onChange={() => {
setState({
...state,
vehicle: {
...state.vehicle,
new: !state.vehicle.new,
selectedid: null
}
});
}}
>
{t("jobs.labels.create.newvehicle")}
</Checkbox>
<Form.Item
label={t("vehicles.fields.v_vin")}
name={["vehicle", "data", "v_vin"]}
rules={[
{
required: state.vehicle.new,
message: t("general.validation.required")
}
]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.plate_no")}
name={["vehicle", "data", "plate_no"]}
rules={[
{
required: state.vehicle.new,
message: t("general.validation.required")
}
]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.plate_st")}
name={["vehicle", "data", "plate_st"]}
rules={[
{
required: state.vehicle.new,
message: t("general.validation.required")
}
]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_type")}
name={["vehicle", "data", "v_type"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_trimcode")}
name={["vehicle", "data", "v_trimcode"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_tone")}
name={["vehicle", "data", "v_tone"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_bstyle")}
name={["vehicle", "data", "v_bstyle"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_stage")}
name={["vehicle", "data", "v_stage"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_prod_dt")}
name={["vehicle", "data", "v_prod_dt"]}
>
<DatePicker disabled={!state.vehicle.new} />
</Form.Item>
{
//TODO Add handling for paint code json
}
<Form.Item
label={t("vehicles.fields.v_paint_codes")}
name={["vehicle", "data", "v_paint_codes"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_options")}
name={["vehicle", "data", "v_options"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_model_yr")}
name={["vehicle", "data", "v_model_yr"]}
rules={[
{
required: state.vehicle.new,
message: t("general.validation.required")
}
]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_model_desc")}
name={["vehicle", "data", "v_model_desc"]}
rules={[
{
required: state.vehicle.new,
message: t("general.validation.required")
}
]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.trim_color")}
name={["vehicle", "data", "trim_color"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_mldgcode")}
name={["vehicle", "data", "v_mldgcode"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_makecode")}
name={["vehicle", "data", "v_makecode"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_make_desc")}
name={["vehicle", "data", "v_make_desc"]}
rules={[
{
required: state.vehicle.new,
message: t("general.validation.required")
}
]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_engine")}
name={["vehicle", "data", "v_engine"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_cond")}
name={["vehicle", "data", "v_cond"]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item
label={t("vehicles.fields.v_color")}
name={["vehicle", "data", "v_color"]}
rules={[
{
required: state.vehicle.new,
message: t("general.validation.required")
}
]}
>
<Input disabled={!state.vehicle.new} />
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,123 @@
import React, { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { Table, Input } from "antd";
import { Link } from "react-router-dom";
import { alphaSort } from "../../utils/sorters";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
export default function JobsCreateVehicleInfoSearchComponent({
loading,
vehicles
}) {
const [state, setState] = useContext(JobCreateContext);
const [tableState, setTableState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const { t } = useTranslation();
const columns = [
{
title: t("vehicles.fields.v_vin"),
dataIndex: "v_vin",
key: "v_vin",
sorter: (a, b) => alphaSort(a.v_vin, b.v_vin),
sortOrder:
tableState.sortedInfo.columnKey === "v_vin" &&
tableState.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/vehicles/" + record.id}>{record.v_vin}</Link>
)
},
{
title: t("vehicles.fields.description"),
dataIndex: "description",
key: "description",
render: (text, record) => {
return (
<span>{`${record.v_model_yr} ${record.v_make_desc} ${record.v_model_desc} ${record.v_color}`}</span>
);
}
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate",
key: "plate",
render: (text, record) => {
return <span>{`${record.plate_st} | ${record.plate_no}`}</span>;
}
}
];
const handleTableChange = (pagination, filters, sorter) => {
setTableState({ ...tableState, filteredInfo: filters, sortedInfo: sorter });
};
//TODO Implement searching & pagination
return (
<Table
loading={loading}
title={() => {
return (
<Input.Search
placeholder="Search..."
onSearch={value => {
setState({
...state,
vehicle: { ...state.vehicle, search: value }
});
}}
enterButton
/>
);
}}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={vehicles}
onChange={handleTableChange}
rowSelection={{
onSelect: props => {
setState({
...state,
vehicle: {
...state.vehicle,
new: false,
selectedid: props.id,
vehicleObj: props
}
});
},
type: "radio",
selectedRowKeys: [state.vehicle.selectedid]
}}
onRow={(record, rowIndex) => {
return {
onClick: event => {
if (record) {
if (record.id) {
setState({
...state,
vehicle: {
...state.vehicle,
new: false,
selectedid: record.id,
vehicleObj: record
}
});
return;
}
}
setState({
...state,
vehicle: { ...state.vehicle, selectedid: null, vehicleObj: null }
});
}
};
}}
/>
);
}

View File

@@ -1,24 +1,17 @@
import { Form, Input, Switch } from "antd";
import React, { useContext } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
export default function JobsDetailClaims({ job }) {
const form = useContext(JobDetailFormContext);
const { getFieldDecorator } = form;
const { t } = useTranslation();
return (
<div>
<Form.Item label={t("jobs.fields.csr")}>
{getFieldDecorator("csr", {
initialValue: job.csr
})(<Input name='csr' />)}
<Form.Item label={t("jobs.fields.csr")} name="csr">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_desc")}>
{getFieldDecorator("loss_desc", {
initialValue: job.loss_desc
})(<Input name='loss_desc' />)}
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
<Input />
</Form.Item>
TODO How to handle different taxes and marking them as exempt?
{
@@ -28,36 +21,27 @@ export default function JobsDetailClaims({ job }) {
// })(<Input name='exempt' />)}
// </Form.Item>
}
<Form.Item label={t("jobs.fields.ponumber")}>
{getFieldDecorator("po_number", {
initialValue: job.po_number
})(<Input name='po_number' />)}
<Form.Item label={t("jobs.fields.ponumber")} name="po_number">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.unitnumber")}>
{getFieldDecorator("unit_number", {
initialValue: job.unit_number
})(<Input name='unit_number' />)}
<Form.Item label={t("jobs.fields.unitnumber")} name="unit_number">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.specialcoveragepolicy")}>
{getFieldDecorator("special_coverage_policy", {
initialValue: job.special_coverage_policy,
valuePropName: "checked"
})(<Switch name='special_coverage_policy' />)}
<Form.Item
label={t("jobs.fields.specialcoveragepolicy")}
valuePropName="checked"
name="special_coverage_policy"
>
<Switch />
</Form.Item>
<Form.Item label={t("jobs.fields.kmin")}>
{getFieldDecorator("kmin", {
initialValue: job.kmin
})(<Input name='kmin' />)}
<Form.Item label={t("jobs.fields.kmin")} name="kmin">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.kmout")}>
{getFieldDecorator("kmout", {
initialValue: job.kmout
})(<Input name='kmout' />)}
<Form.Item label={t("jobs.fields.kmout")} name="kmout">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.referralsource")}>
{getFieldDecorator("referral_source", {
initialValue: job.referral_source
})(<Input name='referral_source' />)}
<Form.Item label={t("jobs.fields.referralsource")} name="referral_source">
<Input />
</Form.Item>
</div>
);

View File

@@ -1,89 +1,78 @@
import { DatePicker, Form } from "antd";
import moment from "moment";
import React, { useContext } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
export default function JobsDetailDatesComponent({ job }) {
const form = useContext(JobDetailFormContext);
const { getFieldDecorator } = form;
const { t } = useTranslation();
// initialValue: job.loss_date ? moment(job.loss_date) : null
// initialValue: job.date_estimated ? moment(job.date_estimated) : null
// initialValue: job.date_open ? moment(job.date_open) : null
// initialValue: job.date_scheduled ? moment(job.date_scheduled) : null
// initialValue: job.scheduled_in ? moment(job.scheduled_in) : null
// initialValue: job.actual_in ? moment(job.actual_in) : null
// initialValue: job.scheduled_completion ? moment(job.scheduled_completion) : null
// initialValue: job.actual_completion ? moment(job.actual_completion) : null
// initialValue: job.scheduled_delivery ? moment(job.scheduled_delivery) : null
// initialValue: job.actual_delivery ? moment(job.actual_delivery) : null
// initialValue: job.date_invoiced ? moment(job.date_invoiced) : null
// initialValue: job.date_closed ? moment(job.date_closed) : null
// initialValue: job.date_exported ? moment(job.date_exported) : null
return (
<div>
<Form.Item label={t("jobs.fields.loss_date")}>
{getFieldDecorator("loss_date", {
initialValue: job.loss_date ? moment(job.loss_date) : null
})(<DatePicker name="loss_date" />)}
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<DatePicker />
</Form.Item>
DAMAGE {JSON.stringify(job.area_of_damage)}
CAA # seems not correct based on field mapping Class seems not correct
based on field mapping
<Form.Item label={t("jobs.fields.date_estimated")}>
{getFieldDecorator("date_estimated", {
initialValue: job.date_estimated ? moment(job.date_estimated) : null
})(<DatePicker name="date_estimated" />)}
<Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated">
<DatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.date_open")}>
{getFieldDecorator("date_open", {
initialValue: job.date_open ? moment(job.date_open) : null
})(<DatePicker name="date_open" />)}
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
<DatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.date_scheduled")}>
{getFieldDecorator("date_scheduled", {
initialValue: job.date_scheduled ? moment(job.date_scheduled) : null
})(<DatePicker name="date_scheduled" />)}
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
<DatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.scheduled_in")}>
{getFieldDecorator("scheduled_in", {
initialValue: job.scheduled_in ? moment(job.scheduled_in) : null
})(<DatePicker name="scheduled_in" />)}
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
<DatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.actual_in")}>
{getFieldDecorator("actual_in", {
initialValue: job.actual_in ? moment(job.actual_in) : null
})(<DatePicker name="actual_in" />)}
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
<DatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.scheduled_completion")}>
{getFieldDecorator("scheduled_completion", {
initialValue: job.scheduled_completion
? moment(job.scheduled_completion)
: null
})(<DatePicker name="scheduled_completion" />)}
<Form.Item
label={t("jobs.fields.scheduled_completion")}
name="scheduled_completion"
>
<DatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.actual_completion")}>
{getFieldDecorator("actual_completion", {
initialValue: job.actual_completion
? moment(job.actual_completion)
: null
})(<DatePicker name="actual_completion" />)}
<Form.Item
label={t("jobs.fields.actual_completion")}
name="actual_completion"
>
<DatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.scheduled_delivery")}>
{getFieldDecorator("scheduled_delivery", {
initialValue: job.scheduled_delivery
? moment(job.scheduled_delivery)
: null
})(<DatePicker name="scheduled_delivery" />)}
<Form.Item
label={t("jobs.fields.scheduled_delivery")}
name="scheduled_delivery"
>
<DatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.actual_delivery")}>
{getFieldDecorator("actual_delivery", {
initialValue: job.actual_delivery ? moment(job.actual_delivery) : null
})(<DatePicker name="actual_delivery" />)}
<Form.Item
label={t("jobs.fields.actual_delivery")}
name="actual_delivery"
>
<DatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.date_invoiced")}>
{getFieldDecorator("date_invoiced", {
initialValue: job.date_invoiced ? moment(job.date_invoiced) : null
})(<DatePicker name="date_invoiced" />)}
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
<DatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.date_closed")}>
{getFieldDecorator("date_closed", {
initialValue: job.date_closed ? moment(job.date_closed) : null
})(<DatePicker name="date_closed" />)}
<Form.Item label={t("jobs.fields.date_closed")} name="date_closed">
<DatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.date_exported")}>
{getFieldDecorator("date_exported", {
initialValue: job.date_exported ? moment(job.date_exported) : null
})(<DatePicker name="date_exported" />)}
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
<DatePicker />
</Form.Item>
</div>
);

View File

@@ -1,181 +1,131 @@
import { Form, Input, InputNumber, Divider } from "antd";
import React, { useContext } from "react";
import { Divider, Form, Input, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
export default function JobsDetailFinancials({ job }) {
const form = useContext(JobDetailFormContext);
const { getFieldDecorator } = form;
const { t } = useTranslation();
return (
<div>
<Form.Item label={t("jobs.fields.ded_amt")}>
{getFieldDecorator("ded_amt", {
initialValue: job.ded_amt
})(<InputNumber name="ded_amt" />)}
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.ded_status")}>
{getFieldDecorator("ded_status", {
initialValue: job.ded_status
})(<Input name="ded_status" />)}
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.depreciation_taxes")}>
{getFieldDecorator("depreciation_taxes", {
initialValue: job.depreciation_taxes
})(<InputNumber name="depreciation_taxes" />)}
<Form.Item
label={t("jobs.fields.depreciation_taxes")}
name="depreciation_taxes"
>
<InputNumber />
</Form.Item>
TODO This is equivalent of GST payable.
<Form.Item label={t("jobs.fields.federal_tax_payable")}>
{getFieldDecorator("federal_tax_payable", {
initialValue: job.federal_tax_payable
})(<InputNumber name="federal_tax_payable" />)}
<Form.Item
label={t("jobs.fields.federal_tax_payable")}
name="federal_tax_payable"
>
<InputNumber />
</Form.Item>
TODO equivalent of other customer amount
<Form.Item label={t("jobs.fields.other_amount_payable")}>
{getFieldDecorator("other_amount_payable", {
initialValue: job.other_amount_payable
})(<InputNumber name="other_amount_payable" />)}
<Form.Item
label={t("jobs.fields.other_amount_payable")}
name="other_amount_payable"
>
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.towing_payable")}>
{getFieldDecorator("towing_payable", {
initialValue: job.towing_payable
})(<InputNumber name="towing_payable" />)}
<Form.Item label={t("jobs.fields.towing_payable")} name="towing_payable">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.storage_payable")}>
{getFieldDecorator("storage_payable", {
initialValue: job.storage_payable
})(<InputNumber name="storage_payable" />)}
<Form.Item
label={t("jobs.fields.storage_payable")}
name="storage_payable"
>
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.adjustment_bottom_line")}>
{getFieldDecorator("adjustment_bottom_line", {
initialValue: job.adjustment_bottom_line
})(<InputNumber name="adjustment_bottom_line" />)}
<Form.Item
label={t("jobs.fields.adjustment_bottom_line")}
name="adjustment_bottom_line"
>
<InputNumber />
</Form.Item>
<Divider />
Totals Table
<Form.Item label={t("jobs.fields.labor_rate_desc")}>
{getFieldDecorator("labor_rate_desc", {
initialValue: job.labor_rate_desc
})(<Input name="labor_rate_desc" />)}
<Form.Item
label={t("jobs.fields.labor_rate_desc")}
name="labor_rate_desc"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lab")}>
{getFieldDecorator("rate_lab", {
initialValue: job.rate_lab
})(<InputNumber name="rate_lab" />)}
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lad")}>
{getFieldDecorator("rate_lad", {
initialValue: job.rate_lad
})(<InputNumber name="rate_lad" />)}
<Form.Item label={t("jobs.fields.rate_lad")} name="rate_lad">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lae")}>
{getFieldDecorator("rate_lae", {
initialValue: job.rate_lae
})(<InputNumber name="rate_lae" />)}
<Form.Item label={t("jobs.fields.rate_lae")} name="rate_lae">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lar")}>
{getFieldDecorator("rate_lar", {
initialValue: job.rate_lar
})(<InputNumber name="rate_lar" />)}
<Form.Item label={t("jobs.fields.rate_lar")} name="rate_lar">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_las")}>
{getFieldDecorator("rate_las", {
initialValue: job.rate_las
})(<InputNumber name="rate_las" />)}
<Form.Item label={t("jobs.fields.rate_las")} name="rate_las">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_laf")}>
{getFieldDecorator("rate_laf", {
initialValue: job.rate_laf
})(<InputNumber name="rate_laf" />)}
<Form.Item label={t("jobs.fields.rate_laf")} name="rate_laf">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lam")}>
{getFieldDecorator("rate_lam", {
initialValue: job.rate_lam
})(<InputNumber name="rate_lam" />)}
<Form.Item label={t("jobs.fields.rate_lam")} name="rate_lam">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lag")}>
{getFieldDecorator("rate_lag", {
initialValue: job.rate_lag
})(<InputNumber name="rate_lag" />)}
<Form.Item label={t("jobs.fields.rate_lag")} name="rate_lag">
<InputNumber />
</Form.Item>
Note //TODO Remove ATP rate?
<Form.Item label={t("jobs.fields.rate_atp")}>
{getFieldDecorator("rate_atp", {
initialValue: job.rate_atp
})(<InputNumber name="rate_atp" />)}
<Form.Item label={t("jobs.fields.rate_atp")} name="rate_atp">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lau")}>
{getFieldDecorator("rate_lau", {
initialValue: job.rate_lau
})(<InputNumber name="rate_lau" />)}
<Form.Item label={t("jobs.fields.rate_lau")} name="rate_lau">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la1")}>
{getFieldDecorator("rate_la1", {
initialValue: job.rate_la1
})(<InputNumber name="rate_la1" />)}
<Form.Item label={t("jobs.fields.rate_la1")} name="rate_la1">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la2")}>
{getFieldDecorator("rate_la2", {
initialValue: job.rate_la2
})(<InputNumber name="rate_la2" />)}
<Form.Item label={t("jobs.fields.rate_la2")} name="rate_la2">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la3")}>
{getFieldDecorator("rate_la3", {
initialValue: job.rate_la3
})(<InputNumber name="rate_la3" />)}
<Form.Item label={t("jobs.fields.rate_la3")} name="rate_la3">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la4")}>
{getFieldDecorator("rate_la4", {
initialValue: job.rate_la4
})(<InputNumber name="rate_la4" />)}
<Form.Item label={t("jobs.fields.rate_la4")} name="rate_la4">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mapa")}>
{getFieldDecorator("rate_mapa", {
initialValue: job.rate_mapa
})(<InputNumber name="rate_mapa" />)}
<Form.Item label={t("jobs.fields.rate_mapa")} name="rate_mapa">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mash")}>
{getFieldDecorator("rate_mash", {
initialValue: job.rate_mash
})(<InputNumber name="rate_mash" />)}
<Form.Item label={t("jobs.fields.rate_mash")} name="rate_mash">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mahw")}>
{getFieldDecorator("rate_mahw", {
initialValue: job.rate_mahw
})(<InputNumber name="rate_mahw" />)}
<Form.Item label={t("jobs.fields.rate_mahw")} name="rate_mahw">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_ma2s")}>
{getFieldDecorator("rate_ma2s", {
initialValue: job.rate_ma2s
})(<InputNumber name="rate_ma2s" />)}
<Form.Item label={t("jobs.fields.rate_ma2s")} name="rate_ma2s">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_ma3s")}>
{getFieldDecorator("rate_ma3s", {
initialValue: job.rate_ma3s
})(<InputNumber name="rate_ma3s" />)}
<Form.Item label={t("jobs.fields.rate_ma3s")} name="rate_ma3s">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mabl")}>
{getFieldDecorator("rate_mabl", {
initialValue: job.rate_mabl
})(<InputNumber name="rate_mabl" />)}
<Form.Item label={t("jobs.fields.rate_mabl")} name="rate_mabl">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_macs")}>
{getFieldDecorator("rate_macs", {
initialValue: job.rate_macs
})(<InputNumber name="rate_macs" />)}
<Form.Item label={t("jobs.fields.rate_macs")} name="rate_macs">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_matd")}>
{getFieldDecorator("rate_matd", {
initialValue: job.rate_matd
})(<InputNumber name="rate_matd" />)}
<Form.Item label={t("jobs.fields.rate_matd")} name="rate_matd">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_laa")} name="rate_laa">
<InputNumber />
</Form.Item>
<Form.Item label={t("jobs.fields.rate_laa")}>
{getFieldDecorator("rate_laa", {
initialValue: job.rate_laa
})(<InputNumber name="rate_laa" />)}
</Form.Item>
</div>
);
}

View File

@@ -0,0 +1,30 @@
import React from "react";
import { Menu, Dropdown, Button } from "antd";
import { useTranslation } from "react-i18next";
import { DownCircleFilled } from "@ant-design/icons";
import { Link } from "react-router-dom";
export default function JobsDetailHeaderActions({ job }) {
const { t } = useTranslation();
const statusmenu = (
<Menu key="popovermenu">
<Menu.Item key="cccontract">
<Link
to={{
pathname: "/manage/courtesycars/contracts/new",
state: { jobId: job.id }
}}
>
{t("menus.jobsactions.newcccontract")}
</Link>
</Menu.Item>
</Menu>
);
return (
<Dropdown overlay={statusmenu} key="changestatus">
<Button>
{t("general.labels.actions")} <DownCircleFilled />
</Button>
</Dropdown>
);
}

View File

@@ -1,3 +1,4 @@
import { DownCircleFilled } from "@ant-design/icons";
import {
Avatar,
Badge,
@@ -5,7 +6,6 @@ import {
Checkbox,
Descriptions,
Dropdown,
Icon,
Menu,
notification,
PageHeader,
@@ -21,6 +21,9 @@ import CarImage from "../../assets/car.svg";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import BarcodePopup from "../barcode-popup/barcode-popup.component";
import OwnerTagPopoverComponent from "../owner-tag-popover/owner-tag-popover.component";
import VehicleTagPopoverComponent from "../vehicle-tag-popover/vehicle-tag-popover.component";
import JobsDetailHeaderActions from "../jobs-detail-header-actions/jobs-detail-header-actions.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -33,7 +36,6 @@ export default connect(
job,
mutationConvertJob,
refetch,
handleSubmit,
scheduleModalState,
bodyshop,
updateJobStatus
@@ -43,10 +45,10 @@ export default connect(
const tombstoneTitle = (
<div>
<Avatar size='large' alt='Vehicle Image' src={CarImage} />
{`${t("jobs.fields.ro_number")} ${
job.ro_number ? job.ro_number : t("general.labels.na")
}`}
<Avatar size="large" alt="Vehicle Image" src={CarImage} />
{job.ro_number
? `${t("jobs.fields.ro_number")} ${job.ro_number}`
: `EST-${job.est_number}`}
</div>
);
@@ -54,7 +56,8 @@ export default connect(
<Menu
onClick={e => {
updateJobStatus(e.key);
}}>
}}
>
{bodyshop.md_ro_statuses.statuses.map(item => (
<Menu.Item key={item}>{item}</Menu.Item>
))}
@@ -62,23 +65,24 @@ export default connect(
);
const menuExtra = [
<Dropdown overlay={statusmenu} key='changestatus'>
<Dropdown overlay={statusmenu} key="changestatus">
<Button>
{t("jobs.actions.changestatus")} <Icon type='down' />
{t("jobs.actions.changestatus")} <DownCircleFilled />
</Button>
</Dropdown>,
<Badge key='schedule' count={job.appointments_aggregate.aggregate.count}>
<Badge key="schedule" count={job.appointments_aggregate.aggregate.count}>
<Button
//TODO Enabled logic based on status.
onClick={() => {
setscheduleModalVisible(true);
}}>
}}
>
{t("jobs.actions.schedule")}
</Button>
</Badge>,
<Button
key='convert'
type='dashed'
key="convert"
type="dashed"
disabled={job.converted}
onClick={() => {
mutationConvertJob({
@@ -90,15 +94,13 @@ export default connect(
message: t("jobs.successes.converted")
});
});
}}>
}}
>
{t("jobs.actions.convert")}
</Button>,
<Button
type='primary'
key='submit'
htmlType='button'
onClick={handleSubmit}>
{t("general.labels.save")}
<JobsDetailHeaderActions key="actions" job={job} />,
<Button type="primary" key="submit" htmlType="submit">
{t("general.actions.save")}
</Button>
];
@@ -110,54 +112,53 @@ export default connect(
title={tombstoneTitle}
//subTitle={tombstoneSubtitle}
tags={
<span key='job-status'>
{job.status ? <Tag color='blue'>{job.status}</Tag> : null}
<Tag color='red'>
{job.owner ? (
<Link to={`/manage/owners/${job.owner.id}`}>
{`${job.ownr_co_nm || ""}${job.ownr_fn || ""} ${job.ownr_ln ||
""}`}
</Link>
) : (
t("jobs.errors.noowner")
)}
</Tag>
<Tag color='green'>
{job.vehicle ? (
<Link to={`/manage/vehicles/${job.vehicle.id}`}>
{job.vehicle.v_model_yr || t("general.labels.na")}{" "}
{job.vehicle.v_make_desc || t("general.labels.na")}{" "}
{job.vehicle.v_model_desc || t("general.labels.na")} |{" "}
{job.vehicle.plate_no || t("general.labels.na")} |{" "}
{job.vehicle.v_vin || t("general.labels.na")}
</Link>
) : null}
</Tag>
<span key="job-status">
{job.status ? <Tag color="blue">{job.status}</Tag> : null}
<OwnerTagPopoverComponent job={job} />
<VehicleTagPopoverComponent job={job} />
<BarcodePopup value={job.id} />
</span>
}
extra={menuExtra}>
<Descriptions size='small' column={5}>
<Descriptions.Item label={t("jobs.fields.repairtotal")}>
extra={menuExtra}
>
<Descriptions size="small" column={5}>
<Descriptions.Item key="total" label={t("jobs.fields.repairtotal")}>
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.customerowing")}>
<Descriptions.Item
key="custowing"
label={t("jobs.fields.customerowing")}
>
##NO BINDING YET##
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.specialcoveragepolicy")}>
<Descriptions.Item
key="scp"
label={t("jobs.fields.specialcoveragepolicy")}
>
<Checkbox checked={job.special_coverage_policy} />
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.scheduled_completion")}>
<Descriptions.Item
key="sched_comp"
label={t("jobs.fields.scheduled_completion")}
>
{job.scheduled_completion ? (
<Moment format='MM/DD/YYYY'>{job.scheduled_completion}</Moment>
<Moment format="MM/DD/YYYY">{job.scheduled_completion}</Moment>
) : null}
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.servicecar")}>
{job.service_car}
<Descriptions.Item key="servicecar" label={t("jobs.fields.servicecar")}>
{job.cccontracts &&
job.cccontracts.map(item => (
<Link
key={item.id}
to={`/manage/courtesycars/contracts/${item.id}`}
>
<div>{`${item.agreementnumber} - ${item.start} - ${item.scheduledreturn}`}</div>
</Link>
))}
</Descriptions.Item>
</Descriptions>
</PageHeader>

View File

@@ -1,146 +1,115 @@
import { Divider, Form, Input, DatePicker } from "antd";
import React, { useContext } from "react";
import { DatePicker, Divider, Form, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone from "../form-items-formatted/phone-form-item.component";
import moment from "moment";
export default function JobsDetailInsurance({ job }) {
const form = useContext(JobDetailFormContext);
const { getFieldDecorator, getFieldValue } = form;
export default function JobsDetailInsurance({ job, form }) {
const { getFieldValue } = form;
const { t } = useTranslation();
//initialValue: job.loss_date ? moment(job.loss_date) : null
console.log("job", job);
return (
<div>
<Form.Item label={t("jobs.fields.ins_co_id")}>
{getFieldDecorator("ins_co_id", {
initialValue: job.ins_co_id
})(<Input name="ins_co_id" />)}
<Form.Item label={t("jobs.fields.ins_co_id")} name="ins_co_id">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.policy_no")}>
{getFieldDecorator("policy_no", {
initialValue: job.policy_no
})(<Input name="policy_no" />)}
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.clm_no")}>
{getFieldDecorator("clm_no", {
initialValue: job.clm_no
})(<Input name="clm_no" />)}
<Form.Item label={t("jobs.fields.clm_no")} name="clm_no">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.regie_number")}>
{getFieldDecorator("regie_number", {
initialValue: job.regie_number
})(<Input name="regie_number" />)}
<Form.Item label={t("jobs.fields.regie_number")} name="regie_number">
<Input />
</Form.Item>
TODO: missing KOL field???
<Form.Item label={t("jobs.fields.loss_date")}>
{getFieldDecorator("loss_date", {
initialValue: job.loss_date ? moment(job.loss_date) : null
})(<DatePicker name="loss_date" />)}
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<DatePicker />
</Form.Item>
DAMAGE {JSON.stringify(job.area_of_damage)}
CAA # seems not correct based on field mapping Class seems not correct
based on field mapping
<Form.Item label={t("jobs.fields.ins_co_nm")}>
{getFieldDecorator("ins_co_nm", {
initialValue: job.ins_co_nm
})(<Input name="ins_co_nm" />)}
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_addr1")}>
{getFieldDecorator("ins_addr1", {
initialValue: job.ins_addr1
})(<Input name="ins_addr1" />)}
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_city")}>
{getFieldDecorator("ins_city", {
initialValue: job.ins_city
})(<Input name="ins_city" />)}
<Form.Item label={t("jobs.fields.ins_city")} name="ins_city">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ct_ln")}>
{getFieldDecorator("ins_ct_ln", {
initialValue: job.ins_ct_ln
})(<Input name="ins_ct_ln" />)}
<Form.Item label={t("jobs.fields.ins_ct_ln")} name="ins_ct_ln">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ct_fn")}>
{getFieldDecorator("ins_ct_fn", {
initialValue: job.ins_ct_fn
})(<Input name="ins_ct_fn" />)}
<Form.Item label={t("jobs.fields.ins_ct_fn")} name="ins_ct_fn">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ph1")}>
{getFieldDecorator("ins_ph1", {
initialValue: job.ins_ph1
})(<FormItemPhone customInput={Input} name="ins_ph1" />)}
<Form.Item label={t("jobs.fields.ins_ph1")} name="ins_ph1">
<FormItemPhone customInput={Input} />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_ea")}>
{getFieldDecorator("ins_ea", {
initialValue: job.ins_ea,
rules: [
{
type: "email",
message: "This is not a valid email address."
}
]
})(<FormItemEmail name="ins_ea" email={getFieldValue("ins_ea")} />)}
<Form.Item
label={t("jobs.fields.ins_ea")}
name="ins_ea"
rules={[
{
type: "email",
message: "This is not a valid email address."
}
]}
>
<FormItemEmail email={getFieldValue("ins_ea")} />
</Form.Item>
<Divider />
Appraiser Info
<Form.Item label={t("jobs.fields.est_co_nm")}>
{getFieldDecorator("est_co_nm", {
initialValue: job.est_co_nm
})(<Input name="est_co_nm" />)}
<Form.Item label={t("jobs.fields.est_co_nm")} name="est_co_nm">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_ct_fn")}>
{getFieldDecorator("est_ct_fn", {
initialValue: job.est_ct_fn
})(<Input name="est_ct_fn" />)}
<Form.Item label={t("jobs.fields.est_ct_fn")} name="est_ct_fn">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_ct_ln")}>
{getFieldDecorator("est_ct_ln", {
initialValue: job.est_ct_ln
})(<Input name="est_ct_ln" />)}
<Form.Item label={t("jobs.fields.est_ct_ln")} name="est_ct_ln">
<Input />
</Form.Item>
TODO: Field is pay date but title is inspection date. Likely incorrect?
<Form.Item label={t("jobs.fields.pay_date")}>
{getFieldDecorator("pay_date", {
initialValue: job.pay_date
})(<Input name="pay_date" />)}
<Form.Item label={t("jobs.fields.pay_date")} name="pay_date">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_ph1")}>
{getFieldDecorator("est_ph1", {
initialValue: job.est_ph1
})(<Input name="est_ph1" />)}
<Form.Item label={t("jobs.fields.est_ph1")} name="est_ph1">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.est_ea")}>
{getFieldDecorator("est_ea", {
initialValue: job.est_ea,
rules: [
{
type: "email",
message: "This is not a valid email address."
}
]
})(<FormItemEmail name="est_ea" email={getFieldValue("est_ea")} />)}
<Form.Item
label={t("jobs.fields.est_ea")}
name="est_ea"
rules={[
{
type: "email",
message: "This is not a valid email address."
}
]}
>
<FormItemEmail email={getFieldValue("est_ea")} />
</Form.Item>
<Form.Item label={t("jobs.fields.selling_dealer")}>
{getFieldDecorator("selling_dealer", {
initialValue: job.selling_dealer
})(<Input name="selling_dealer" />)}
<Form.Item label={t("jobs.fields.selling_dealer")} name="selling_dealer">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.servicing_dealer")}>
{getFieldDecorator("servicing_dealer", {
initialValue: job.servicing_dealer
})(<Input name="servicing_dealer" />)}
<Form.Item
label={t("jobs.fields.servicing_dealer")}
name="servicing_dealer"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.selling_dealer_contact")}>
{getFieldDecorator("selling_dealer_contact", {
initialValue: job.selling_dealer_contact
})(<Input name="selling_dealer_contact" />)}
<Form.Item
label={t("jobs.fields.selling_dealer_contact")}
name="selling_dealer_contact"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.servicing_dealer_contact")}>
{getFieldDecorator("servicing_dealer_contact", {
initialValue: job.servicing_dealer_contact
})(<Input name="servicing_dealer_contact" />)}
<Form.Item
label={t("jobs.fields.servicing_dealer_contact")}
name="servicing_dealer_contact"
>
<Input />
</Form.Item>
TODO: Adding servicing/selling dealer contact info?
</div>

View File

@@ -1,16 +1,15 @@
import { Button } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
toggleModalVisible,
setModalContext
} from "../../redux/modals/modals.actions";
import { setModalContext } from "../../redux/modals/modals.actions";
import InvoicesListTableComponent from "../invoices-list-table/invoices-list-table.component";
import AlertComponent from "../alert/alert.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter")),
setInvoiceEnterContext: context =>
dispatch(setModalContext({ context: context, modal: "invoiceEnter" }))
});
@@ -18,13 +17,13 @@ export default connect(
mapStateToProps,
mapDispatchToProps
)(function JobsDetailPliComponent({
toggleModalVisible,
setInvoiceEnterContext,
job
job,
invoicesQuery
}) {
return (
<div>
<div
<Button
onClick={() => {
setInvoiceEnterContext({
actions: { refetch: null },
@@ -35,7 +34,14 @@ export default connect(
}}
>
Enter Invoice
</div>
</Button>
{invoicesQuery.error ? (
<AlertComponent message={invoicesQuery.error.message} type="error" />
) : null}
<InvoicesListTableComponent
loading={invoicesQuery.loading}
invoices={invoicesQuery.data ? invoicesQuery.data.invoices : null}
/>
</div>
);
});

View File

@@ -1,7 +1,12 @@
import React from "react";
import { useQuery } from "@apollo/react-hooks";
import JobsDetailPliComponent from "./jobs-detail-pli.component";
import { QUERY_INVOICES_BY_JOBID } from "../../graphql/invoices.queries";
export default function JobsDetailPliContainer({ job }) {
console.log("job", job);
return <JobsDetailPliComponent job={job} />;
const invoicesQuery = useQuery(QUERY_INVOICES_BY_JOBID, {
variables: { jobid: job.id },
fetchPolicy: "network-only"
});
return <JobsDetailPliComponent job={job} invoicesQuery={invoicesQuery} />;
}

View File

@@ -0,0 +1,106 @@
import axios from "axios";
import React, { useEffect, useState } from "react";
import Gallery from "react-grid-gallery";
//import { Document, Page, pdfjs } from "react-pdf";
import DocumentsUploadContainer from "../documents-upload/documents-upload.container";
//import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import { Collapse } from "antd";
import { useTranslation } from "react-i18next";
//pdfjs.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.1.266/pdf.worker.min.js`;
function JobsDocumentsComponent({ data, jobId, refetch }) {
const [galleryImages, setgalleryImages] = useState([]);
const [pdfDocuments, setPdfDocuments] = useState([]);
useEffect(() => {
setgalleryImages(
data
.filter(item => item.thumb_url !== "application/pdf")
.reduce((acc, value) => {
acc.push({
src: value.url,
thumbnail: value.thumb_url,
thumbnailHeight: 150,
thumbnailWidth: 150,
isSelected: false
});
return acc;
}, [])
);
}, [data, setgalleryImages]);
useEffect(() => {
setPdfDocuments(
data
.filter(item => item.thumb_url === "application/pdf")
.reduce((acc, value) => {
acc.push({
src: value.url,
thumbnail: value.thumb_url,
thumbnailHeight: 150,
thumbnailWidth: 150,
isSelected: false
});
return acc;
}, [])
);
}, [data, setPdfDocuments]);
const { t } = useTranslation();
return (
<div className="clearfix">
<DocumentsUploadContainer jobId={jobId} callbackAfterUpload={refetch} />
<button
onClick={() => {
axios
.get("/downloadImages", {
images: galleryImages.map(i => i.src)
})
.then(r => console.log("r", r));
}}
>
Dl
</button>
<Collapse defaultActiveKey="photos">
<Collapse.Panel key="t" header="t">
<Gallery
images={pdfDocuments}
onSelectImage={(index, image) => {
setPdfDocuments(
pdfDocuments.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g
)
);
}}
/>
</Collapse.Panel>
<Collapse.Panel key="photos" header={t("documents.labels.photos")}>
<Gallery
images={galleryImages}
onSelectImage={(index, image) => {
setgalleryImages(
galleryImages.map((g, idx) =>
index === idx ? { ...g, isSelected: !g.isSelected } : g
)
);
}}
></Gallery>
</Collapse.Panel>
{
// <Collapse.Panel
// key="documents"
// header={t("documents.labels.documents")}
// >
// {pdfDocuments.map((doc, idx) => (
// <Document key={idx} loading={<LoadingSpinner />} file={doc.src}>
// <Page pageIndex={0} />
// </Document>
// ))}
// </Collapse.Panel>
}
</Collapse>
</div>
);
}
export default JobsDocumentsComponent;

Some files were not shown because too many files have changed in this diff Show More