0
.elasticbeanstalk/config.yml
Normal file
0
.elasticbeanstalk/config.yml
Normal file
92
_reference/AuditTriggerFunctions.sql
Normal file
92
_reference/AuditTriggerFunctions.sql
Normal 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.
|
||||
|
||||
-- Here’s 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
3
client/debug.log
Normal 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)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Alert component should render Alert component 1`] = `ShallowWrapper {}`;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {}`;
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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} />);
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Icon } from "antd";
|
||||
import Icon from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import { MdRemoveCircleOutline } from "react-icons/md";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) ||
|
||||
[]
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
32
client/src/components/chat-dock/chat-dock.container.jsx
Normal file
32
client/src/components/chat-dock/chat-dock.container.jsx
Normal 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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
119
client/src/components/contract-cars/contract-cars.component.jsx
Normal file
119
client/src/components/contract-cars/contract-cars.component.jsx
Normal 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]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
264
client/src/components/contract-form/contract-form.component.jsx
Normal file
264
client/src/components/contract-form/contract-form.component.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
187
client/src/components/contract-jobs/contract-jobs.component.jsx
Normal file
187
client/src/components/contract-jobs/contract-jobs.component.jsx
Normal 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]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
126
client/src/components/dashboard-grid/dashboard-grid.styles.css
Normal file
126
client/src/components/dashboard-grid/dashboard-grid.styles.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
});
|
||||
@@ -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());
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
0
client/src/components/email-overlay/email-setup.md
Normal file
0
client/src/components/email-overlay/email-setup.md
Normal 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);
|
||||
@@ -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 />
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user