Merged in heroku-staging (pull request #2)

Heroku staging
This commit is contained in:
Snapt Software
2020-04-01 23:33:20 +00:00
527 changed files with 33800 additions and 4494 deletions

View File

@@ -1,5 +1,7 @@
React App: React App:
React Hooks are used for Authentication ONLY to ensure the correct web token is passed.
Yarn Dependency Management:
To force upgrades for some packages: yarn upgrade-interactive --latest
GraphQL API: GraphQL API:
Hasura is hosted on another dyno. Several environmental variables are required, including disabling the console. Hasura is hosted on another dyno. Several environmental variables are required, including disabling the console.
@@ -9,4 +11,4 @@ To Start Hasura CLI:
npx hasura console --admin-secret Dev-BodyShopAppBySnaptSoftware! npx hasura console --admin-secret Dev-BodyShopAppBySnaptSoftware!
Migrating to Staging: Migrating to Staging:
npx hasura migrate apply --up 10 --endpoint https://bodyshop-staging-db.herokuapp.com/ --admin-secret Staging-BodyShopAppBySnaptSoftware! npx hasura migrate apply --up 10 --endpoint https://bodyshop-staging-db.herokuapp.com/ --admin-secret Staging-BodyShopAppBySnaptSoftware!

View File

@@ -1,5 +1,51 @@
**Required items** **Required items**
-Bodyshop Record -Bodyshop Record
..\*Include the statuses file in the format of:
```json
{
"statuses": [
"Open",
"Scheduled",
"Arrived",
"Repair Plan",
"Parts",
"Body",
"Prep",
"Paint",
"Reassembly",
"Sublet",
"Detail",
"Completed",
"Delivered",
"Invoiced",
"Exported"
],
"open_statuses": [
"Open",
"Scheduled",
"Arrived",
"Repair Plan",
"Parts",
"Body",
"Prep",
"Paint",
"Reassembly",
"Sublet",
"Detail",
"Completed"
],
"default_arrived": "Arrived",
"default_exported": "Exported",
"default_imported": "Open",
"default_invoiced": "Invoiced",
"default_completed": "Completed",
"default_delivered": "Delivered",
"default_scheduled": "Scheduled"
}
```
--\* Set the region for the shop.
-Counter Record - type: ronum -Counter Record - type: ronum

View File

@@ -0,0 +1,192 @@
{
"OP0": {
"desc": "REMOVE / REPLACE PARTIAL",
"opcode": "OP11",
"partcode": "PAA"
},
"OP1": {
"desc": "REFINISH / REPAIR",
"opcode": "OP1",
"partcode": "PAE"
},
"OP10": {
"desc": "REPAIR , PARTIAL",
"opcode": "OP9",
"partcode": "PAE"
},
"OP100": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP101": {
"desc": "REMOVE/REPLACE RECYCLED PART",
"opcode": "OP11",
"partcode": "PAL"
},
"OP103": {
"desc": "REMOVE / REPLACE PARTIAL",
"opcode": "OP11",
"partcode": "PAA"
},
"OP104": {
"desc": "REMOVE / REPLACE PARTIAL LABOUR",
"opcode": "OP11",
"partcode": "PAA"
},
"OP105": {
"desc": "!!ADJUST MANUALLY!!",
"opcode": "OP99",
"partcode": "PAE"
},
"OP106": {
"desc": "REPAIR , PARTIAL",
"opcode": "OP9",
"partcode": "PAE"
},
"OP107": {
"desc": "CHIPGUARD",
"opcode": "OP6",
"partcode": "PAE"
},
"OP108": {
"desc": "MULTI TONE",
"opcode": "OP6",
"partcode": "PAE"
},
"OP109": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP11": {
"desc": "REMOVE / REPLACE",
"opcode": "OP11",
"partcode": "PAN"
},
"OP110": {
"desc": "REFINISH / REPAIR",
"opcode": "OP1",
"partcode": "PAE"
},
"OP111": {
"desc": "REMOVE / REPLACE",
"opcode": "OP11",
"partcode": "PAN"
},
"OP112": {
"desc": "REMOVE / REPLACE",
"opcode": "OP11",
"partcode": "PAA"
},
"OP113": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP114": {
"desc": "REPLACE PRE-PRICED",
"opcode": "OP11",
"partcode": "PAA"
},
"OP12": {
"desc": "REMOVE / REPLACE PARTIAL",
"opcode": "OP11",
"partcode": "PAN"
},
"OP120": {
"desc": "REPAIR , PARTIAL",
"opcode": "OP9",
"partcode": "PAE"
},
"OP13": {
"desc": "ADDITIONAL COSTS",
"opcode": "OP13",
"partcode": "PAE"
},
"OP14": {
"desc": "ADDITIONAL OPERATIONS",
"opcode": "OP14",
"partcode": "PAE"
},
"OP15": {
"desc": "BLEND",
"opcode": "OP15",
"partcode": "PAE"
},
"OP16": {
"desc": "SUBLET",
"opcode": "OP16",
"partcode": "PAS"
},
"OP17": {
"desc": "POLICY LIMIT ADJUSTMENT",
"opcode": "OP9",
"partcode": "PAE"
},
"OP18": {
"desc": "APPEAR ALLOWANCE",
"opcode": "OP7",
"partcode": "PAE"
},
"OP2": {
"desc": "REMOVE / INSTALL",
"opcode": "OP2",
"partcode": "PAE"
},
"OP24": {
"desc": "CHIPGUARD",
"opcode": "OP6",
"partcode": "PAE"
},
"OP25": {
"desc": "TWO TONE",
"opcode": "OP6",
"partcode": "PAE"
},
"OP26": {
"desc": "PAINTLESS DENT REPAIR",
"opcode": "OP16",
"partcode": "PAE"
},
"OP260": {
"desc": "SUBLET",
"opcode": "OP16",
"partcode": "PAE"
},
"OP3": {
"desc": "ADDITIONAL LABOR",
"opcode": "OP9",
"partcode": "PAE"
},
"OP4": {
"desc": "ALIGNMENT",
"opcode": "OP4",
"partcode": "PAS"
},
"OP5": {
"desc": "OVERHAUL",
"opcode": "OP5",
"partcode": "PAE"
},
"OP6": {
"desc": "REFINISH",
"opcode": "OP6",
"partcode": "PAE"
},
"OP7": {
"desc": "INSPECT",
"opcode": "OP7",
"partcode": "PAE"
},
"OP8": {
"desc": "CHECK / ADJUST",
"opcode": "OP8",
"partcode": "PAE"
},
"OP9": {
"desc": "REPAIR",
"opcode": "OP9",
"partcode": "PAE"
}
}

View File

@@ -9,3 +9,6 @@ Bucket=
__React Based__ __React Based__
REACT_APP_GRAPHQL_ENDPOINT REACT_APP_GRAPHQL_ENDPOINT
REACT_APP_GRAPHQL_ENDPOINT_WS REACT_APP_GRAPHQL_ENDPOINT_WS
__MetaData__
Region based OpCodes

File diff suppressed because it is too large Load Diff

View File

@@ -4,33 +4,44 @@
"private": true, "private": true,
"proxy": "https://localhost:5000", "proxy": "https://localhost:5000",
"dependencies": { "dependencies": {
"antd": "^3.26.0", "@ckeditor/ckeditor5-build-classic": "^16.0.0",
"@ckeditor/ckeditor5-react": "^2.1.0",
"antd": "^3.26.8",
"apollo-boost": "^0.4.4", "apollo-boost": "^0.4.4",
"apollo-link-context": "^1.0.19", "apollo-link-context": "^1.0.19",
"apollo-link-error": "^1.1.12", "apollo-link-error": "^1.1.12",
"apollo-link-logger": "^1.2.3", "apollo-link-logger": "^1.2.3",
"apollo-link-ws": "^1.0.19", "apollo-link-ws": "^1.0.19",
"axios": "^0.19.1", "axios": "^0.19.2",
"chart.js": "^2.9.3", "chart.js": "^2.9.3",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"firebase": "^7.5.0", "firebase": "^7.8.1",
"graphql": "^14.5.8", "graphql": "^14.6.0",
"i18next": "^19.0.2", "i18next": "^19.1.0",
"node-sass": "^4.13.0", "node-sass": "^4.13.1",
"react": "^16.12.0", "react": "^16.12.0",
"react-apollo": "^3.1.3", "react-apollo": "^3.1.3",
"react-chartjs-2": "^2.8.0", "react-barcode": "^1.4.0",
"react-big-calendar": "^0.23.0",
"react-chartjs-2": "^2.9.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-i18next": "^11.2.7", "react-html-email": "^3.0.0",
"react-icons": "^3.8.0", "react-i18next": "^11.3.1",
"react-icons": "^3.9.0",
"react-image-file-resizer": "^0.2.1", "react-image-file-resizer": "^0.2.1",
"react-moment": "^0.9.7", "react-moment": "^0.9.7",
"react-number-format": "^4.3.1", "react-number-format": "^4.3.1",
"react-redux": "^7.1.3",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "3.2.0", "react-scripts": "3.3.1",
"react-trello": "^2.2.3", "redux": "^4.0.5",
"styled-components": "^4.4.1", "redux-logger": "^3.0.6",
"subscriptions-transport-ws": "^0.9.16" "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"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View File

@@ -1,6 +1,7 @@
{ {
"short_name": "Bodyshop", "short_name": "Bodyshop.app",
"name": "Bodyshop Management System", "name": "Bodyshop Management System",
"description": "The ultimate bodyshop management system",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
@@ -20,6 +21,6 @@
], ],
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"theme_color": "#002366", "theme_color": "#fff",
"background_color": "#000000" "background_color": "#fff"
} }

View File

@@ -1,23 +1,18 @@
import React, { Component } from "react"; import { ApolloLink } from "apollo-boost";
import { InMemoryCache } from "apollo-cache-inmemory";
import App from "./App";
import Spin from "../components/loading-spinner/loading-spinner.component";
import ApolloClient from "apollo-client"; import ApolloClient from "apollo-client";
import { split } from "apollo-link"; import { split } from "apollo-link";
import { setContext } from "apollo-link-context";
import { HttpLink } from "apollo-link-http"; import { HttpLink } from "apollo-link-http";
import apolloLogger from "apollo-link-logger";
import { WebSocketLink } from "apollo-link-ws"; import { WebSocketLink } from "apollo-link-ws";
import { getMainDefinition } from "apollo-utilities"; import { getMainDefinition } from "apollo-utilities";
import { InMemoryCache } from "apollo-cache-inmemory"; import React, { Component } from "react";
import { setContext } from "apollo-link-context";
import { resolvers, typeDefs } from "../graphql/resolvers";
import apolloLogger from "apollo-link-logger";
import { ApolloLink } from "apollo-boost";
import { ApolloProvider } from "react-apollo"; import { ApolloProvider } from "react-apollo";
import { persistCache } from "apollo-cache-persist"; import SpinnerComponent from "../components/loading-spinner/loading-spinner.component";
import initialState from "../graphql/initial-state";
//import { shouldRefreshToken, refreshToken } from "../graphql/middleware"; //import { shouldRefreshToken, refreshToken } from "../graphql/middleware";
import errorLink from "../graphql/apollo-error-handling"; import errorLink from "../graphql/apollo-error-handling";
import App from "./App";
class AppContainer extends Component { class AppContainer extends Component {
state = { state = {
@@ -69,14 +64,8 @@ class AppContainer extends Component {
); );
const authLink = setContext((_, { headers }) => { const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
// return the headers to the context so httpLink can read them
if (token) { if (token) {
// if (shouldRefreshToken) {
// refreshToken();
// }
return { return {
headers: { headers: {
...headers, ...headers,
@@ -99,31 +88,13 @@ class AppContainer extends Component {
const client = new ApolloClient({ const client = new ApolloClient({
link: ApolloLink.from(middlewares), link: ApolloLink.from(middlewares),
cache, cache,
typeDefs,
resolvers,
connectToDevTools: true connectToDevTools: true
}); });
client.writeData({
data: initialState
});
try {
await persistCache({
cache,
storage: window.sessionStorage,
debug: true
});
} catch (error) {
console.error("Error restoring Apollo cache", error);
}
this.setState({ this.setState({
client, client,
loaded: true loaded: true
}); });
//Init local state.
} }
componentWillUnmount() {} componentWillUnmount() {}
@@ -132,7 +103,7 @@ class AppContainer extends Component {
const { client, loaded } = this.state; const { client, loaded } = this.state;
if (!loaded) { if (!loaded) {
return <Spin />; return <SpinnerComponent />;
} }
return ( return (

View File

@@ -1,113 +1,50 @@
import React, { useEffect, Suspense, lazy, useState } from "react";
import { useApolloClient, useQuery } from "@apollo/react-hooks";
import { Switch, Route, Redirect } from "react-router-dom";
import firebase from "../firebase/firebase.utils";
import i18next from "i18next"; import i18next from "i18next";
import React, { lazy, Suspense, useEffect } from "react";
import "./App.css"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports //Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import AlertComponent from "../components/alert/alert.component"; import { checkUserSession } from "../redux/user/user.actions";
import ErrorBoundary from "../components/error-boundary/error-boundary.component"; import { selectCurrentUser } from "../redux/user/user.selectors";
import { auth } from "../firebase/firebase.utils";
import { UPSERT_USER } from "../graphql/user.queries";
import { GET_CURRENT_USER, GET_LANGUAGE } from "../graphql/local.queries";
// import { QUERY_BODYSHOP } from "../graphql/bodyshop.queries"; // import { QUERY_BODYSHOP } from "../graphql/bodyshop.queries";
import PrivateRoute from "../utils/private-route"; import PrivateRoute from "../utils/private-route";
import "./App.css";
const LandingPage = lazy(() => import("../pages/landing/landing.page")); const LandingPage = lazy(() => import("../pages/landing/landing.page"));
const ManagePage = lazy(() => import("../pages/manage/manage.page")); const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page")); const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const Unauthorized = lazy(() => const Unauthorized = lazy(() =>
import("../pages/unauthorized/unauthorized.component") import("../pages/unauthorized/unauthorized.component")
); );
export default () => { const mapStateToProps = createStructuredSelector({
const apolloClient = useApolloClient(); currentUser: selectCurrentUser
const [loaded, setloaded] = useState(false); });
const mapDispatchToProps = dispatch => ({
checkUserSession: () => dispatch(checkUserSession())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(({ checkUserSession, currentUser }) => {
useEffect(() => { useEffect(() => {
//Run the auth code only on the first render. checkUserSession();
const unsubscribeFromAuth = auth.onAuthStateChanged(async user => { return () => {};
console.log("Auth State Changed."); }, [checkUserSession]);
setloaded(true); const { t } = useTranslation();
if (user) { if (currentUser && currentUser.language)
let token; i18next.changeLanguage(currentUser.language, (err, t) => {
token = await user.getIdToken();
const idTokenResult = await user.getIdTokenResult();
const hasuraClaim =
idTokenResult.claims["https://hasura.io/jwt/claims"];
if (!hasuraClaim) {
// Check if refresh is required.
const metadataRef = firebase
.database()
.ref("metadata/" + user.uid + "/refreshTime");
metadataRef.on("value", async () => {
// Force refresh to pick up the latest custom claims changes.
token = await user.getIdToken(true);
});
}
//add the bearer token to the headers.
localStorage.setItem("token", token);
const now = new Date();
window.sessionStorage.setItem(`lastTokenRefreshTime`, now);
// window.sessionStorage.setItem("user", user);
apolloClient
.mutate({
mutation: UPSERT_USER,
variables: { authEmail: user.email, authToken: user.uid }
})
.then()
.catch(error => {
console.log("User login upsert error.", error);
});
apolloClient.writeData({
data: {
currentUser: {
email: user.email,
displayName: user.displayName,
token,
uid: user.uid,
photoUrl: user.photoURL,
__typename: "currentUser"
}
}
});
} else {
apolloClient.writeData({ data: { currentUser: null } });
localStorage.removeItem("token");
}
});
return function cleanup() {
unsubscribeFromAuth();
};
}, [apolloClient]);
const HookCurrentUser = useQuery(GET_CURRENT_USER);
const HookLanguage = useQuery(GET_LANGUAGE);
if (!loaded) return <LoadingSpinner />;
if (HookCurrentUser.loading || HookLanguage.loading)
return <LoadingSpinner />;
if (HookCurrentUser.error || HookLanguage.error)
return (
<AlertComponent
message={HookCurrentUser.error.message || HookLanguage.error.message}
/>
);
if (HookLanguage.data.language)
i18next.changeLanguage(HookLanguage.data.language, (err, t) => {
if (err) if (err)
return console.log("Error encountered when changing languages.", err); return console.log("Error encountered when changing languages.", err);
}); });
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;
}
return ( return (
<div> <div>
<Switch> <Switch>
@@ -115,19 +52,12 @@ export default () => {
<Suspense fallback={<LoadingSpinner />}> <Suspense fallback={<LoadingSpinner />}>
<Route exact path='/' component={LandingPage} /> <Route exact path='/' component={LandingPage} />
<Route exact path='/unauthorized' component={Unauthorized} /> <Route exact path='/unauthorized' component={Unauthorized} />
<Route
exact <Route exact path='/signin' component={SignInPage} />
path='/signin'
render={() =>
HookCurrentUser.data.currentUser ? (
<Redirect to='/manage' />
) : (
<SignInPage />
)
}
/>
<PrivateRoute <PrivateRoute
isAuthorized={HookCurrentUser.data.currentUser ? true : false} //isAuthorized={HookCurrentUser.data.currentUser ? true : false}
isAuthorized={currentUser.authorized}
path='/manage' path='/manage'
component={ManagePage} component={ManagePage}
/> />
@@ -136,4 +66,4 @@ export default () => {
</Switch> </Switch>
</div> </div>
); );
}; });

View File

@@ -0,0 +1,787 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1668"
height="1160"
id="svg2"
version="1.1"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="unfolded_car.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.71043165"
inkscape:cx="463.20424"
inkscape:cy="602.99002"
inkscape:document-units="px"
inkscape:current-layer="primary"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="false"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="car"
inkscape:label="CAR"
transform="translate(253.99998,-253.99995)"
style="display:inline">
<g
id="g4113"
transform="translate(-13.779768,3.524026)">
<path
sodipodi:nodetypes="csssscccsssscsccscccsscccssc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3070"
d="M 748.57143,752.85714 C 790,737.14285 888.57143,741.42857 940,740 c 51.42857,-1.42857 160.4745,10.23062 201.4286,27.14286 40.9958,16.92944 134.7843,67.65586 151.4285,72.85714 22.8572,7.14286 41.4286,7.14286 80,20 38.5715,12.85714 25.7143,32.85714 25.7143,32.85714 l -30,-4.28571 -5.7562,52.92008 c 0,0 37.1848,1.36563 41.4705,15.65135 4.2857,14.28571 5.7143,31.42857 -2.8571,41.42857 -8.5715,9.99997 -14.2857,-1.42857 -18.5715,12.85717 -4.2857,14.2857 -2.8571,28.5714 -27.1428,27.1428 -24.2857,-1.4285 -98.5715,0 -98.5715,0 0,0 -15.7142,-108.5714 -98.5714,-105.71426 -82.8571,2.85715 -95.7143,105.71426 -95.7143,105.71426 H 562.85714 c 0,0 -5.71428,-104.28569 -97.14286,-105.71426 -91.42857,-1.42857 -98.57142,105.71426 -98.57142,105.71426 H 301.42857 L 282.85714,1000 c -0.51524,0 -26.24328,-10e-6 -21.42857,-17.14285 4.65143,-16.56149 -4.28571,-41.42858 17.14286,-41.42858 21.42857,0 47.14286,1.42857 47.14286,1.42857 L 341.42857,898.57143 300,895.71429 c 0,0 34.28571,-24.28572 118.57143,-32.85715 84.28571,-8.57143 157.14286,-8.57143 192.85714,-31.42857 35.71429,-22.85714 137.14286,-78.57143 137.14286,-78.57143 z"
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3846"
d="m 282.85714,1000 h 92.85715"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3850"
d="M 555.71429,1000 H 1072.8571"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3852"
d="m 1245.7143,1000 h 151.4286"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csccc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3884"
d="M 618.57143,847.14286 C 634.28572,828.57143 741.94515,765.61839 770,758.57143 c 29.50156,-7.41035 103.00398,-7.14286 103.00398,-7.14286 l -7.14285,95.71429 z"
style="fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3888"
d="m 658.57143,817.14286 v 28.57143"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccccc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3892"
d="m 898.69729,752.83617 -4.28572,94.32767 h 207.16383 c -11.3076,-20.75266 -46.6124,-74.9056 -72.8572,-88.57143 -14.2857,-10 -82.87805,-4.32767 -130.02091,-5.75624 z"
style="fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cscsc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3896"
d="m 1065.7143,760 c 0,0 80,84.28571 85.7143,87.14286 5.7143,2.85714 115.7143,1.42857 115.7143,1.42857 0,0 -77.1429,-47.14286 -102.8572,-58.57143 -25.7143,-11.42857 -90,-31.42857 -98.5714,-30 z"
style="fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3900"
d="m 599.63544,837.66076 c -14.07595,30.96709 -18.29873,71.78734 -18.29873,94.30886 0,22.52152 4.22279,91.49368 22.52152,105.56958"
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path3900-6"
d="m 632.78482,993.12906 c -1.40759,5.63038 -4.04683,81.90444 -6.51012,106.93324 -3.67029,18.7146 -4.98821,51.2184 -6.15823,87.3149 0,22.5215 7.03798,80.2329 12.66836,105.5696"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3920"
d="m 357.52911,998.12658 c 0,0 15.48355,-85.86329 106.97722,-85.86329 91.49367,0 109.7924,87.27089 109.7924,87.27089"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3920-5"
d="m 800.28863,1253.5341 c 0,0 15.48355,-85.8633 106.97722,-85.8633 91.49364,0 109.79245,87.2709 109.79245,87.2709"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3940"
d="M 323.74684,943.23038 H 387.0886"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cssc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3942"
d="m 1033.1747,746.16708 c 18.2988,12.66836 56.3038,50.67343 92.9012,104.16203 36.5975,53.48861 8.4456,59.11899 -18.2987,74.60254 -26.7443,15.48354 -49.2658,42.22784 -67.5645,112.60755"
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path3944"
d="M 1112.7747,1196.0455 H 983.27594"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3946"
d="M 540.51646,941.82278 H 1085.2557"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path3948"
d="m 1062.7342,791.21013 v 54.8962"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
rx="2.9330556"
ry="7.3789682"
y="1144.9166"
x="558.65344"
height="14.541238"
width="41.285542"
id="rect3950"
style="display:inline;fill:#e6e6e6;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
rx="2.9330556"
ry="7.3789682"
y="1144.9166"
x="816.24335"
height="14.541238"
width="41.285542"
id="rect3950-4"
style="display:inline;fill:#e6e6e6;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
ry="7.691968"
rx="1.6302098"
y="1146.318"
x="259.53336"
height="11.738417"
width="18.776392"
id="rect4014"
style="fill:#ffcb00;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<g
id="g4451"
transform="translate(-13.779768,15.524026)">
<path
inkscape:transform-center-x="-1.6185511"
transform="translate(-37.23036,423.94932)"
d="m 99.997791,388.63797 -11.711946,16.12011 -18.950327,-6.15733 0,-19.92556 18.950327,-6.15733 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="true"
sodipodi:arg2="0.62831853"
sodipodi:arg1="0"
sodipodi:r2="13.712585"
sodipodi:r1="16.949688"
sodipodi:cy="388.63797"
sodipodi:cx="83.048103"
sodipodi:sides="5"
id="path4141"
style="fill:none;stroke:#000000;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="star" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path4143"
d="m 31.741795,745.02273 315.301265,-111.2"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path4145"
d="M 30.3342,888.59742 345.63546,1004.0202"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccssccccc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4165"
d="m 595.41266,378.78481 140.75949,38.00506 c 0,0 -8.44557,49.22222 -8.44557,71.9633 0,20.14574 0,124.39619 0,147.62151 0,30.96708 8.44557,78.82532 8.44557,78.82532 l -140.7595,33.78228 c 0,0 -17.24303,-61.90221 -17.24303,-92.90127 0.38411,-33.80191 1.75909,-154.79945 2.1114,-185.80253 -1.4076,-30.96709 15.13164,-91.49367 15.13164,-91.49367 z"
style="fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csccsc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4175"
d="m 736.17216,416.78987 c 0,0 94.30885,5.63038 152.02025,5.63038 106.97721,0 201.28609,-5.63038 201.28609,-5.63038 m -1.4076,297.00254 c 0,0 -77.4177,-5.63039 -199.87849,-5.63039 -68.97215,0 -152.02026,7.03798 -152.02026,7.03798"
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
ry="34.43626"
rx="31.189682"
y="729.20502"
x="528.6228"
height="181.57974"
width="106.97722"
id="rect4177"
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccccccc"
inkscape:connector-curvature="0"
id="path4135-9"
d="M 345.77724,632.78476 H 51.589892 l -14.07595,102.75443 -7.03797,11.26076 v 142.16709 l 7.03797,14.07595 15.48355,101.34681 H 341.55445"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csscccssccc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4203"
d="m 1086.6633,416.78987 c 0,0 -12.6684,47.85823 -12.6684,73.19494 0,25.33671 0,121.05316 0,146.38987 0,25.33671 12.6684,77.41773 12.6684,77.41773 l 205.5089,38.00505 108.3848,10e-6 c 0,0 14.0759,-81.64051 14.0759,-115.42279 0,-33.78227 0,-109.7924 0,-147.79746 0,-38.00507 -14.0759,-109.79241 -14.0759,-109.79241 h -108.3848 z"
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csscsccsc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4205"
d="m 1097.9241,435.08861 c 0,0 -8.4456,40.82025 -8.4456,56.3038 0,15.48354 0,125.27594 0,144.98227 0,19.70633 7.038,57.7114 7.038,57.7114 0,0 94.3088,26.7443 123.8683,26.7443 29.5595,0 42.2279,0 42.2279,0 V 408.3443 c 0,0 -22.5216,0 -47.8583,0 -25.3367,0 -116.8303,26.74431 -116.8303,26.74431 z"
style="fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4207"
d="m 1292.1722,378.78481 c 0,0 30.967,40.82025 30.967,111.2 0,70.37975 0,81.64051 0,146.38987 0,64.74937 -30.967,116.83038 -30.967,116.83038"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4209"
d="m 578.52152,489.98481 32.37468,-32.37468 v -40.82026 112.6076"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path4209-7"
d="m 323.81774,865.03792 32.37468,-32.37468 v -40.82026 112.6076"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path4209-76"
d="m 1005.0937,787.62021 -32.37469,-32.37468 v -40.82026 112.6076"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cssc"
transform="translate(-253.99998,253.99995)"
inkscape:connector-curvature="0"
id="path4243"
d="m 595.41266,378.78481 c 0,0 -42.22785,78.82532 -42.22785,111.2 0,32.37468 0,105.56962 0,146.38987 0,40.82026 42.22785,114.01519 42.22785,114.01519"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<path
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -106.79241,740.91638 1.4076,-91.49367 c 0,0 5.630382,-23.92911 -25.33671,-25.3367 -30.96709,-1.4076 -26.74431,2.81518 -26.74431,2.81518 l 1.40759,415.24051 c 0,0 9.85317,0 28.1519,0 28.151917,0 22.52153,-22.5216 22.52153,-22.5216 l -1e-5,-95.71637 c -12.67694,0 -11.26075,-9.61416 -11.26075,-16.89115 0,-16.94968 -10e-6,-136.5367 -10e-6,-149.20506 0.45035,-18.89009 9.85317,-16.89114 9.85317,-16.89114 z"
id="path4245"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccsccsccccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -155.98734,646.16195 h 49.26582"
id="path4247"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -155.13671,1020.4303 h 49.26582"
id="path4247-2"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -156.69114,705.05816 h 49.26582"
id="path4247-1"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2.20000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -153.87595,964.87082 h 49.26582"
id="path4247-0"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M -147.09621,703.82272 V 964.22778"
id="path4281-1"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M -137.24304,705.38727 V 965.79233"
id="path4281-8"
inkscape:connector-curvature="0" />
<g
id="g4428"
transform="translate(-13.779768,15.524026)">
<path
sodipodi:nodetypes="csssscccsssscsccscccsscccssc"
inkscape:connector-curvature="0"
id="path3070-9"
d="m 494.57145,641.6137 c 41.42857,15.71429 140,11.42857 191.42857,12.85714 51.42857,1.42857 160.4745,-10.23062 201.4286,-27.14286 40.9958,-16.92944 134.78428,-67.65586 151.42848,-72.85714 22.8572,-7.14286 41.4286,-7.14286 80,-20 38.5715,-12.85714 25.7143,-32.85714 25.7143,-32.85714 l -30,4.28571 -5.7562,-52.92008 c 0,0 37.1848,-1.36563 41.4705,-15.65135 4.2857,-14.28571 5.7143,-31.42857 -2.8571,-41.42857 -8.5715,-9.99997 -14.2857,1.42857 -18.5715,-12.85717 -4.2857,-14.2857 -2.8571,-28.5714 -27.1428,-27.1428 -24.2857,1.4285 -98.5715,0 -98.5715,0 0,0 -15.71418,108.5714 -98.57138,105.71426 -82.8571,-2.85715 -95.7143,-105.71426 -95.7143,-105.71426 H 308.85716 c 0,0 -5.71428,104.28569 -97.14286,105.71426 -91.42857,1.42857 -98.57142,-105.71426 -98.57142,-105.71426 H 47.428587 l -18.57143,38.5714 c -0.51524,0 -26.243277,10e-6 -21.428567,17.14285 4.65143,16.56149 -4.28571,41.42858 17.14286,41.42858 21.428567,0 47.142857,-1.42857 47.142857,-1.42857 l 15.71428,44.28571 -41.42857,2.85714 c 0,0 34.28571,24.28572 118.571433,32.85715 84.28571,8.57143 157.14286,8.57143 192.85714,31.42857 35.71429,22.85714 137.14286,78.57143 137.14286,78.57143 z"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3846-2"
d="M 28.857157,394.47084 H 121.71431"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3850-6"
d="M 301.71431,394.47084 H 818.85712"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3852-6"
d="M 991.71432,394.47084 H 1143.1429"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csccc"
inkscape:connector-curvature="0"
id="path3884-4"
d="m 364.57145,547.32798 c 15.71429,18.57143 123.37372,81.52447 151.42857,88.57143 29.50156,7.41035 103.00398,7.14286 103.00398,7.14286 l -7.14285,-95.71429 z"
style="display:inline;fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3888-9"
d="M 404.57145,577.32798 V 548.75655"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path3892-5"
d="M 644.69731,641.63467 640.41159,547.307 h 207.16383 c -11.3076,20.75266 -46.6124,74.9056 -72.8572,88.57143 -14.2857,10 -82.87805,4.32767 -130.02091,5.75624 z"
style="display:inline;fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cscsc"
inkscape:connector-curvature="0"
id="path3896-0"
d="m 811.71432,634.47084 c 0,0 80,-84.28571 85.7143,-87.14286 5.7143,-2.85714 115.71428,-1.42857 115.71428,-1.42857 0,0 -77.14288,47.14286 -102.85718,58.57143 -25.7143,11.42857 -90,31.42857 -98.5714,30 z"
style="display:inline;fill:#f0ffeb;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="csc"
inkscape:connector-curvature="0"
id="path3900-4"
d="m 345.63546,556.81008 c -14.07595,-30.96709 -18.29873,-71.78734 -18.29873,-94.30886 0,-22.52152 4.22279,-91.49368 22.52152,-105.56958"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path3900-6-8"
d="m 632.78482,655.34173 c -1.40759,-5.63038 -4.04683,-81.90444 -6.51012,-106.93324 -3.67029,-18.7146 -4.98821,-51.2184 -6.15823,-87.3149 0,-22.5215 7.03798,-80.2329 12.66836,-105.5696"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3920-7"
d="m 103.52913,396.34426 c 0,0 15.48355,85.86329 106.97722,85.86329 91.49367,0 109.7924,-87.27089 109.7924,-87.27089"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3920-5-1"
d="m 800.28863,394.93669 c 0,0 15.48355,85.8633 106.97722,85.8633 91.49364,0 109.79245,-87.2709 109.79245,-87.2709"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path3940-7"
d="M 69.746857,451.24046 H 133.08862"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cssc"
inkscape:connector-curvature="0"
id="path3942-2"
d="m 779.17472,648.30376 c 18.2988,-12.66836 56.3038,-50.67343 92.9012,-104.16203 36.5975,-53.48861 8.4456,-59.11899 -18.2987,-74.60254 -26.7443,-15.48354 -49.2658,-42.22784 -67.5645,-112.60755"
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0"
id="path3944-7"
d="M 1112.7747,452.42529 H 983.27594"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3946-2"
d="M 286.51648,452.64806 H 831.25572"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path3948-2"
d="m 808.73422,603.26071 v -54.8962"
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="scale(1,-1)"
rx="2.9330556"
ry="7.3789682"
y="-503.55417"
x="558.65344"
height="14.541238"
width="41.285542"
id="rect3950-6"
style="display:inline;fill:#e6e6e6;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="scale(1,-1)"
rx="2.9330556"
ry="7.3789682"
y="-503.55417"
x="816.24329"
height="14.541238"
width="41.285542"
id="rect3950-4-1"
style="display:inline;fill:#e6e6e6;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="scale(1,-1)"
ry="7.691968"
rx="1.6302098"
y="-502.1528"
x="259.53333"
height="11.738417"
width="18.776392"
id="rect4014-0"
style="display:inline;fill:#ffcb00;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
transform="translate(941.34179,284.00501)"
id="path4335"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="59.118988"
cy="211.28101"
r="16.89114" />
</g>
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M -126.57469,704.93158 V 965.33664"
id="path4281-8-0"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffc0;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -153.87595,992.4894 v -26.0405 h 22.52152 22.52152 v 26.0405 26.0405 h -22.52152 -22.52152 z"
id="path4361"
inkscape:connector-curvature="0" />
<path
style="fill:#ffffc0;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -153.87595,675.78054 v -27.4481 h 22.52152 22.52152 v 27.4481 27.4481 h -22.52152 -22.52152 z"
id="path4363"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -157.39494,624.37968 c 0,0 -4.22278,-12.66836 -18.29873,-12.66836 -14.07595,0 -14.07595,8.44557 -14.07595,8.44557 v 423.68601 c 0,0 1.40759,9.8532 14.07595,9.8532 12.66835,0 18.29873,-9.8532 18.29873,-9.8532"
id="path4377"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csccsc" />
<path
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -191.17722,624.37967 c 0,0 -35.18987,-1.40759 -35.18987,21.11393 0,22.52152 0,349.08354 0,371.605 0,22.5215 36.59747,25.3367 36.59747,25.3367"
id="path4381"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssc" />
<rect
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4467"
width="16.891144"
height="35.189873"
x="-216.51393"
y="648.30878"
rx="7.7417746"
ry="6.2843671" />
<rect
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4467-6"
width="16.891144"
height="35.189873"
x="-216.51393"
y="985.57465"
rx="7.7417746"
ry="6.2843671" />
<rect
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4487"
width="30.967089"
height="377.23544"
x="-62.939251"
y="645.49365" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1200.9342,633.85815 v 73.19494 h 67.5645 v -83.0481 l -25.3367,-14.07595 z"
id="path4499"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1200.9342,963.23538 h 67.5645 v 78.82532 l -23.9291,14.0759 -43.6354,-18.2988 z"
id="path4501"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 1200.9342,692.97714 V 977.31132"
id="path4503"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 1268.4987,693.56955 V 977.90373"
id="path4505"
inkscape:connector-curvature="0" />
<rect
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4534"
width="14.075921"
height="147.79749"
x="1216.5645"
y="759.50879" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 1224.1949,758.1012 V 704.61259"
id="path4536"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 1224.1949,961.97968 V 908.49107"
id="path4536-0"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 1254.5696,707.42778 V 963.61006"
id="path4556"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1268.757,624.97209 c 0,0 4.2228,-12.66836 18.2987,-12.66836 14.076,0 14.076,8.44557 14.076,8.44557 v 423.686 c 0,0 -1.4076,9.8532 -14.076,9.8532 -12.6683,0 -18.2987,-9.8532 -18.2987,-9.8532"
id="path4377-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csccsc" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1302.5393,624.97208 c 0,0 35.1898,-1.40759 35.1898,21.11393 0,22.52152 0,349.08354 0,371.60499 0,22.5215 -36.5974,25.3367 -36.5974,25.3367"
id="path4381-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssc" />
<rect
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4467-5"
width="16.891144"
height="35.189873"
x="-1329.876"
y="648.90118"
rx="7.7417746"
ry="6.2843671"
transform="scale(-1,1)" />
<rect
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4467-6-5"
width="16.891144"
height="35.189873"
x="-1329.876"
y="986.16711"
rx="7.7417746"
ry="6.2843671"
transform="scale(-1,1)" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1199.6734,632.82525 67.5645,74.60253"
id="path4585"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1199.6734,708.02018 67.5645,-73.19493"
id="path4587"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1199.8962,962.97968 67.5645,74.60252"
id="path4585-9"
inkscape:connector-curvature="0" />
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1199.8962,1038.1746 67.5645,-73.19492"
id="path4587-0"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<circle
style="fill:#c8c8c8;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4610"
transform="translate(78.488602,1074.6683)"
cx="119.64557"
cy="202.83545"
r="76.010124" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4612"
transform="translate(94.675942,1204.8708)"
cx="103.45823"
cy="72.632912"
r="44.339241" />
<circle
style="fill:#c8c8c8;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4610-9"
transform="translate(78.488602,187.66068)"
cx="119.64557"
cy="202.83545"
r="76.010124" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4612-4"
transform="translate(94.675942,317.86322)"
cx="103.45823"
cy="72.632912"
r="44.339241" />
<circle
style="fill:#c8c8c8;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4610-5"
transform="translate(773.02535,187.66068)"
cx="119.64557"
cy="202.83545"
r="76.010124" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4612-1"
transform="translate(789.21269,317.86322)"
cx="103.45823"
cy="72.632912"
r="44.339241" />
<circle
style="fill:#c8c8c8;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4610-4"
transform="translate(773.02535,1074.6683)"
cx="119.64557"
cy="202.83545"
r="76.010124" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4612-3"
transform="translate(789.21269,1204.8708)"
cx="103.45823"
cy="72.632912"
r="44.339241" />
<path
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1338.5088,829.07334 h 40.8203"
id="path3083"
inkscape:connector-curvature="0" />
<circle
style="fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path3853"
transform="translate(1441.2633,600.30879)"
cx="-59.118988"
cy="229.57974"
r="4.222785" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 778.06325,845.37208 -38.7088,-38.70886"
id="path3855"
inkscape:connector-curvature="0" />
<g
id="g3952"
transform="translate(-13.779768,15.524026)">
<circle
transform="translate(-79.458203,449.80248)"
id="path3857"
style="fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(-79.458203,569.9172)"
id="path3857-2"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(-79.458203,690.03203)"
id="path3857-3"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(-79.458203,810.14679)"
id="path3857-7"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
</g>
<g
id="g3946"
transform="translate(-13.779768,17.524026)">
<circle
transform="translate(1381.6254,448.24805)"
id="path3857-97"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(1381.6254,568.36277)"
id="path3857-2-3"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(1381.6254,688.4776)"
id="path3857-3-6"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
<circle
transform="translate(1381.6254,808.59236)"
id="path3857-7-1"
style="display:inline;fill:#3c3c3c;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="-81.640511"
cy="188.75949"
r="4.222785" />
</g>
</g>
<g
inkscape:groupmode="layer"
id="primary"
inkscape:label="primary">
<circle
style="fill:#a02c2c"
id="01"
cx="320.93164"
cy="-388.63797"
r="66.15696"
transform="scale(1,-1)"
inkscape:label="01" />
<circle
style="fill:#a02c2c"
id="02"
cx="320.93164"
cy="-757.42786"
r="66.15696"
transform="scale(1,-1)"
inkscape:label="01" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 43 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,144 @@
<svg id="svg166" version="1.1" viewBox="0 0 1668 1160" xmlns="http://www.w3.org/2000/svg">
<g id="g158" transform="translate(254 -254)">
<g id="g34" transform="translate(-13.78 3.524)" stroke="#000">
<path id="path10" d="m494.57 1006.9c41.429-15.714 140-11.427 191.43-12.857 51.429-1.429 160.48 10.23 201.43 27.143 40.995 16.93 134.78 67.656 151.43 72.857 22.857 7.143 41.429 7.143 80 20 38.572 12.857 25.714 32.857 25.714 32.857l-30-4.286-5.756 52.92s37.185 1.366 41.47 15.652c4.286 14.286 5.715 31.428-2.856 41.428-8.572 10-14.286-1.428-18.572 12.858-4.286 14.285-2.857 28.571-27.143 27.142-24.285-1.428-98.571 0-98.571 0s-15.714-108.57-98.573-105.71c-82.857 2.857-95.714 105.71-95.714 105.71h-500s-5.714-104.28-97.143-105.71c-91.428-1.428-98.571 105.71-98.571 105.71h-65.713l-18.572-38.571c-0.515 0-26.243 0-21.428-17.143 4.651-16.561-4.286-41.428 17.142-41.428 21.429 0 47.143 1.428 47.143 1.428l15.715-44.286-41.429-2.857s34.286-24.285 118.57-32.857c84.286-8.571 157.14-8.571 192.86-31.428 35.714-22.858 137.14-78.572 137.14-78.572z" fill="none" stroke-width="5"/>
<path id="path12" d="m28.857 1254h92.857m180 0h517.14m172.86 0h151.43" fill="none" stroke-width="2"/>
<path id="path14" d="m364.57 1101.1c15.715-18.572 123.37-81.525 151.43-88.572 29.502-7.41 103-7.142 103-7.142l-7.143 95.714z" fill="#f0ffeb" stroke-width="5"/>
<path id="path16" d="m404.57 1071.1v28.571" fill="none" stroke-width="2"/>
<path id="path18" d="m644.7 1006.8-4.285 94.328h207.16c-11.307-20.753-46.612-74.906-72.857-88.572-14.285-10-82.878-4.327-130.02-5.756zm167.02 7.164s80 84.286 85.715 87.143c5.714 2.857 115.71 1.428 115.71 1.428s-77.143-47.141-102.86-58.571c-25.715-11.429-90-31.429-98.572-30z" fill="#f0ffeb" stroke-width="5"/>
<g fill="none">
<path id="path20" d="m345.64 1091.7c-14.075 30.968-18.298 71.788-18.298 94.31 0 22.521 4.223 91.493 22.521 105.57m282.93-298.41c-1.408 5.63-4.047 81.903-6.51 106.93-3.67 18.715-4.989 51.219-6.159 87.315 0 22.522 7.038 80.233 12.669 105.57" stroke-width="5"/>
<path id="path22" d="m103.53 1252.1s15.483-85.864 106.98-85.864c91.494 0 109.79 87.271 109.79 87.271m479.99 0s15.483-85.863 106.98-85.863c91.493 0 109.79 87.27 109.79 87.27m-947.31-57.711h63.342" stroke-width="2"/>
<path id="path24" d="m779.18 1000.2c18.299 12.668 56.304 50.673 92.9 104.16 36.598 53.489 8.447 59.12-18.298 74.603-26.744 15.483-49.266 42.227-67.564 112.61" stroke-width="5"/>
<path id="path26" d="m1112.8 1196h-129.5m-696.76-0.222h544.74m-22.522-150.61v54.896" stroke-width="2"/>
</g>
<g stroke-width="2">
<rect id="rect28" x="558.65" y="1144.9" width="41.286" height="14.541" rx="2.933" ry="7.379" fill="#e6e6e6"/>
<rect id="rect30" x="816.24" y="1144.9" width="41.286" height="14.541" rx="2.933" ry="7.379" fill="#e6e6e6"/>
<rect id="rect32" x="259.53" y="1146.3" width="18.776" height="11.738" rx="1.63" ry="7.692" fill="#ffcb00"/>
</g>
</g>
<g id="g54" transform="translate(-13.78 15.524)" stroke="#000">
<path id="path36" d="m62.767 812.59-11.712 16.12-18.95-6.157v-19.925l18.95-6.158z" fill="none" stroke-width="5"/>
<path id="path38" d="m31.742 745.02 315.3-111.2m-316.71 254.77 315.3 115.42" fill="none" stroke-width="2"/>
<path id="path40" d="m341.41 632.78 140.76 38.005s-8.446 49.222-8.446 71.963v147.62c0 30.967 8.445 78.825 8.445 78.825l-140.76 33.782s-17.242-61.902-17.242-92.901l2.111-185.8c-1.408-30.967 15.132-91.493 15.132-91.493z" fill="#f0ffeb" stroke-width="5"/>
<g fill="none">
<path id="path42" d="m482.17 670.79s94.309 5.63 152.02 5.63c106.98 0 201.29-5.63 201.29-5.63m-1.408 297s-77.418-5.63-199.88-5.63c-68.972 0-152.02 7.038-152.02 7.038" stroke-width="5"/>
<rect id="rect44" x="528.62" y="729.2" width="106.98" height="181.58" rx="31.19" ry="34.436" stroke-width="2"/>
<path id="path46" d="m345.78 632.78h-294.19l-14.076 102.75-7.038 11.26v142.17l7.038 14.076 15.483 101.35h288.56m491.11-333.6s-12.668 47.858-12.668 73.195v146.39c0 25.336 12.668 77.417 12.668 77.417l205.51 38.005h108.38s14.076-81.64 14.076-115.42v-147.8c0-38.005-14.076-109.79-14.076-109.79h-108.38z" stroke-width="5"/>
</g>
<path id="path48" d="m843.92 689.09s-8.445 40.82-8.445 56.303v144.98c0 19.706 7.038 57.711 7.038 57.711s94.308 26.744 123.87 26.744h42.228v-312.49h-47.859c-25.336 0-116.83 26.745-116.83 26.745z" fill="#f0ffeb" stroke-width="5"/>
<path id="path50" d="m1038.2 632.78s30.967 40.82 30.967 111.2v146.39c0 64.749-30.967 116.83-30.967 116.83m-713.65-263.22 32.374-32.375v-40.82 112.61m-33.078 81.641 32.374-32.375v-40.82 112.61m648.9-116.83-32.375-32.374v-40.82 112.61" fill="none" stroke-width="2"/>
<path id="path52" d="m341.41 632.78s-42.228 78.825-42.228 111.2v146.39c0 40.82 42.228 114.02 42.228 114.02" fill="none" stroke-width="2"/>
</g>
<g fill="none" stroke="#000">
<path id="path56" d="m-106.79 740.92 1.407-91.493s5.63-23.93-25.337-25.337c-30.967-1.408-26.744 2.815-26.744 2.815l1.408 415.24h28.152c28.152 0 22.521-22.52 22.521-22.52v-95.717c-12.677 0-11.26-9.614-11.26-16.891v-149.2c0.45-18.89 9.853-16.892 9.853-16.892z" stroke-width="5"/>
<path id="path58" d="m-155.99 646.16h49.265m-48.415 374.27h49.266m-50.82-315.37h49.266m-46.451 259.81h49.266" stroke-width="2.2"/>
<path id="path60" d="m-147.1 703.82v260.4m9.853-258.84v260.4" stroke-width="2"/>
</g>
<g id="g88" transform="translate(-13.78 15.524)" stroke="#000">
<path id="path62" d="m494.57 641.61c41.429 15.714 140 11.428 191.43 12.856s160.48-10.23 201.43-27.143c40.995-16.93 134.78-67.656 151.43-72.857 22.857-7.143 41.429-7.143 80-20 38.572-12.857 25.714-32.857 25.714-32.857l-30 4.285-5.756-52.92s37.185-1.365 41.47-15.651c4.286-14.286 5.715-31.429-2.856-41.429-8.572-10-14.286 1.429-18.572-12.857-4.286-14.285-2.857-28.571-27.143-27.143-24.285 1.429-98.571 0-98.571 0s-15.714 108.57-98.572 105.72c-82.857-2.857-95.714-105.72-95.714-105.72h-500s-5.714 104.29-97.143 105.72c-91.428 1.428-98.571-105.72-98.571-105.72h-65.714l-18.572 38.572c-0.515 0-26.243 0-21.428 17.143 4.651 16.561-4.286 41.428 17.142 41.428 21.429 0 47.143-1.428 47.143-1.428l15.715 44.285-41.429 2.859s34.286 24.285 118.57 32.857c84.286 8.571 157.14 8.571 192.86 31.428 35.714 22.857 137.14 78.572 137.14 78.572z" fill="none" stroke-width="5"/>
<path id="path64" d="m28.857 394.47h92.857m180 0h517.14m172.86 0h151.43" fill="none" stroke-width="2"/>
<path id="path66" d="m364.57 547.33c15.715 18.571 123.37 81.524 151.43 88.571 29.502 7.41 103 7.143 103 7.143l-7.143-95.714z" fill="#f0ffeb" stroke-width="5"/>
<path id="path68" d="m404.57 577.33v-28.571" fill="none" stroke-width="2"/>
<path id="path70" d="m644.7 641.64-4.285-94.328h207.16c-11.307 20.753-46.612 74.906-72.857 88.571-14.285 10-82.878 4.328-130.02 5.757zm167.02-7.165s80-84.285 85.715-87.142c5.714-2.857 115.71-1.429 115.71-1.429s-77.143 47.143-102.86 58.572c-25.715 11.428-90 31.428-98.572 30z" fill="#f0ffeb" stroke-width="5"/>
<g fill="none">
<path id="path72" d="m345.64 556.81c-14.075-30.967-18.298-71.787-18.298-94.309 0-22.521 4.223-91.493 22.521-105.57m282.93 298.41c-1.408-5.63-4.047-81.905-6.51-106.93-3.67-18.714-4.989-51.218-6.159-87.314 0-22.522 7.038-80.233 12.669-105.57" stroke-width="5"/>
<path id="path74" d="m103.53 396.34s15.483 85.864 106.98 85.864c91.494 0 109.79-87.271 109.79-87.271m479.99 0s15.483 85.863 106.98 85.863c91.493 0 109.79-87.27 109.79-87.27m-947.31 57.71h63.342" stroke-width="2"/>
<path id="path76" d="m779.18 648.3c18.299-12.669 56.304-50.674 92.9-104.16 36.598-53.489 8.447-59.12-18.298-74.603-26.744-15.483-49.266-42.228-67.564-112.61" stroke-width="5"/>
<path id="path78" d="m1112.8 452.42h-129.5m-696.76 0.223h544.74m-22.522 150.61v-54.895" stroke-width="2"/>
</g>
<g stroke-width="2">
<rect id="rect80" transform="scale(1 -1)" x="558.65" y="-503.55" width="41.286" height="14.541" rx="2.933" ry="7.379" fill="#e6e6e6"/>
<rect id="rect82" transform="scale(1 -1)" x="816.24" y="-503.55" width="41.286" height="14.541" rx="2.933" ry="7.379" fill="#e6e6e6"/>
<rect id="rect84" transform="scale(1 -1)" x="259.53" y="-502.15" width="18.776" height="11.738" rx="1.63" ry="7.692" fill="#ffcb00"/>
<circle id="circle86" transform="translate(941.34 284)" cx="59.119" cy="211.28" r="16.891" fill="#fff"/>
</g>
</g>
<path id="path90" d="m-126.58 704.93v260.4" fill="none" stroke="#000" stroke-width="2"/>
<path id="path92" d="m-153.88 992.49v-26.041h45.043v52.08h-45.043zm0-316.71v-27.448h45.043v54.898h-45.043z" fill="#ffffc0"/>
<g fill="none" stroke="#000">
<path id="path94" d="m-157.4 624.38s-4.223-12.669-18.299-12.669-14.076 8.446-14.076 8.446v423.69s1.408 9.853 14.076 9.853c12.669 0 18.3-9.853 18.3-9.853" stroke-width="5"/>
<path id="path96" d="m-191.18 624.38s-35.19-1.408-35.19 21.114v371.6c0 22.521 36.597 25.336 36.597 25.336" stroke-width="5"/>
<g stroke-width="2">
<rect id="rect98" x="-216.51" y="648.31" width="16.891" height="35.19" rx="7.742" ry="6.284"/>
<rect id="rect100" x="-216.51" y="985.58" width="16.891" height="35.19" rx="7.742" ry="6.284"/>
<path id="path102" d="m-62.939 645.49h30.967v377.24h-30.967z"/>
</g>
<path id="path104" d="m1200.9 633.86v73.195h67.565v-83.048l-25.337-14.076zm0 329.38h67.565v78.826l-23.93 14.076-43.635-18.3zm0-270.26v284.33m67.565-283.74v284.33" stroke-width="5"/>
<path id="path106" d="m1216.6 759.51h14.076v147.8h-14.076zm7.631-1.408v-53.488m0 257.37v-53.49m30.375-201.06v256.18" stroke-width="2"/>
<path id="path108" d="m1268.8 624.97s4.223-12.668 18.299-12.668 14.076 8.445 14.076 8.445v423.69s-1.408 9.853-14.076 9.853c-12.669 0-18.299-9.853-18.299-9.853m33.783-419.46s35.19-1.408 35.19 21.114v371.6c0 22.521-36.598 25.337-36.598 25.337" stroke-width="5"/>
<g stroke-width="2">
<rect id="rect110" transform="scale(-1 1)" x="-1329.9" y="648.9" width="16.891" height="35.19" rx="7.742" ry="6.284"/>
<rect id="rect112" transform="scale(-1 1)" x="-1329.9" y="986.17" width="16.891" height="35.19" rx="7.742" ry="6.284"/>
<path id="path114" d="m1199.7 632.82 67.565 74.603m-67.565 0.592 67.565-73.195m-67.342 328.16 67.565 74.602m-67.565 0.593 67.565-73.195"/>
</g>
</g>
<g stroke="#000">
<g stroke-width="2">
<circle id="circle116" transform="translate(78.489 1074.7)" cx="119.65" cy="202.84" r="76.01" fill="#c8c8c8"/>
<circle id="circle118" transform="translate(94.676 1204.9)" cx="103.46" cy="72.633" r="44.339" fill="#fff"/>
<circle id="circle120" transform="translate(78.489 187.66)" cx="119.65" cy="202.84" r="76.01" fill="#c8c8c8"/>
<circle id="circle122" transform="translate(94.676 317.86)" cx="103.46" cy="72.633" r="44.339" fill="#fff"/>
<circle id="circle124" transform="translate(773.02 187.66)" cx="119.65" cy="202.84" r="76.01" fill="#c8c8c8"/>
<circle id="circle126" transform="translate(789.21 317.86)" cx="103.46" cy="72.633" r="44.339" fill="#fff"/>
<circle id="circle128" transform="translate(773.02 1074.7)" cx="119.65" cy="202.84" r="76.01" fill="#c8c8c8"/>
<circle id="circle130" transform="translate(789.21 1204.9)" cx="103.46" cy="72.633" r="44.339" fill="#fff"/>
</g>
<path id="path132" d="m1338.5 829.07h40.82" fill="none" stroke-width="5"/>
<circle id="circle134" transform="translate(1441.3 600.31)" cx="-59.119" cy="229.58" r="4.223" fill="#3c3c3c" stroke-width="5"/>
<g stroke-width="2">
<path id="path136" d="m778.06 845.37-38.709-38.709" fill="none"/>
<g id="g146" transform="translate(-13.78 15.524)" fill="#3c3c3c">
<circle id="circle138" transform="translate(-79.458 449.8)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle140" transform="translate(-79.458 569.92)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle142" transform="translate(-79.458 690.03)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle144" transform="translate(-79.458 810.15)" cx="-81.641" cy="188.76" r="4.223"/>
</g>
<g id="g156" transform="translate(-13.78 17.524)" fill="#3c3c3c">
<circle id="circle148" transform="translate(1381.6 448.25)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle150" transform="translate(1381.6 568.36)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle152" transform="translate(1381.6 688.48)" cx="-81.641" cy="188.76" r="4.223"/>
<circle id="circle154" transform="translate(1381.6 808.59)" cx="-81.641" cy="188.76" r="4.223"/>
</g>
</g>
</g>
</g>
<g id="layer2" fill="#d00000">
<circle id="p02" cx="503.65" cy="248.75" r="61.935" />
<circle id="p03" cx="863.41" cy="248.75" r="61.935"/>
<circle id="p04" cx="1181.5" cy="248.75" r="61.935"/>
<circle id="p05" cx="1378.4" cy="151.16" r="61.935"/>
<circle id="p06" cx="1535.1" cy="581.37" r="61.935"/>
<circle id="p07" cx="1378.4" cy="997.9" r="61.935"/>
<circle id="p08" cx="1181.5" cy="914.24" r="61.935"/>
<circle id="p09" transform="scale(1,-1)" cx="863.41" cy="-914.24" r="61.935"/>
<circle id="p10" cx="503.65" cy="914.24" r="61.935"/>
<circle id="p11" cx="297.77" cy="997.9" r="61.935"/>
<circle id="p12" cx="93.269" cy="581.37" r="61.935"/>
<circle id="p25" cx="424.31" cy="581.37" r="61.935"/>
<circle id="p27" cx="972.84" cy="581.37" r="61.935"/>
<circle id="p01" cx="297.77" cy="151.16" r="61.935"/>
<circle id="p26" cx="1339.4" cy="581.37" r="61.935"/>
</g>
<g id="g4994" fill="#ffef00">
<circle id="s02" cx="503.65" cy="248.75" r="61.935"/>
<circle id="s03" cx="863.41" cy="248.75" r="61.935"/>
<circle id="s04" cx="1181.5" cy="248.75" r="61.935"/>
<circle id="s05" cx="1378.4" cy="151.16" r="61.935"/>
<circle id="s06" cx="1535.1" cy="581.37" r="61.935"/>
<circle id="s07" cx="1378.4" cy="997.9" r="61.935"/>
<circle id="s08" cx="1181.5" cy="914.24" r="61.935"/>
<circle id="s09" transform="scale(1,-1)" cx="863.41" cy="-914.24" r="61.935"/>
<circle id="s10" cx="503.65" cy="914.24" r="61.935"/>
<circle id="s11" cx="297.77" cy="997.9" r="61.935"/>
<circle id="s12" cx="93.269" cy="581.37" r="61.935"/>
<circle id="s25" cx="424.31" cy="581.37" r="61.935"/>
<circle id="s27" cx="972.84" cy="581.37" r="61.935"/>
<circle id="s01" cx="297.77" cy="151.16" r="61.935"/>
<circle id="s26" cx="1339.4" cy="581.37" r="61.935"/>
</g>
<g id="layer3">
<text id="p15" opacity="0" x="382.62802" y="1034.3463" fill="#fd0000" font-family="sans-serif" font-size="1696.9px" letter-spacing="0px" stroke-width="17.676" word-spacing="0px" style="line-height:5.25" xml:space="preserve"><tspan id="tspan4997" x="382.62802" y="1034.3463" fill="#fd0000" stroke-width="17.676">x</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,37 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
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";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
setEmailOptions: e => dispatch(setEmailOptions(e))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function Test({ setEmailOptions }) {
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>
);
});

View File

@@ -2,10 +2,12 @@ import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import Alert from "./alert.component"; import Alert from "./alert.component";
import { MockedProvider } from "@apollo/react-testing"; import { MockedProvider } from "@apollo/react-testing";
import { shallow } from "enzyme"; import { shallow, mount } from "enzyme";
const div = document.createElement("div"); const div = document.createElement("div");
it("renders without crashing", () => { it("renders without crashing", () => {
shallow(<Alert type="error" />); const wrapper = mount(<Alert type="error" message="Test Error" />);
console.log("wrapper", wrapper);
// expect(wrapper.children()).to.have.lengthOf(1);
}); });

View File

@@ -0,0 +1,76 @@
import { Select, Button, Popover, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function AllocationsAssignmentComponent({
bodyshop,
handleAssignment,
assignment,
setAssignment,
visibilityState,
maxHours
}) {
const { t } = useTranslation();
const onChange = e => {
console.log("e", e);
setAssignment({ ...assignment, employeeid: e });
};
const [visibility, setVisibility] = visibilityState;
const popContent = (
<div>
<Select
showSearch
style={{ width: 200 }}
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}`}
</Select.Option>
))}
</Select>
<InputNumber
defaultValue={assignment.hours}
placeholder={t("joblines.fields.mod_lb_hrs")}
max={parseFloat(maxHours)}
min={0}
onChange={e => setAssignment({ ...assignment, hours: e })}
/>
<Button
type="primary"
disabled={!assignment.employeeid}
onClick={handleAssignment}
>
Assign
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</div>
);
return (
<Popover content={popContent} visible={visibility}>
<Button onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>
</Popover>
);
});

View File

@@ -0,0 +1,47 @@
import React, { useState } from "react";
import AllocationsAssignmentComponent from "./allocations-assignment.component";
import { useMutation } from "react-apollo";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next";
import { notification } from "antd";
export default function AllocationsAssignmentContainer({
jobLineId,
hours,
refetch
}) {
const visibilityState = useState(false);
const { t } = useTranslation();
const [assignment, setAssignment] = useState({
joblineid: jobLineId,
hours: parseFloat(hours),
employeeid: null
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
const handleAssignment = () => {
insertAllocation({ variables: { alloc: { ...assignment } } })
.then(r => {
notification["success"]({
message: t("allocations.successes.save")
});
visibilityState[1](false);
if (refetch) refetch();
})
.catch(error => {
notification["error"]({
message: t("employees.errors.saving", { message: error.message })
});
});
};
return (
<AllocationsAssignmentComponent
handleAssignment={handleAssignment}
maxHours={hours}
assignment={assignment}
setAssignment={setAssignment}
visibilityState={visibilityState}
/>
);
}

View File

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

View File

@@ -0,0 +1,67 @@
import { Button, Popover, Select } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function AllocationsBulkAssignmentComponent({
disabled,
bodyshop,
handleAssignment,
assignment,
setAssignment,
visibilityState
}) {
const { t } = useTranslation();
const onChange = e => {
console.log("e", e);
setAssignment({ ...assignment, employeeid: e });
};
const [visibility, setVisibility] = visibilityState;
const popContent = (
<div>
<Select
showSearch
style={{ width: 200 }}
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}`}
</Select.Option>
))}
</Select>
<Button
type='primary'
disabled={!assignment.employeeid}
onClick={handleAssignment}>
Assign
</Button>
<Button onClick={() => setVisibility(false)}>Close</Button>
</div>
);
return (
<Popover content={popContent} visible={visibility}>
<Button disabled={disabled} onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")}
</Button>
</Popover>
);
});

View File

@@ -0,0 +1,47 @@
import React, { useState } from "react";
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
import { useMutation } from "react-apollo";
import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
import { useTranslation } from "react-i18next";
import { notification } from "antd";
export default function AllocationsBulkAssignmentContainer({
jobLines,
refetch
}) {
const visibilityState = useState(false);
const { t } = useTranslation();
const [assignment, setAssignment] = useState({
employeeid: null
});
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
const handleAssignment = () => {
const allocs = jobLines.reduce((acc, value) => {
acc.push({
joblineid: value.id,
hours: parseFloat(value.mod_lb_hrs) || 0,
employeeid: assignment.employeeid
});
return acc;
}, []);
insertAllocation({ variables: { alloc: allocs } }).then(r => {
notification["success"]({
message: t("employees.successes.save")
});
visibilityState[1](false);
if (refetch) refetch();
});
};
return (
<AllocationsBulkAssignment
disabled={jobLines.length > 0 ? false : true}
handleAssignment={handleAssignment}
assignment={assignment}
setAssignment={setAssignment}
visibilityState={visibilityState}
/>
);
}

View File

@@ -0,0 +1,19 @@
import { Icon } from "antd";
import React from "react";
import { MdRemoveCircleOutline } from "react-icons/md";
export default function AllocationsLabelComponent({ allocation, handleClick }) {
return (
<div style={{ display: "flex" }}>
<span>
{`${allocation.employee.first_name || ""} ${allocation.employee
.last_name || ""} (${allocation.hours || ""})`}
</span>
<Icon
style={{ color: "red", padding: "0px 4px" }}
component={MdRemoveCircleOutline}
onClick={handleClick}
/>
</div>
);
}

View File

@@ -0,0 +1,30 @@
import React from "react";
import { useMutation } from "react-apollo";
import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
import AllocationsLabelComponent from "./allocations-employee-label.component";
import { notification } from "antd";
import { useTranslation } from "react-i18next";
export default function AllocationsLabelContainer({ allocation, refetch }) {
const [deleteAllocation] = useMutation(DELETE_ALLOCATION);
const { t } = useTranslation();
const handleClick = e => {
e.preventDefault();
deleteAllocation({ variables: { id: allocation.id } })
.then(r => {
notification["success"]({
message: t("allocations.successes.deleted")
});
if (refetch) refetch();
})
.catch(error => {
notification["error"]({ message: t("allocations.errors.deleting") });
});
};
return (
<AllocationsLabelComponent
allocation={allocation}
handleClick={handleClick}
/>
);
}

View File

@@ -0,0 +1,22 @@
import { Tag, Popover } from "antd";
import React from "react";
import Barcode from "react-barcode";
import { useTranslation } from "react-i18next";
export default function BarcodePopupComponent({ value }) {
const { t } = useTranslation();
return (
<div>
<Popover
content={
<Barcode
value={value}
background="transparent"
displayValue={false}
/>
}
>
<Tag>{t("general.labels.barcode")}</Tag>
</Popover>
</div>
);
}

View File

@@ -0,0 +1,120 @@
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";
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({
conversation,
toggleConversationVisible,
closeConversation
}) {
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>
) : (
<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>
</div>
);
});

View File

@@ -0,0 +1,17 @@
import React from "react";
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} />;
});

View File

@@ -0,0 +1,127 @@
.messages {
height: auto;
min-height: calc(100% - 93px);
max-height: calc(100% - 93px);
overflow-y: scroll;
overflow-x: hidden;
}
@media screen and (max-width: 735px) {
.messages {
max-height: calc(100% - 105px);
}
}
.messages::-webkit-scrollbar {
width: 8px;
background: transparent;
}
.messages::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.3);
}
.messages ul li {
display: inline-block;
clear: both;
//float: left;
margin: 5px 15px 5px 15px;
width: calc(100% - 25px);
font-size: 0.9em;
}
.messages ul li:nth-last-child(1) {
margin-bottom: 20px;
}
.messages ul li.sent img {
margin: 6px 8px 0 0;
}
.messages ul li.sent p {
background: #435f7a;
color: #f5f5f5;
}
.messages ul li.replies img {
float: right;
margin: 6px 0 0 8px;
}
.messages ul li.replies p {
background: #f5f5f5;
float: right;
}
.messages ul li img {
width: 22px;
border-radius: 50%;
float: left;
}
.messages ul li p {
display: inline-block;
padding: 10px 15px;
border-radius: 20px;
max-width: 205px;
line-height: 130%;
}
@media screen and (min-width: 735px) {
.messages ul li p {
max-width: 300px;
}
}
.message-input {
position: absolute;
bottom: 0;
width: 100%;
z-index: 99;
}
.message-input .wrap {
position: relative;
}
.message-input .wrap input {
font-family: "proxima-nova", "Source Sans Pro", sans-serif;
float: left;
border: none;
width: calc(100% - 90px);
padding: 11px 32px 10px 8px;
font-size: 0.8em;
color: #32465a;
}
@media screen and (max-width: 735px) {
.message-input .wrap input {
padding: 15px 32px 16px 8px;
}
}
.message-input .wrap input:focus {
outline: none;
}
.message-input .wrap .attachment {
position: absolute;
right: 60px;
z-index: 4;
margin-top: 10px;
font-size: 1.1em;
color: #435f7a;
opacity: 0.5;
cursor: pointer;
}
@media screen and (max-width: 735px) {
.message-input .wrap .attachment {
margin-top: 17px;
right: 65px;
}
}
.message-input .wrap .attachment:hover {
opacity: 1;
}
.message-input .wrap button {
float: right;
border: none;
width: 50px;
padding: 12px 0;
cursor: pointer;
background: #32465a;
color: #f5f5f5;
}
@media screen and (max-width: 735px) {
.message-input .wrap button {
padding: 16px 0;
}
}
.message-input .wrap button:hover {
background: #435f7a;
}
.message-input .wrap button:focus {
outline: none;
}

View File

@@ -0,0 +1,23 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { openConversation } from "../../redux/messaging/messaging.actions";
import { Icon } from "antd";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
openConversation: phone => dispatch(openConversation(phone))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function ChatOpenButton({ openConversation, phone }) {
return (
<Icon
style={{ margin: 4 }}
type="message"
onClick={() => openConversation(phone)}
/>
);
});

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
import React from "react";
export default function ChatWindowComponent() {
return <div>Chat Windows and more</div>;
}

View File

@@ -1,18 +0,0 @@
import React, { useState } from "react";
import ChatWindowComponent from "./chat-window.component";
import { Button } from "antd";
export default function ChatWindowContainer() {
const [visible, setVisible] = useState(false);
return (
<div style={{ position: "absolute", zIndex: 1000 }}>
{visible ? <ChatWindowComponent /> : null}
<Button
onClick={() => {
setVisible(!visible);
}}>
Open!
</Button>
</div>
);
}

View File

@@ -1,71 +0,0 @@
import { useApolloClient, useQuery } from "@apollo/react-hooks";
import { Avatar, Col, Dropdown, Icon, Menu, Row } from "antd";
import i18next from "i18next";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import UserImage from "../../assets/User.svg";
import { GET_CURRENT_USER } from "../../graphql/local.queries";
import AlertComponent from "../alert/alert.component";
import SignOut from "../sign-out/sign-out.component";
export default function CurrentUserDropdown() {
const { t } = useTranslation();
const { loading, error, data } = useQuery(GET_CURRENT_USER);
const client = useApolloClient();
const handleMenuClick = e => {
if (e.item.props.actiontype === "lang-select") {
i18next.changeLanguage(e.key, (err, t) => {
if (err)
return console.log("Error encountered when changing languages.", err);
client.writeData({ data: { language: e.key } });
});
}
};
const menu = (
<Menu mode='vertical' onClick={handleMenuClick}>
<Menu.Item>
<SignOut />
</Menu.Item>
<Menu.Item>
<Link to='/manage/profile'> {t("menus.currentuser.profile")}</Link>
</Menu.Item>
<Menu.SubMenu
title={
<span>
<Icon type='global' />
<span>{t("menus.currentuser.languageselector")}</span>
</span>
}>
<Menu.Item actiontype='lang-select' key='en_us'>
{t("general.languages.english")}
</Menu.Item>
<Menu.Item actiontype='lang-select' key='fr'>
{t("general.languages.french")}
</Menu.Item>
<Menu.Item actiontype='lang-select' key='es'>
{t("general.languages.spanish")}
</Menu.Item>
</Menu.SubMenu>
</Menu>
);
if (loading) return null;
if (error) return <AlertComponent message={error.message} type='error' />;
const { currentUser } = data;
return (
<Dropdown overlay={menu}>
<Row>
<Col span={8}>
<Avatar size='large' alt='Avatar' src={UserImage} />
</Col>
<Col span={16} style={{ color: "white" }}>
{currentUser.displayName || t("general.labels.unknown")}
</Col>
</Row>
</Dropdown>
);
}

View File

@@ -0,0 +1,39 @@
import React from "react";
import { Input } from "antd";
import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
export default function SendEmailButtonComponent({
messageOptions,
handleConfigChange,
handleHtmlChange
}) {
return (
<div>
<Input
defaultValue={messageOptions.to}
onChange={handleConfigChange}
name='to'
/>
CC
<Input
defaultValue={messageOptions.cc}
onChange={handleConfigChange}
name='cc'
/>
Subject
<Input
defaultValue={messageOptions.subject}
onChange={handleConfigChange}
name='subject'
/>
<CKEditor
editor={ClassicEditor}
data={messageOptions.html}
onChange={(event, editor) => {
handleHtmlChange(editor.getData());
}}
/>
</div>
);
}

View File

@@ -0,0 +1,104 @@
import { Button, Modal, notification } from "antd";
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useLazyQuery } from "react-apollo";
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 LoadingSpinner from "../loading-spinner/loading-spinner.component";
import EmailOverlayComponent from "./email-overlay.component";
const mapStateToProps = createStructuredSelector({
modalVisible: selectEmailVisible,
emailConfig: selectEmailConfig
});
const mapDispatchToProps = dispatch => ({
toggleEmailOverlayVisible: () => dispatch(toggleEmailOverlayVisible())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function SendEmail({ emailConfig, modalVisible, toggleEmailOverlayVisible }) {
const { t } = useTranslation();
const [messageOptions, setMessageOptions] = useState(
emailConfig.messageOptions
);
useEffect(() => {
setMessageOptions(emailConfig.messageOptions);
}, [setMessageOptions, emailConfig.messageOptions]);
const [executeQuery, { called, loading, data }] = useLazyQuery(
emailConfig.queryConfig[0],
{
...emailConfig.queryConfig[1],
fetchPolicy: "network-only"
}
);
if (
emailConfig.queryConfig[0] &&
emailConfig.queryConfig[1] &&
modalVisible &&
!called
) {
executeQuery();
}
if (data && !messageOptions.html && emailConfig.template) {
setMessageOptions({
...messageOptions,
html: ReactDOMServer.renderToStaticMarkup(
<emailConfig.template data={data} />
)
//html: renderEmail(<emailConfig.template data={data} />)
});
}
const handleOk = () => {
//sendEmail(messageOptions);
axios
.post("/sendemail", messageOptions)
.then(response => {
console.log(JSON.stringify(response));
notification["success"]({ message: t("emails.successes.sent") });
toggleEmailOverlayVisible();
})
.catch(error => {
console.log(JSON.stringify(error));
notification["error"]({
message: t("emails.errors.notsent", { message: error.message })
});
});
};
const handleConfigChange = event => {
const { name, value } = event.target;
setMessageOptions({ ...messageOptions, [name]: value });
};
const handleHtmlChange = text => {
setMessageOptions({ ...messageOptions, html: text });
};
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>
);
});

View File

@@ -4,7 +4,7 @@ import React from "react";
export default function FooterComponent() { export default function FooterComponent() {
return ( return (
<Row> <Row>
<Col span={8} offset={9}> <Col span={8} offset={8}>
Copyright Snapt Software 2019. All rights reserved. Copyright Snapt Software 2019. All rights reserved.
</Col> </Col>
</Row> </Row>

View File

@@ -5,9 +5,13 @@ function FormItemEmail(props, ref) {
<Input <Input
{...props} {...props}
addonAfter={ addonAfter={
<a href={`mailto:${props.email}`}> props.email ? (
<a href={`mailto:${props.email}`}>
<Icon type="mail" />
</a>
) : (
<Icon type="mail" /> <Icon type="mail" />
</a> )
} }
/> />
); );

View File

@@ -0,0 +1,21 @@
import { Button } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import AlertComponent from "../alert/alert.component";
export default function ResetForm({ resetFields }) {
const { t } = useTranslation();
return (
<AlertComponent
message={
<div>
{t("general.messages.unsavedchanges")}
<Button style={{ marginLeft: "20px" }} onClick={() => resetFields()}>
{t("general.actions.reset")}
</Button>
</div>
}
closable
/>
);
}

View File

@@ -1,79 +0,0 @@
import React from "react";
// import { Icon, Button, Input, AutoComplete } from "antd";
// const { Option } = AutoComplete;
// function onSelect(value) {
// console.log("onSelect", value);
// }
// function getRandomInt(max, min = 0) {
// return Math.floor(Math.random() * (max - min + 1)) + min; // eslint-disable-line no-mixed-operators
// }
// function searchResult(query) {
// return new Array(getRandomInt(5))
// .join(".")
// .split(".")
// .map((item, idx) => ({
// query,
// category: `${query}${idx}`,
// count: getRandomInt(200, 100)
// }));
// }
// function renderOption(item) {
// return (
// <Option key={item.category} text={item.category}>
// <div className='global-search-item'>
// <span className='global-search-item-desc'>
// Found {item.query} on
// <a
// href={`https://s.taobao.com/search?q=${item.query}`}
// target='_blank'
// rel='noopener noreferrer'>
// {item.category}
// </a>
// </span>
// <span className='global-search-item-count'>{item.count} results</span>
// </div>
// </Option>
// );
// }
export default class GlobalSearch extends React.Component {
state = {
dataSource: []
};
// handleSearch = value => {
// this.setState({
// dataSource: value ? searchResult(value) : []
// });
// };
render() {
return (
<div />
// <div style={{ width: 300 }}>
// <AutoComplete
// size="large"
// style={{ width: "100%" }}
// dataSource={dataSource.map(renderOption)}
// onSelect={onSelect}
// onSearch={this.handleSearch}
// placeholder="input here"
// optionLabelProp="text"
// >
// <Input
// suffix={
// <Button style={{ marginRight: -12 }} size="large" type="primary">
// <Icon type="search" />
// </Button>
// }
// />
// </AutoComplete>
// </div>
);
}
}

View File

@@ -1,73 +1,181 @@
import { useApolloClient } from "@apollo/react-hooks"; import { Avatar, Col, Icon, Menu, Row } from "antd";
import { Col, Icon, Menu, Row } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import CurrentUserDropdown from "../current-user-dropdown/current-user-dropdown.component"; import UserImage from "../../assets/User.svg";
import GlobalSearch from "../global-search/global-search.component"; import { signOutStart } from "../../redux/user/user.actions";
import ManageSignInButton from "../manage-sign-in-button/manage-sign-in-button.component"; import ManageSignInButton from "../manage-sign-in-button/manage-sign-in-button.component";
import "./header.styles.scss";
export default ({ landingHeader, navItems, selectedNavItem }) => { export default ({
const apolloClient = useApolloClient(); landingHeader,
selectedNavItem,
logo,
handleMenuClick,
currentUser
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const handleClick = e => { //TODO Add
apolloClient.writeData({ data: { selectedNavItem: e.key } });
};
return ( return (
<Row type='flex' justify='space-around'> <Row type="flex" justify="space-around" align="middle">
<Col span={16}> {logo ? (
<Menu <Col span={3}>
theme='dark' <img alt="Shop Logo" src={logo} style={{ height: "40px" }} />
className='header' </Col>
onClick={handleClick} ) : null}
selectedKeys={selectedNavItem} <Col span={14}>
mode='horizontal'> {landingHeader ? (
<Menu.Item> <Menu
<GlobalSearch /> theme="dark"
</Menu.Item> className="header"
selectedKeys={selectedNavItem}
mode="horizontal"
onClick={handleMenuClick}
>
<ManageSignInButton />
<Menu.Item key='home'> <Menu.SubMenu
<Link to='/manage'> title={
<Icon type='home' /> <div>
{t("menus.header.home")} <Avatar
</Link> size="medium"
</Menu.Item> alt="Avatar"
<Menu.SubMenu title={t("menus.header.jobs")}> src={
<Menu.Item key='jobs'> currentUser.photoURL ? currentUser.photoURL : UserImage
<Link to='/manage/jobs'> }
<Icon type='home' /> style={{ margin: "10px" }}
{t("menus.header.activejobs")} />
{currentUser.displayName || t("general.labels.unknown")}
</div>
}
>
<Menu.Item onClick={signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item>
<Link to="/manage/profile">
{t("menus.currentuser.profile")}
</Link>
</Menu.Item>
<Menu.SubMenu
title={
<span>
<Icon type="global" />
<span>{t("menus.currentuser.languageselector")}</span>
</span>
}
>
<Menu.Item actiontype="lang-select" key="en_us">
{t("general.languages.english")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="fr">
{t("general.languages.french")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="es">
{t("general.languages.spanish")}
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
</Menu>
) : (
<Menu
theme="dark"
className="header"
selectedKeys={selectedNavItem}
mode="horizontal"
onClick={handleMenuClick}
>
<Menu.Item key="home">
<Link to="/manage">
<Icon type="home" />
{t("menus.header.home")}
</Link> </Link>
</Menu.Item> </Menu.Item>
<Menu.Item key='availablejobs'> <Menu.SubMenu title={t("menus.header.jobs")}>
<Link to='/manage/available'> <Menu.Item key="schedule">
<Icon type='home' /> <Link to="/manage/schedule">
{t("menus.header.availablejobs")} <Icon type="calendar" />
</Link> {t("menus.header.schedule")}
</Menu.Item> </Link>
</Menu.SubMenu> </Menu.Item>
<Menu.Item key="activejobs">
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
</Menu.Item>
<Menu.Item key="availablejobs">
<Link to="/manage/available">
{t("menus.header.availablejobs")}
</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={t("menus.header.customers")}>
<Menu.Item key="owners">
<Link to="/manage/owners">
<Icon type="team" />
{t("menus.header.owners")}
</Link>
</Menu.Item>
<Menu.Item key="vehicles">
<Link to="/manage/vehicles">
<Icon type="car" />
{t("menus.header.vehicles")}
</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>
</Menu.Item>
<Menu.Item key="shop-vendors">
<Link to="/manage/shop/vendors">
{t("menus.header.shop_vendors")}
</Link>
</Menu.Item>
</Menu.SubMenu>
{ <Menu.SubMenu
// navItems.map(navItem => ( title={
// <Menu.Item key={navItem.title}> <div>
// <Link to={navItem.path}> <Avatar
// {navItem.icontype ? <Icon type={navItem.icontype} /> : null} size="medium"
// {navItem.title} alt="Avatar"
// </Link> src={
// </Menu.Item> currentUser.photoURL ? currentUser.photoURL : UserImage
// )) }
} style={{ margin: "10px" }}
/>
{!landingHeader ? null : ( {currentUser.displayName || t("general.labels.unknown")}
<Menu.Item> </div>
<ManageSignInButton /> }
</Menu.Item> >
)} <Menu.Item onClick={signOutStart()}>
</Menu> {t("user.actions.signout")}
</Col> </Menu.Item>
<Col span={6} offset={2}> <Menu.Item>
{!landingHeader ? <CurrentUserDropdown /> : null} <Link to="/manage/profile">
{t("menus.currentuser.profile")}
</Link>
</Menu.Item>
<Menu.SubMenu
title={
<span>
<Icon type="global" />
<span>{t("menus.currentuser.languageselector")}</span>
</span>
}
>
<Menu.Item actiontype="lang-select" key="en_us">
{t("general.languages.english")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="fr">
{t("general.languages.french")}
</Menu.Item>
<Menu.Item actiontype="lang-select" key="es">
{t("general.languages.spanish")}
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
</Menu>
)}
</Col> </Col>
</Row> </Row>
); );

View File

@@ -1,46 +1,52 @@
import React from "react"; import React from "react";
import "./header.styles.scss";
import { useQuery } from "react-apollo";
// //import {
// GET_LANDING_NAV_ITEMS,
// GET_NAV_ITEMS
// } from "../../graphql/metadata.queries";
import { GET_CURRENT_SELECTED_NAV_ITEM } from "../../graphql/local.queries";
//import LoadingSpinner from "../loading-spinner/loading-spinner.component";
//import AlertComponent from "../alert/alert.component";
import HeaderComponent from "./header.component"; import HeaderComponent from "./header.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import i18next from "i18next";
import { setUserLanguage, signOutStart } from "../../redux/user/user.actions";
import {
selectCurrentUser,
selectBodyshop
} from "../../redux/user/user.selectors";
export default ({ landingHeader, signedIn }) => { const mapStateToProps = createStructuredSelector({
const hookSelectedNavItem = useQuery(GET_CURRENT_SELECTED_NAV_ITEM); currentUser: selectCurrentUser,
bodyshop: selectBodyshop
});
// let hookNavItems; const mapDispatchToProps = dispatch => ({
// if (landingHeader) { signOutStart: () => dispatch(signOutStart()),
// hookNavItems = useQuery(GET_LANDING_NAV_ITEMS, { setUserLanguage: language => dispatch(setUserLanguage(language))
// fetchPolicy: "network-only" });
// });
// } else {
// hookNavItems = useQuery(GET_NAV_ITEMS, {
// fetchPolicy: "network-only"
// });
// }
// if (hookNavItems.loading || hookSelectedNavItem.loading) export default connect(
// return <LoadingSpinner />; mapStateToProps,
// if (hookNavItems.error) mapDispatchToProps
// return <AlertComponent message={hookNavItems.error.message} />; )(function HeaderContainer({
// if (hookSelectedNavItem.error) landingHeader,
// return console.log( currentUser,
// "Unable to load Selected Navigation Item.", bodyshop,
// hookSelectedNavItem.error signOutStart,
// ); setUserLanguage
}) {
const { selectedNavItem } = hookSelectedNavItem.data; const handleMenuClick = e => {
// const navItems = JSON.parse(hookNavItems.data.masterdata_by_pk.value); if (e.item.props.actiontype === "lang-select") {
i18next.changeLanguage(e.key, (err, t) => {
if (err)
return console.log("Error encountered when changing languages.", err);
setUserLanguage(e.key);
});
}
};
return ( return (
<HeaderComponent <HeaderComponent
handleMenuClick={handleMenuClick}
signOutStart={signOutStart}
landingHeader={landingHeader} landingHeader={landingHeader}
selectedNavItem={selectedNavItem} selectedNavItem={null}
currentUser={currentUser}
logo={bodyshop ? bodyshop.logo_img_path : null}
/> />
); );
}; });

View File

@@ -1,4 +0,0 @@
.header{
text-align: center;
width: 100%;
}

View File

@@ -0,0 +1,71 @@
import { Modal, Form, Input, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import ResetForm from "../form-items-formatted/reset-form-item.component";
export default function InvoiceEnterModalComponent({
visible,
invoice,
handleCancel,
handleSubmit,
form
}) {
const { t } = useTranslation();
const { getFieldDecorator, isFieldsTouched, 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>
</Modal>
);
}

View File

@@ -0,0 +1,114 @@
import { Form, notification } from "antd";
import React from "react";
import { useMutation } from "react-apollo";
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 { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectInvoiceEnterModal } from "../../redux/modals/modals.selectors";
import InvoiceEnterModalComponent from "./invoice-enter-modal.component";
const mapStateToProps = createStructuredSelector({
invoiceEnterModal: selectInvoiceEnterModal
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter"))
});
function InvoiceEnterModalContainer({
invoiceEnterModal,
toggleModalVisible,
form
}) {
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("invoices.errors.validation"),
description: err.message
});
}
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();
// }
}
});
};
const handleCancel = () => {
toggleModalVisible();
};
return (
<InvoiceEnterModalComponent
visible={invoiceEnterModal.visible}
invoice={invoiceEnterModal.context}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
form={form}
/>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(
Form.create({ name: "InvoiceEnterModalContainer" })(
InvoiceEnterModalContainer
)
);

View File

@@ -0,0 +1,735 @@
import React from "react";
export default ({ dmg1, dmg2 }) => (
<svg
id='svg166'
version='1.1'
viewBox='0 0 1668 1160'
xmlns='http://www.w3.org/2000/svg'>
<g id='g158' transform='translate(254 -254)'>
<g id='g34' transform='translate(-13.78 3.524)' stroke='#000'>
<path
id='path10'
d='m494.57 1006.9c41.429-15.714 140-11.427 191.43-12.857 51.429-1.429 160.48 10.23 201.43 27.143 40.995 16.93 134.78 67.656 151.43 72.857 22.857 7.143 41.429 7.143 80 20 38.572 12.857 25.714 32.857 25.714 32.857l-30-4.286-5.756 52.92s37.185 1.366 41.47 15.652c4.286 14.286 5.715 31.428-2.856 41.428-8.572 10-14.286-1.428-18.572 12.858-4.286 14.285-2.857 28.571-27.143 27.142-24.285-1.428-98.571 0-98.571 0s-15.714-108.57-98.573-105.71c-82.857 2.857-95.714 105.71-95.714 105.71h-500s-5.714-104.28-97.143-105.71c-91.428-1.428-98.571 105.71-98.571 105.71h-65.713l-18.572-38.571c-0.515 0-26.243 0-21.428-17.143 4.651-16.561-4.286-41.428 17.142-41.428 21.429 0 47.143 1.428 47.143 1.428l15.715-44.286-41.429-2.857s34.286-24.285 118.57-32.857c84.286-8.571 157.14-8.571 192.86-31.428 35.714-22.858 137.14-78.572 137.14-78.572z'
fill='none'
strokeWidth='5'
/>
<path
id='path12'
d='m28.857 1254h92.857m180 0h517.14m172.86 0h151.43'
fill='none'
strokeWidth='2'
/>
<path
id='path14'
d='m364.57 1101.1c15.715-18.572 123.37-81.525 151.43-88.572 29.502-7.41 103-7.142 103-7.142l-7.143 95.714z'
fill='#f0ffeb'
strokeWidth='5'
/>
<path
id='path16'
d='m404.57 1071.1v28.571'
fill='none'
strokeWidth='2'
/>
<path
id='path18'
d='m644.7 1006.8-4.285 94.328h207.16c-11.307-20.753-46.612-74.906-72.857-88.572-14.285-10-82.878-4.327-130.02-5.756zm167.02 7.164s80 84.286 85.715 87.143c5.714 2.857 115.71 1.428 115.71 1.428s-77.143-47.141-102.86-58.571c-25.715-11.429-90-31.429-98.572-30z'
fill='#f0ffeb'
strokeWidth='5'
/>
<g fill='none'>
<path
id='path20'
d='m345.64 1091.7c-14.075 30.968-18.298 71.788-18.298 94.31 0 22.521 4.223 91.493 22.521 105.57m282.93-298.41c-1.408 5.63-4.047 81.903-6.51 106.93-3.67 18.715-4.989 51.219-6.159 87.315 0 22.522 7.038 80.233 12.669 105.57'
strokeWidth='5'
/>
<path
id='path22'
d='m103.53 1252.1s15.483-85.864 106.98-85.864c91.494 0 109.79 87.271 109.79 87.271m479.99 0s15.483-85.863 106.98-85.863c91.493 0 109.79 87.27 109.79 87.27m-947.31-57.711h63.342'
strokeWidth='2'
/>
<path
id='path24'
d='m779.18 1000.2c18.299 12.668 56.304 50.673 92.9 104.16 36.598 53.489 8.447 59.12-18.298 74.603-26.744 15.483-49.266 42.227-67.564 112.61'
strokeWidth='5'
/>
<path
id='path26'
d='m1112.8 1196h-129.5m-696.76-0.222h544.74m-22.522-150.61v54.896'
strokeWidth='2'
/>
</g>
<g strokeWidth='2'>
<rect
id='rect28'
x='558.65'
y='1144.9'
width='41.286'
height='14.541'
rx='2.933'
ry='7.379'
fill='#e6e6e6'
/>
<rect
id='rect30'
x='816.24'
y='1144.9'
width='41.286'
height='14.541'
rx='2.933'
ry='7.379'
fill='#e6e6e6'
/>
<rect
id='rect32'
x='259.53'
y='1146.3'
width='18.776'
height='11.738'
rx='1.63'
ry='7.692'
fill='#ffcb00'
/>
</g>
</g>
<g id='g54' transform='translate(-13.78 15.524)' stroke='#000'>
<path
id='path36'
d='m62.767 812.59-11.712 16.12-18.95-6.157v-19.925l18.95-6.158z'
fill='none'
strokeWidth='5'
/>
<path
id='path38'
d='m31.742 745.02 315.3-111.2m-316.71 254.77 315.3 115.42'
fill='none'
strokeWidth='2'
/>
<path
id='path40'
d='m341.41 632.78 140.76 38.005s-8.446 49.222-8.446 71.963v147.62c0 30.967 8.445 78.825 8.445 78.825l-140.76 33.782s-17.242-61.902-17.242-92.901l2.111-185.8c-1.408-30.967 15.132-91.493 15.132-91.493z'
fill='#f0ffeb'
strokeWidth='5'
/>
<g fill='none'>
<path
id='path42'
d='m482.17 670.79s94.309 5.63 152.02 5.63c106.98 0 201.29-5.63 201.29-5.63m-1.408 297s-77.418-5.63-199.88-5.63c-68.972 0-152.02 7.038-152.02 7.038'
strokeWidth='5'
/>
<rect
id='rect44'
x='528.62'
y='729.2'
width='106.98'
height='181.58'
rx='31.19'
ry='34.436'
strokeWidth='2'
/>
<path
id='path46'
d='m345.78 632.78h-294.19l-14.076 102.75-7.038 11.26v142.17l7.038 14.076 15.483 101.35h288.56m491.11-333.6s-12.668 47.858-12.668 73.195v146.39c0 25.336 12.668 77.417 12.668 77.417l205.51 38.005h108.38s14.076-81.64 14.076-115.42v-147.8c0-38.005-14.076-109.79-14.076-109.79h-108.38z'
strokeWidth='5'
/>
</g>
<path
id='path48'
d='m843.92 689.09s-8.445 40.82-8.445 56.303v144.98c0 19.706 7.038 57.711 7.038 57.711s94.308 26.744 123.87 26.744h42.228v-312.49h-47.859c-25.336 0-116.83 26.745-116.83 26.745z'
fill='#f0ffeb'
strokeWidth='5'
/>
<path
id='path50'
d='m1038.2 632.78s30.967 40.82 30.967 111.2v146.39c0 64.749-30.967 116.83-30.967 116.83m-713.65-263.22 32.374-32.375v-40.82 112.61m-33.078 81.641 32.374-32.375v-40.82 112.61m648.9-116.83-32.375-32.374v-40.82 112.61'
fill='none'
strokeWidth='2'
/>
<path
id='path52'
d='m341.41 632.78s-42.228 78.825-42.228 111.2v146.39c0 40.82 42.228 114.02 42.228 114.02'
fill='none'
strokeWidth='2'
/>
</g>
<g fill='none' stroke='#000'>
<path
id='path56'
d='m-106.79 740.92 1.407-91.493s5.63-23.93-25.337-25.337c-30.967-1.408-26.744 2.815-26.744 2.815l1.408 415.24h28.152c28.152 0 22.521-22.52 22.521-22.52v-95.717c-12.677 0-11.26-9.614-11.26-16.891v-149.2c0.45-18.89 9.853-16.892 9.853-16.892z'
strokeWidth='5'
/>
<path
id='path58'
d='m-155.99 646.16h49.265m-48.415 374.27h49.266m-50.82-315.37h49.266m-46.451 259.81h49.266'
strokeWidth='2.2'
/>
<path
id='path60'
d='m-147.1 703.82v260.4m9.853-258.84v260.4'
strokeWidth='2'
/>
</g>
<g id='g88' transform='translate(-13.78 15.524)' stroke='#000'>
<path
id='path62'
d='m494.57 641.61c41.429 15.714 140 11.428 191.43 12.856s160.48-10.23 201.43-27.143c40.995-16.93 134.78-67.656 151.43-72.857 22.857-7.143 41.429-7.143 80-20 38.572-12.857 25.714-32.857 25.714-32.857l-30 4.285-5.756-52.92s37.185-1.365 41.47-15.651c4.286-14.286 5.715-31.429-2.856-41.429-8.572-10-14.286 1.429-18.572-12.857-4.286-14.285-2.857-28.571-27.143-27.143-24.285 1.429-98.571 0-98.571 0s-15.714 108.57-98.572 105.72c-82.857-2.857-95.714-105.72-95.714-105.72h-500s-5.714 104.29-97.143 105.72c-91.428 1.428-98.571-105.72-98.571-105.72h-65.714l-18.572 38.572c-0.515 0-26.243 0-21.428 17.143 4.651 16.561-4.286 41.428 17.142 41.428 21.429 0 47.143-1.428 47.143-1.428l15.715 44.285-41.429 2.859s34.286 24.285 118.57 32.857c84.286 8.571 157.14 8.571 192.86 31.428 35.714 22.857 137.14 78.572 137.14 78.572z'
fill='none'
strokeWidth='5'
/>
<path
id='path64'
d='m28.857 394.47h92.857m180 0h517.14m172.86 0h151.43'
fill='none'
strokeWidth='2'
/>
<path
id='path66'
d='m364.57 547.33c15.715 18.571 123.37 81.524 151.43 88.571 29.502 7.41 103 7.143 103 7.143l-7.143-95.714z'
fill='#f0ffeb'
strokeWidth='5'
/>
<path
id='path68'
d='m404.57 577.33v-28.571'
fill='none'
strokeWidth='2'
/>
<path
id='path70'
d='m644.7 641.64-4.285-94.328h207.16c-11.307 20.753-46.612 74.906-72.857 88.571-14.285 10-82.878 4.328-130.02 5.757zm167.02-7.165s80-84.285 85.715-87.142c5.714-2.857 115.71-1.429 115.71-1.429s-77.143 47.143-102.86 58.572c-25.715 11.428-90 31.428-98.572 30z'
fill='#f0ffeb'
strokeWidth='5'
/>
<g fill='none'>
<path
id='path72'
d='m345.64 556.81c-14.075-30.967-18.298-71.787-18.298-94.309 0-22.521 4.223-91.493 22.521-105.57m282.93 298.41c-1.408-5.63-4.047-81.905-6.51-106.93-3.67-18.714-4.989-51.218-6.159-87.314 0-22.522 7.038-80.233 12.669-105.57'
strokeWidth='5'
/>
<path
id='path74'
d='m103.53 396.34s15.483 85.864 106.98 85.864c91.494 0 109.79-87.271 109.79-87.271m479.99 0s15.483 85.863 106.98 85.863c91.493 0 109.79-87.27 109.79-87.27m-947.31 57.71h63.342'
strokeWidth='2'
/>
<path
id='path76'
d='m779.18 648.3c18.299-12.669 56.304-50.674 92.9-104.16 36.598-53.489 8.447-59.12-18.298-74.603-26.744-15.483-49.266-42.228-67.564-112.61'
strokeWidth='5'
/>
<path
id='path78'
d='m1112.8 452.42h-129.5m-696.76 0.223h544.74m-22.522 150.61v-54.895'
strokeWidth='2'
/>
</g>
<g strokeWidth='2'>
<rect
id='rect80'
transform='scale(1 -1)'
x='558.65'
y='-503.55'
width='41.286'
height='14.541'
rx='2.933'
ry='7.379'
fill='#e6e6e6'
/>
<rect
id='rect82'
transform='scale(1 -1)'
x='816.24'
y='-503.55'
width='41.286'
height='14.541'
rx='2.933'
ry='7.379'
fill='#e6e6e6'
/>
<rect
id='rect84'
transform='scale(1 -1)'
x='259.53'
y='-502.15'
width='18.776'
height='11.738'
rx='1.63'
ry='7.692'
fill='#ffcb00'
/>
<circle
id='circle86'
transform='translate(941.34 284)'
cx='59.119'
cy='211.28'
r='16.891'
fill='#fff'
/>
</g>
</g>
<path
id='path90'
d='m-126.58 704.93v260.4'
fill='none'
stroke='#000'
strokeWidth='2'
/>
<path
id='path92'
d='m-153.88 992.49v-26.041h45.043v52.08h-45.043zm0-316.71v-27.448h45.043v54.898h-45.043z'
fill='#ffffc0'
/>
<g fill='none' stroke='#000'>
<path
id='path94'
d='m-157.4 624.38s-4.223-12.669-18.299-12.669-14.076 8.446-14.076 8.446v423.69s1.408 9.853 14.076 9.853c12.669 0 18.3-9.853 18.3-9.853'
strokeWidth='5'
/>
<path
id='path96'
d='m-191.18 624.38s-35.19-1.408-35.19 21.114v371.6c0 22.521 36.597 25.336 36.597 25.336'
strokeWidth='5'
/>
<g strokeWidth='2'>
<rect
id='rect98'
x='-216.51'
y='648.31'
width='16.891'
height='35.19'
rx='7.742'
ry='6.284'
/>
<rect
id='rect100'
x='-216.51'
y='985.58'
width='16.891'
height='35.19'
rx='7.742'
ry='6.284'
/>
<path id='path102' d='m-62.939 645.49h30.967v377.24h-30.967z' />
</g>
<path
id='path104'
d='m1200.9 633.86v73.195h67.565v-83.048l-25.337-14.076zm0 329.38h67.565v78.826l-23.93 14.076-43.635-18.3zm0-270.26v284.33m67.565-283.74v284.33'
strokeWidth='5'
/>
<path
id='path106'
d='m1216.6 759.51h14.076v147.8h-14.076zm7.631-1.408v-53.488m0 257.37v-53.49m30.375-201.06v256.18'
strokeWidth='2'
/>
<path
id='path108'
d='m1268.8 624.97s4.223-12.668 18.299-12.668 14.076 8.445 14.076 8.445v423.69s-1.408 9.853-14.076 9.853c-12.669 0-18.299-9.853-18.299-9.853m33.783-419.46s35.19-1.408 35.19 21.114v371.6c0 22.521-36.598 25.337-36.598 25.337'
strokeWidth='5'
/>
<g strokeWidth='2'>
<rect
id='rect110'
transform='scale(-1 1)'
x='-1329.9'
y='648.9'
width='16.891'
height='35.19'
rx='7.742'
ry='6.284'
/>
<rect
id='rect112'
transform='scale(-1 1)'
x='-1329.9'
y='986.17'
width='16.891'
height='35.19'
rx='7.742'
ry='6.284'
/>
<path
id='path114'
d='m1199.7 632.82 67.565 74.603m-67.565 0.592 67.565-73.195m-67.342 328.16 67.565 74.602m-67.565 0.593 67.565-73.195'
/>
</g>
</g>
<g stroke='#000'>
<g strokeWidth='2'>
<circle
id='circle116'
transform='translate(78.489 1074.7)'
cx='119.65'
cy='202.84'
r='76.01'
fill='#c8c8c8'
/>
<circle
id='circle118'
transform='translate(94.676 1204.9)'
cx='103.46'
cy='72.633'
r='44.339'
fill='#fff'
/>
<circle
id='circle120'
transform='translate(78.489 187.66)'
cx='119.65'
cy='202.84'
r='76.01'
fill='#c8c8c8'
/>
<circle
id='circle122'
transform='translate(94.676 317.86)'
cx='103.46'
cy='72.633'
r='44.339'
fill='#fff'
/>
<circle
id='circle124'
transform='translate(773.02 187.66)'
cx='119.65'
cy='202.84'
r='76.01'
fill='#c8c8c8'
/>
<circle
id='circle126'
transform='translate(789.21 317.86)'
cx='103.46'
cy='72.633'
r='44.339'
fill='#fff'
/>
<circle
id='circle128'
transform='translate(773.02 1074.7)'
cx='119.65'
cy='202.84'
r='76.01'
fill='#c8c8c8'
/>
<circle
id='circle130'
transform='translate(789.21 1204.9)'
cx='103.46'
cy='72.633'
r='44.339'
fill='#fff'
/>
</g>
<path
id='path132'
d='m1338.5 829.07h40.82'
fill='none'
strokeWidth='5'
/>
<circle
id='circle134'
transform='translate(1441.3 600.31)'
cx='-59.119'
cy='229.58'
r='4.223'
fill='#3c3c3c'
strokeWidth='5'
/>
<g strokeWidth='2'>
<path id='path136' d='m778.06 845.37-38.709-38.709' fill='none' />
<g id='g146' transform='translate(-13.78 15.524)' fill='#3c3c3c'>
<circle
id='circle138'
transform='translate(-79.458 449.8)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle140'
transform='translate(-79.458 569.92)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle142'
transform='translate(-79.458 690.03)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle144'
transform='translate(-79.458 810.15)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
</g>
<g id='g156' transform='translate(-13.78 17.524)' fill='#3c3c3c'>
<circle
id='circle148'
transform='translate(1381.6 448.25)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle150'
transform='translate(1381.6 568.36)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle152'
transform='translate(1381.6 688.48)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
<circle
id='circle154'
transform='translate(1381.6 808.59)'
cx='-81.641'
cy='188.76'
r='4.223'
/>
</g>
</g>
</g>
</g>
<g id='layer2' fill='#d00000'>
<circle
id='p02'
cx='503.65'
cy='248.75'
r='61.935'
opacity={dmg1 === "02" ? 100 : 0}
/>
<circle
id='p03'
cx='863.41'
cy='248.75'
r='61.935'
opacity={dmg1 === "03" ? 100 : 0}
/>
<circle
id='p04'
cx='1181.5'
cy='248.75'
r='61.935'
opacity={dmg1 === "04" ? 100 : 0}
/>
<circle
id='p05'
cx='1378.4'
cy='151.16'
r='61.935'
opacity={dmg1 === "05" ? 100 : 0}
/>
<circle
id='p06'
cx='1535.1'
cy='581.37'
r='61.935'
opacity={dmg1 === "06" ? 100 : 0}
/>
<circle
id='p07'
cx='1378.4'
cy='997.9'
r='61.935'
opacity={dmg1 === "07" ? 100 : 0}
/>
<circle
id='p08'
cx='1181.5'
cy='914.24'
r='61.935'
opacity={dmg1 === "08" ? 100 : 0}
/>
<circle
id='p09'
transform='scale(1,-1)'
cx='863.41'
cy='-914.24'
r='61.935'
opacity={dmg1 === "09" ? 100 : 0}
/>
<circle
id='p10'
cx='503.65'
cy='914.24'
r='61.935'
opacity={dmg1 === "10" ? 100 : 0}
/>
<circle
id='p11'
cx='297.77'
cy='997.9'
r='61.935'
opacity={dmg1 === "11" ? 100 : 0}
/>
<circle
id='p12'
cx='93.269'
cy='581.37'
r='61.935'
opacity={dmg1 === "12" ? 100 : 0}
/>
<circle
id='p25'
cx='424.31'
cy='581.37'
r='61.935'
opacity={dmg1 === "25" ? 100 : 0}
/>
<circle
id='p27'
cx='972.84'
cy='581.37'
r='61.935'
opacity={dmg1 === "27" ? 100 : 0}
/>
<circle
id='p01'
cx='297.77'
cy='151.16'
r='61.935'
opacity={dmg1 === "01" ? 100 : 0}
/>
<circle
id='p26'
cx='1339.4'
cy='581.37'
r='61.935'
opacity={dmg1 === "26" ? 100 : 0}
/>
</g>
<g id='g4994' fill='#ffef00'>
<circle
id='s02'
cx='503.65'
cy='248.75'
r='61.935'
opacity={dmg2 === "02" ? 100 : 0}
/>
<circle
id='s03'
cx='863.41'
cy='248.75'
r='61.935'
opacity={dmg2 === "03" ? 100 : 0}
/>
<circle
id='s04'
cx='1181.5'
cy='248.75'
r='61.935'
opacity={dmg2 === "04" ? 100 : 0}
/>
<circle
id='s05'
cx='1378.4'
cy='151.16'
r='61.935'
opacity={dmg2 === "05" ? 100 : 0}
/>
<circle
id='s06'
cx='1535.1'
cy='581.37'
r='61.935'
opacity={dmg2 === "06" ? 100 : 0}
/>
<circle
id='s07'
cx='1378.4'
cy='997.9'
r='61.935'
opacity={dmg2 === "07" ? 100 : 0}
/>
<circle
id='s08'
cx='1181.5'
cy='914.24'
r='61.935'
opacity={dmg2 === "08" ? 100 : 0}
/>
<circle
id='s09'
transform='scale(1,-1)'
cx='863.41'
cy='-914.24'
r='61.935'
opacity={dmg2 === "09" ? 100 : 0}
/>
<circle
id='s10'
cx='503.65'
cy='914.24'
r='61.935'
opacity={dmg2 === "10" ? 100 : 0}
/>
<circle
id='s11'
cx='297.77'
cy='997.9'
r='61.935'
opacity={dmg2 === "11" ? 100 : 0}
/>
<circle
id='s12'
cx='93.269'
cy='581.37'
r='61.935'
opacity={dmg2 === "12" ? 100 : 0}
/>
<circle
id='s25'
cx='424.31'
cy='581.37'
r='61.935'
opacity={dmg2 === "25" ? 100 : 0}
/>
<circle
id='s27'
cx='972.84'
cy='581.37'
r='61.935'
opacity={dmg2 === "27" ? 100 : 0}
/>
<circle
id='s01'
cx='297.77'
cy='151.16'
r='61.935'
opacity={dmg2 === "01" ? 100 : 0}
/>
<circle
id='s26'
cx='1339.4'
cy='581.37'
r='61.935'
opacity={dmg2 === "26" ? 100 : 0}
/>
{
// <text
// id='p15'
// opacity='0'
// x='382.62802'
// y='1034.3463'
// fill='#fd0000'
// fontFamily='sans-serif'
// fontSize='1696.9px'
// letterSpacing='0px'
// strokeWidth='17.676'
// wordSpacing='0px'
// style='line-height:5.25'>
// x
// </text>
}
</g>
</svg>
);

View File

@@ -17,8 +17,7 @@ import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component"; import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
import "./job-detail-cards.styles.scss"; import "./job-detail-cards.styles.scss";
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component"; import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
import ScheduleJobModalContainer from "../schedule-job-modal/schedule-job-modal.container";
export default function JobDetailCards({ selectedJob }) { export default function JobDetailCards({ selectedJob }) {
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, { const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
@@ -27,6 +26,7 @@ export default function JobDetailCards({ selectedJob }) {
skip: !selectedJob skip: !selectedJob
}); });
const [noteModalVisible, setNoteModalVisible] = useState(false); const [noteModalVisible, setNoteModalVisible] = useState(false);
const scheduleModalState = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
if (!selectedJob) { if (!selectedJob) {
@@ -43,13 +43,18 @@ export default function JobDetailCards({ selectedJob }) {
changeVisibility={setNoteModalVisible} changeVisibility={setNoteModalVisible}
refetch={refetch} refetch={refetch}
/> />
<ScheduleJobModalContainer
scheduleModalState={scheduleModalState}
jobId={data.jobs_by_pk.id}
refetch={refetch}
/>
<PageHeader <PageHeader
ghost={false} ghost={false}
onBack={() => window.history.back()} onBack={() => window.history.back()}
tags={ tags={
<span key='job-status'> <span key='job-status'>
{data.jobs_by_pk.job_status ? ( {data.jobs_by_pk.status ? (
<Tag color='blue'>{data.jobs_by_pk.job_status.name}</Tag> <Tag color='blue'>{data.jobs_by_pk.status}</Tag>
) : null} ) : null}
</span> </span>
} }
@@ -67,6 +72,14 @@ export default function JobDetailCards({ selectedJob }) {
) )
} }
extra={[ extra={[
<Button
key='schedule'
//TODO Enabled logic based on status.
onClick={() => {
scheduleModalState[1](true);
}}>
{t("jobs.actions.schedule")}
</Button>,
<Link <Link
key='documents' key='documents'
to={`/manage/jobs/${data.jobs_by_pk.id}#documents`}> to={`/manage/jobs/${data.jobs_by_pk.id}#documents`}>

View File

@@ -14,7 +14,10 @@ export default function JobDetailCardsCustomerComponent({ loading, data }) {
extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}> extraLink={data && data.owner ? `/manage/owners/${data.owner.id}` : null}>
{data ? ( {data ? (
<span> <span>
<div>{`${data.ownr_fn || ""} ${data.ownr_ln || ""}`}</div> <div>
<Link to={`/manage/owners/${data.owner.id}`}>{`${data.ownr_fn ||
""} ${data.ownr_ln || ""}`}</Link>
</div>
<div> <div>
{t("jobs.fields.phoneshort")}: {t("jobs.fields.phoneshort")}:
<PhoneFormatter>{`${data.ownr_ph1 || <PhoneFormatter>{`${data.ownr_ph1 ||

View File

@@ -1,18 +1,14 @@
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import CardTemplate from "./job-detail-cards.template.component"; import CardTemplate from "./job-detail-cards.template.component";
import UnfoldedCar from "../../assets/unfolded_car.svg"; import Car from "../job-damage-visual/job-damage-visual.component";
export default function JobDetailCardsDamageComponent({ loading, data }) { export default function JobDetailCardsDamageComponent({ loading, data }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { area_of_damage } = data;
return ( return (
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}> <CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
{data ? ( <Car dmg1={area_of_damage.impact1} dmg2={area_of_damage.impact2} />
<span>
<img src={UnfoldedCar} alt='Damaged Area' width={200} height={200} />
</span>
) : null}
</CardTemplate> </CardTemplate>
); );
} }

View File

@@ -1,8 +1,8 @@
import { Timeline } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DateFormatter } from "../../utils/DateFormatter";
import CardTemplate from "./job-detail-cards.template.component"; import CardTemplate from "./job-detail-cards.template.component";
import Moment from "react-moment";
import { Timeline } from "antd";
export default function JobDetailCardsDatesComponent({ loading, data }) { export default function JobDetailCardsDatesComponent({ loading, data }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -31,90 +31,84 @@ export default function JobDetailCardsDatesComponent({ loading, data }) {
{data.actual_in ? ( {data.actual_in ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.actual_in")} {t("jobs.fields.actual_in")}
<Moment format='MM/DD/YYYY'>{data.actual_in || ""}</Moment> <DateFormatter>{data.actual_in}</DateFormatter>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
{data.scheduled_completion ? ( {data.scheduled_completion ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.scheduled_completion")} {t("jobs.fields.scheduled_completion")}
<Moment format='MM/DD/YYYY'> <DateFormatter>{data.scheduled_completion}</DateFormatter>
{data.scheduled_completion || ""}
</Moment>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
{data.scheduled_in ? ( {data.scheduled_in ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.scheduled_in")} {t("jobs.fields.scheduled_in")}
<Moment format='MM/DD/YYYY'>{data.scheduled_in || ""}</Moment> <DateFormatter>{data.scheduled_in}</DateFormatter>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
{data.actual_completion ? ( {data.actual_completion ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.actual_completion")} {t("jobs.fields.actual_completion")}
<Moment format='MM/DD/YYYY'> <DateFormatter>{data.actual_completion}</DateFormatter>
{data.actual_completion || ""}
</Moment>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
{data.scheduled_delivery ? ( {data.scheduled_delivery ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.scheduled_delivery")} {t("jobs.fields.scheduled_delivery")}
<Moment format='MM/DD/YYYY'> <DateFormatter>{data.scheduled_delivery}</DateFormatter>
{data.scheduled_delivery || ""}
</Moment>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
{data.actual_delivery ? ( {data.actual_delivery ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.actual_delivery")} {t("jobs.fields.actual_delivery")}
<Moment format='MM/DD/YYYY'>{data.actual_delivery || ""}</Moment> <DateFormatter>{data.actual_delivery}</DateFormatter>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
{data.date_estimated ? ( {data.date_estimated ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.date_estimated")} {t("jobs.fields.date_estimated")}
<Moment format='MM/DD/YYYY'>{data.date_estimated || ""}</Moment> <DateFormatter>{data.date_estimated}</DateFormatter>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
{data.date_open ? ( {data.date_open ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.date_open")} {t("jobs.fields.date_open")}
<Moment format='MM/DD/YYYY'>{data.date_open || ""}</Moment> <DateFormatter>{data.date_open}</DateFormatter>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
{data.date_scheduled ? ( {data.date_scheduled ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.date_scheduled")} {t("jobs.fields.date_scheduled")}
<Moment format='MM/DD/YYYY'>{data.date_scheduled || ""}</Moment> <DateFormatter>{data.date_scheduled}</DateFormatter>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
{data.date_invoiced ? ( {data.date_invoiced ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.date_invoiced")} {t("jobs.fields.date_invoiced")}
<Moment format='MM/DD/YYYY'>{data.date_invoiced || ""}</Moment> <DateFormatter>{data.date_invoiced}</DateFormatter>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
{data.date_closed ? ( {data.date_closed ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.date_closed")} {t("jobs.fields.date_closed")}
<Moment format='MM/DD/YYYY'>{data.date_closed || ""}</Moment> <DateFormatter>{data.date_closed}</DateFormatter>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
{data.date_exported ? ( {data.date_exported ? (
<Timeline.Item> <Timeline.Item>
{t("jobs.fields.date_exported")} {t("jobs.fields.date_exported")}
<Moment format='MM/DD/YYYY'>{data.date_exported || ""}</Moment> <DateFormatter>{data.date_exported}</DateFormatter>
</Timeline.Item> </Timeline.Item>
) : null} ) : null}
</Timeline> </Timeline>

View File

@@ -18,13 +18,12 @@ export default function JobDetailCardsDocumentsComponent({ loading, data }) {
<CardTemplate <CardTemplate
loading={loading} loading={loading}
title={t("jobs.labels.cards.documents")} title={t("jobs.labels.cards.documents")}
extraLink={`/manage/jobs/${data.id}#documents`}> extraLink={`/manage/jobs/${data.id}#documents`}
{data.documents.count > 0 ? ( >
{data.documents.length > 0 ? (
<Carousel autoplay> <Carousel autoplay>
{data.documents.map(item => ( {data.documents.map(item => (
<div key={item.id}> <img key={item.id} src={item.thumb_url} alt={item.name} />
<img src={item.thumb_url} alt={item.name} />
</div>
))} ))}
</Carousel> </Carousel>
) : ( ) : (

View File

@@ -1,11 +1,12 @@
.ant-carousel .slick-slide { .ant-carousel .slick-slide {
text-align: center; text-align: center;
height: 160px; height: 50px;
line-height: 160px; width: 50px;
background: #364d79; line-height: 50px;
overflow: hidden; background: #364d79;
} overflow: hidden;
}
.ant-carousel .slick-slide h3 { .ant-carousel .slick-slide h3 {
color: #fff; color: #ccddaa;
} }

View File

@@ -10,45 +10,45 @@ export default function JobDetailCardsInsuranceComponent({ loading, data }) {
<CardTemplate loading={loading} title={t("jobs.labels.cards.insurance")}> <CardTemplate loading={loading} title={t("jobs.labels.cards.insurance")}>
{data ? ( {data ? (
<span> <span>
<div>{data?.ins_co_nm || t("general.labels.unknown")}</div> <div>{data.ins_co_nm || t("general.labels.unknown")}</div>
<div>{data?.clm_no || t("general.labels.unknown")}</div> <div>{data.clm_no || t("general.labels.unknown")}</div>
<div> <div>
{t("jobs.labels.cards.filehandler")} {t("jobs.labels.cards.filehandler")}
{data?.ins_ea ? ( {data.ins_ea ? (
<a href={`mailto:${data.ins_ea}`}> <a href={`mailto:${data.ins_ea}`}>
<div>{`${data?.ins_ct_fn || ""} ${data?.ins_ct_ln || ""}`}</div> <div>{`${data.ins_ct_fn || ""} ${data.ins_ct_ln || ""}`}</div>
</a> </a>
) : ( ) : (
<div>{`${data?.ins_ct_fn || ""} ${data?.ins_ct_ln || ""}`}</div> <div>{`${data.ins_ct_fn || ""} ${data.ins_ct_ln || ""}`}</div>
)} )}
{data?.ins_ph1 ? ( {data.ins_ph1 ? (
<PhoneFormatter>{data?.ins_ph1}</PhoneFormatter> <PhoneFormatter>{data.ins_ph1}</PhoneFormatter>
) : null} ) : null}
</div> </div>
<div> <div>
{t("jobs.labels.cards.appraiser")} {t("jobs.labels.cards.appraiser")}
{data?.est_ea ? ( {data.est_ea ? (
<a href={`mailto:${data.est_ea}`}> <a href={`mailto:${data.est_ea}`}>
<div>{`${data?.ins_ct_fn || ""} ${data?.ins_ct_ln || ""}`}</div> <div>{`${data.ins_ct_fn || ""} ${data.ins_ct_ln || ""}`}</div>
</a> </a>
) : ( ) : (
<div>{`${data?.ins_ct_fn || ""} ${data?.ins_ct_ln || ""}`}</div> <div>{`${data.ins_ct_fn || ""} ${data.ins_ct_ln || ""}`}</div>
)} )}
</div> </div>
<div> <div>
{t("jobs.labels.cards.estimator")} {t("jobs.labels.cards.estimator")}
{data?.est_ea ? ( {data.est_ea ? (
<a href={`mailto:${data.est_ea}`}> <a href={`mailto:${data.est_ea}`}>
<div>{`${data?.est_ct_fn || ""} ${data?.est_ct_ln || ""}`}</div> <div>{`${data.est_ct_fn || ""} ${data.est_ct_ln || ""}`}</div>
</a> </a>
) : ( ) : (
<div>{`${data?.est_ct_fn || ""} ${data?.est_ct_ln || ""}`}</div> <div>{`${data.est_ct_fn || ""} ${data.est_ct_ln || ""}`}</div>
)} )}
{data?.est_ph1 ? ( {data.est_ph1 ? (
<PhoneFormatter>{data?.est_ph1}</PhoneFormatter> <PhoneFormatter>{data.est_ph1}</PhoneFormatter>
) : null} ) : null}
</div> </div>
</span> </span>
) : null} ) : null}

View File

@@ -22,7 +22,7 @@ export default function JobDetailCardsNotesComponent({ loading, data }) {
<List <List
size='small' size='small'
bordered bordered
dataSource={data?.notes} dataSource={data.notes}
renderItem={item => ( renderItem={item => (
<List.Item> <List.Item>
{item.critical ? ( {item.critical ? (

View File

@@ -9,7 +9,7 @@ export default function JobDetailCardsVehicleComponent({ loading, data }) {
<CardTemplate <CardTemplate
loading={loading} loading={loading}
title={t("jobs.labels.cards.vehicle")} title={t("jobs.labels.cards.vehicle")}
extraLink={data?.vehicle ? `/manage/vehicles/${data?.vehicle?.id}` : null} extraLink={data.vehicle ? `/manage/vehicles/${data.vehicle.id}` : null}
> >
{data ? ( {data ? (
<span> <span>

View File

@@ -1,52 +0,0 @@
import React from "react";
import { Form, Input, InputNumber } from "antd";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
export default class EditableCell extends React.Component {
getInput = () => {
if (this.props.inputType === "number") {
return <InputNumber />;
}
return <Input />;
};
renderCell = ({ getFieldDecorator }) => {
const {
editing,
dataIndex,
title,
inputType,
record,
index,
children,
...restProps
} = this.props;
return (
<td {...restProps}>
{editing ? (
<Form.Item style={{ margin: 0 }}>
{getFieldDecorator(dataIndex, {
rules: [
{
required: true,
message: `Please Input ${title}!`
}
],
initialValue: record[dataIndex]
})(this.getInput())}
</Form.Item>
) : (
children
)}
</td>
);
};
render() {
return (
<JobDetailFormContext.Consumer>
{this.renderCell}
</JobDetailFormContext.Consumer>
);
}
}

View File

@@ -1,62 +1,103 @@
import { Table, Button } from "antd"; import { Button, Input, Table } from "antd";
import React, { useContext, useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context"; import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import EditableCell from "./job-lines-cell.component"; import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container";
import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
export default function JobLinesComponent({ job }) { export default function JobLinesComponent({
//const form = useContext(JobDetailFormContext); loading,
//const { getFieldDecorator } = form; refetch,
jobLines,
setSearchText,
selectedLines,
setSelectedLines,
partsOrderModalVisible,
jobId,
setJobLineEditContext
}) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {}
filteredInfo: { text: "" }
}); });
const [editingKey, setEditingKey] = useState("");
const { t } = useTranslation(); const { t } = useTranslation();
const setPartsModalVisible = partsOrderModalVisible[1];
const columns = [ const columns = [
{ {
title: t("joblines.fields.unq_seq"), title: t("joblines.fields.unq_seq"),
dataIndex: "joblines.unq_seq", dataIndex: "unq_seq",
key: "joblines.unq_seq", key: "unq_seq",
// onFilter: (value, record) => record.ro_number.includes(value), // onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null, // filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a, b), sorter: (a, b) => a.unq_seq - b.unq_seq,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "unq_seq" && state.sortedInfo.order, state.sortedInfo.columnKey === "unq_seq" && state.sortedInfo.order,
//ellipsis: true, //ellipsis: true,
editable: true editable: true,
width: 75
}, },
{ {
title: t("joblines.fields.line_desc"), title: t("joblines.fields.line_desc"),
dataIndex: "line_desc", dataIndex: "line_desc",
key: "joblines.line_desc", key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc), sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order, state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true, ellipsis: true,
editable: true editable: true,
width: "20%"
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) =>
alphaSort(
a.oem_partno ? a.oem_partno : a.op_code_desc,
b.oem_partno ? b.oem_partno : b.op_code_desc
),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
editable: true,
width: "10%",
render: (text, record) => (
<span>
{record.oem_partno ? record.oem_partno : record.op_code_desc}
</span>
)
}, },
{ {
title: t("joblines.fields.part_type"), title: t("joblines.fields.part_type"),
dataIndex: "part_type", dataIndex: "part_type",
key: "joblines.part_type", key: "part_type",
sorter: (a, b) => alphaSort(a.part_type, b.part_type), sorter: (a, b) => alphaSort(a.part_type, b.part_type),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order, state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
ellipsis: true, ellipsis: true,
editable: true editable: true,
width: "7%"
},
{
title: t("joblines.fields.line_ind"),
dataIndex: "line_ind",
key: "line_ind",
sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
sortOrder:
state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order
}, },
{ {
title: t("joblines.fields.db_price"), title: t("joblines.fields.db_price"),
dataIndex: "db_price", dataIndex: "db_price",
key: "joblines.db_price", key: "db_price",
sorter: (a, b) => a.db_price - b.db_price, sorter: (a, b) => a.db_price - b.db_price,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "db_price" && state.sortedInfo.order, state.sortedInfo.columnKey === "db_price" && state.sortedInfo.order,
ellipsis: true, ellipsis: true,
width: "8%",
render: (text, record) => ( render: (text, record) => (
<CurrencyFormatter>{record.db_price}</CurrencyFormatter> <CurrencyFormatter>{record.db_price}</CurrencyFormatter>
) )
@@ -64,22 +105,85 @@ export default function JobLinesComponent({ job }) {
{ {
title: t("joblines.fields.act_price"), title: t("joblines.fields.act_price"),
dataIndex: "act_price", dataIndex: "act_price",
key: "joblines.act_price", key: "act_price",
sorter: (a, b) => a.act_price - b.act_price, sorter: (a, b) => a.act_price - b.act_price,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order, state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true, ellipsis: true,
width: "8%",
render: (text, record) => ( render: (text, record) => (
<div> <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
{" "} )
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>{" "} },
{
title: t("joblines.fields.mod_lb_hrs"),
dataIndex: "mod_lb_hrs",
key: "mod_lb_hrs",
sorter: (a, b) => a.mod_lb_hrs - b.mod_lb_hrs,
sortOrder:
state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order
},
{
title: t("joblines.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order
},
{
title: t("allocations.fields.employee"),
dataIndex: "employee",
key: "employee",
width: "10%",
sorter: (a, b) =>
alphaSort(
a.allocations[0] &&
a.allocations[0].employee.first_name +
a.allocations[0].employee.last_name,
b.allocations[0] &&
b.allocations[0].employee.first_name +
b.allocations[0].employee.last_name
),
sortOrder:
state.sortedInfo.columnKey === "employee" && state.sortedInfo.order,
render: (text, record) => (
<span>
{record.allocations && record.allocations.length > 0
? record.allocations.map(item => (
<AllocationsEmployeeLabelContainer
key={item.id}
refetch={refetch}
allocation={item}
/>
))
: null}
<AllocationsAssignmentContainer
key={record.id}
refetch={refetch}
jobLineId={record.id}
hours={record.mod_lb_hrs}
/>
</span>
)
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<span>
<Button <Button
onClick={() => { onClick={() => {
setEditingKey(record.id); setJobLineEditContext({
}}> actions: { refetch: refetch },
EDIT context: record
});
}}
>
{t("general.actions.edit")}
</Button> </Button>
</div> </span>
) )
} }
]; ];
@@ -88,37 +192,91 @@ export default function JobLinesComponent({ job }) {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
// const handleChange = event => { const formItemLayout = {
// const { value } = event.target; labelCol: {
// setState({ ...state, filterinfo: { text: [value] } }); xs: { span: 12 },
// }; sm: { span: 5 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 }
}
};
return ( return (
<Table <div>
size='small' <PartsOrderModalContainer
pagination={{ position: "bottom" }} partsOrderModalVisible={partsOrderModalVisible}
columns={columns.map(col => { linesToOrder={selectedLines}
if (!col.editable) { refetch={refetch}
return col; jobId={jobId}
} />
return {
...col, <Table
onCell: record => ({ title={() => {
record, return (
inputType: col.dataIndex === "age" ? "number" : "text", <div>
dataIndex: col.dataIndex, <Input.Search
title: col.title, placeholder={t("general.labels.search")}
editing: editingKey === record.id onChange={e => {
}) e.preventDefault();
}; setSearchText(e.target.value);
})} }}
components={{ />
body: { <Button
cell: EditableCell disabled={selectedLines.length > 0 ? false : true}
} onClick={() => setPartsModalVisible(true)}
}} >
rowKey='id' {t("parts.actions.order")}
dataSource={job.joblines} </Button>
onChange={handleTableChange} <AllocationsBulkAssignmentContainer
/> jobLines={selectedLines}
refetch={refetch}
/>
<Button
onClick={() => {
setJobLineEditContext({
actions: { refetch: refetch },
context: { jobid: jobId }
});
}}
>
{t("joblines.actions.new")}
</Button>
</div>
);
}}
{...formItemLayout}
loading={loading}
size="small"
expandedRowRender={record => (
<div style={{ margin: 0 }}>
<strong>{t("parts_orders.labels.orderhistory")}</strong>
{record.parts_order_lines.map(item => (
<div key={item.id}>
{`${item.parts_order.order_number || ""} from `}
<Link to={`/manage/shop/vendors/${item.parts_order.vendor.id}`}>
{item.parts_order.vendor.name || ""}
</Link>
{` on ${item.parts_order.order_date || ""}`}
</div>
))}
</div>
)}
pagination={{ position: "top", defaultPageSize: 25 }}
rowSelection={{
// selectedRowKeys: selectedLines,
onSelectAll: (selected, selectedRows, changeRows) => {
setSelectedLines(selectedRows);
},
onSelect: (record, selected, selectedRows, nativeEvent) =>
setSelectedLines(selectedRows)
}}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={jobLines}
onChange={handleTableChange}
/>
</div>
); );
} }

View File

@@ -1,19 +1,72 @@
import React from "react";
import JobLinesComponent from "./job-lines.component";
import { useQuery } from "@apollo/react-hooks"; import { useQuery } from "@apollo/react-hooks";
import AlertComponent from "../alert/alert.component"; import React, { useState } from "react";
import { GET_JOB_LINES_BY_PK } from "../../graphql/jobs-lines.queries"; import { GET_JOB_LINES_BY_PK } from "../../graphql/jobs-lines.queries";
import AlertComponent from "../alert/alert.component";
import JobLinesComponent from "./job-lines.component";
export default function JobLinesContainer({ jobId }) { import { connect } from "react-redux";
import { setModalContext } from "../../redux/modals/modals.actions";
const { loading, error, data } = useQuery(GET_JOB_LINES_BY_PK, { const mapDispatchToProps = dispatch => ({
setJobLineEditContext: context =>
dispatch(setModalContext({ context: context, modal: "jobLineEdit" }))
});
export default connect(
null,
mapDispatchToProps
)(function JobLinesContainer({ jobId, setJobLineEditContext }) {
const { loading, error, data, refetch } = useQuery(GET_JOB_LINES_BY_PK, {
variables: { id: jobId }, variables: { id: jobId },
fetchPolicy: "network-only" fetchPolicy: "network-only"
}); });
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<JobLinesComponent loading={loading} joblines={data ? data.joblines : null} />
);
}
const [searchText, setSearchText] = useState("");
const [selectedLines, setSelectedLines] = useState([]);
const partsOrderModalVisible = useState(false);
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<JobLinesComponent
loading={loading}
refetch={refetch}
jobLines={
data && data.joblines
? searchText
? data.joblines.filter(
jl =>
(jl.unq_seq || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.line_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.part_type || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.oem_partno || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.op_code_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(jl.db_price || "")
.toString()
.includes(searchText.toLowerCase()) ||
(jl.act_price || "")
.toString()
.includes(searchText.toLowerCase())
)
: data.joblines
: null
}
setSearchText={setSearchText}
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
partsOrderModalVisible={partsOrderModalVisible}
jobId={jobId}
setJobLineEditContext={setJobLineEditContext}
/>
);
});

View File

@@ -0,0 +1,69 @@
import { Modal, Form, Input, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import ResetForm from "../form-items-formatted/reset-form-item.component";
export default function JobLinesUpsertModalComponent({
visible,
jobLine,
handleOk,
handleCancel,
handleSubmit,
form
}) {
const { t } = useTranslation();
const { getFieldDecorator, isFieldsTouched, resetFields } = 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}
>
{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" />)}
</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>
</Modal>
);
}

View File

@@ -0,0 +1,110 @@
import { Form, notification } from "antd";
import React from "react";
import { useMutation } from "react-apollo";
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 { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
const mapStateToProps = createStructuredSelector({
jobLineEditModal: selectJobLineEditModal
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit"))
});
function JobLinesUpsertModalContainer({
jobLineEditModal,
toggleModalVisible,
form
}) {
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
})
});
});
if (jobLineEditModal.actions.refetch)
jobLineEditModal.actions.refetch();
toggleModalVisible();
}
}
});
};
const handleCancel = () => {
toggleModalVisible();
};
return (
<JobLinesUpdsertModal
visible={jobLineEditModal.visible}
jobLine={jobLineEditModal.context}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
form={form}
/>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(
Form.create({ name: "JobsDetailPageContainer" })(JobLinesUpsertModalContainer)
);

View File

@@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.container"; import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.container";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
export default function JobsAvailableComponent({ export default function JobsAvailableComponent({
loading, loading,
data, data,
@@ -77,7 +79,10 @@ export default function JobsAvailableComponent({
key: "clm_amt", key: "clm_amt",
sorter: (a, b) => a.clm_amt - b.clm_amt, sorter: (a, b) => a.clm_amt - b.clm_amt,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "clm_amt" && state.sortedInfo.order state.sortedInfo.columnKey === "clm_amt" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_amt}</CurrencyFormatter>
)
//width: "12%", //width: "12%",
//ellipsis: true //ellipsis: true
}, },
@@ -141,7 +146,8 @@ export default function JobsAvailableComponent({
estData.data.available_jobs_by_pk && estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data && estData.data.available_jobs_by_pk.est_data &&
estData.data.available_jobs_by_pk.est_data.owner && estData.data.available_jobs_by_pk.est_data.owner &&
estData.data.available_jobs_by_pk.est_data.owner.data estData.data.available_jobs_by_pk.est_data.owner.data &&
!estData.data.available_jobs_by_pk.issupplement
? estData.data.available_jobs_by_pk.est_data.owner.data ? estData.data.available_jobs_by_pk.est_data.owner.data
: null; : null;
@@ -164,7 +170,7 @@ export default function JobsAvailableComponent({
return ( return (
<div> <div>
<Input.Search <Input.Search
placeholder="Search..." placeholder="Search...//TODO Implement Search"
onSearch={value => { onSearch={value => {
console.log(value); console.log(value);
}} }}

View File

@@ -29,14 +29,7 @@ export default withRouter(function JobsAvailableContainer({
const onModalOk = () => { const onModalOk = () => {
setModalVisible(false); setModalVisible(false);
console.log("selectedOwner", selectedOwner);
setInsertLoading(true); setInsertLoading(true);
console.log(
"logitest",
estData.data &&
estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data
);
if ( if (
!( !(

View File

@@ -3,13 +3,25 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
export default function JobsAvailableSupplementComponent({ export default function JobsAvailableSupplementComponent({
loading, loading,
data, data,
refetch, refetch,
deleteJob, deleteJob,
updateJob,
onModalOk,
onModalCancel,
modalVisible,
setModalVisible,
selectedJob,
setSelectedJob,
deleteAllNewJobs, deleteAllNewJobs,
estDataLazyLoad loadEstData,
estData,
importOptionsState
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -81,7 +93,10 @@ export default function JobsAvailableSupplementComponent({
key: "clm_amt", key: "clm_amt",
sorter: (a, b) => a.clm_amt - b.clm_amt, sorter: (a, b) => a.clm_amt - b.clm_amt,
sortOrder: sortOrder:
state.sortedInfo.columnKey === "clm_amt" && state.sortedInfo.order state.sortedInfo.columnKey === "clm_amt" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_amt}</CurrencyFormatter>
)
//width: "12%", //width: "12%",
//ellipsis: true //ellipsis: true
}, },
@@ -127,7 +142,8 @@ export default function JobsAvailableSupplementComponent({
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
alert("Add"); loadEstData({ variables: { id: record.id } });
setModalVisible(true);
}} }}
> >
<Icon type="plus" /> <Icon type="plus" />
@@ -140,54 +156,66 @@ export default function JobsAvailableSupplementComponent({
]; ];
return ( return (
<Table <div>
loading={loading} <JobsFindModalContainer
title={() => { loading={estData.loading}
return ( error={estData.error}
<div> selectedJob={selectedJob}
<Input.Search setSelectedJob={setSelectedJob}
placeholder="Search..." importOptionsState={importOptionsState}
onSearch={value => { visible={modalVisible}
console.log(value); onOk={onModalOk}
}} onCancel={onModalCancel}
enterButton />
/> <Table
<Button loading={loading}
onClick={() => { title={() => {
refetch(); return (
}} <div>
> <Input.Search
<Icon type="sync" /> placeholder="Search..."
</Button> onSearch={value => {
<Button console.log(value);
onClick={() => { }}
deleteAllNewJobs() enterButton
.then(r => { />
notification["success"]({ <Button
message: t("jobs.successes.all_deleted", { onClick={() => {
count: r.data.delete_available_jobs.affected_rows refetch();
}) }}
>
<Icon type="sync" />
</Button>
<Button
onClick={() => {
deleteAllNewJobs()
.then(r => {
notification["success"]({
message: t("jobs.successes.all_deleted", {
count: r.data.delete_available_jobs.affected_rows
})
});
refetch();
})
.catch(r => {
notification["error"]({
message: t("jobs.errors.deleted") + " " + r.message
});
}); });
refetch(); }}
}) >
.catch(r => { Delete All
notification["error"]({ </Button>
message: t("jobs.errors.deleted") + " " + r.message </div>
}); );
}); }}
}} size="small"
> pagination={{ position: "top" }}
Delete All columns={columns.map(item => ({ ...item }))}
</Button> rowKey="id"
</div> dataSource={data && data.available_jobs}
); onChange={handleTableChange}
}} />
size="small" </div>
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={data && data.available_jobs}
onChange={handleTableChange}
/>
); );
} }

View File

@@ -1,12 +1,21 @@
import React from "react"; import { notification } from "antd";
import React, { useState } from "react";
import { useMutation, useQuery } from "react-apollo"; import { useMutation, useQuery } from "react-apollo";
import { DELETE_ALL_AVAILABLE_SUPPLEMENT_JOBS, QUERY_AVAILABLE_SUPPLEMENT_JOBS } from "../../graphql/available-jobs.queries"; import { useTranslation } from "react-i18next";
import { withRouter } from "react-router-dom";
import {
DELETE_ALL_AVAILABLE_SUPPLEMENT_JOBS,
QUERY_AVAILABLE_SUPPLEMENT_JOBS
} from "../../graphql/available-jobs.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import JobsAvailableSupplementComponent from "./jobs-available-supplement.component"; import JobsAvailableSupplementComponent from "./jobs-available-supplement.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
export default function JobsAvailableSupplementContainer({ export default withRouter(function JobsAvailableSupplementContainer({
deleteJob, deleteJob,
estDataLazyLoad estDataLazyLoad,
history
}) { }) {
const { loading, error, data, refetch } = useQuery( const { loading, error, data, refetch } = useQuery(
QUERY_AVAILABLE_SUPPLEMENT_JOBS, QUERY_AVAILABLE_SUPPLEMENT_JOBS,
@@ -14,17 +23,107 @@ export default function JobsAvailableSupplementContainer({
fetchPolicy: "network-only" fetchPolicy: "network-only"
} }
); );
const { t } = useTranslation();
const [deleteAllNewJobs] = useMutation(DELETE_ALL_AVAILABLE_SUPPLEMENT_JOBS); const [deleteAllNewJobs] = useMutation(DELETE_ALL_AVAILABLE_SUPPLEMENT_JOBS);
if (error) return <AlertComponent type="error" message={error.message} />; const [modalVisible, setModalVisible] = useState(false);
const [selectedJob, setSelectedJob] = useState(null);
const [insertLoading, setInsertLoading] = useState(false);
const [updateJob] = useMutation(UPDATE_JOB);
const [loadEstData, estData] = estDataLazyLoad;
const importOptionsState = useState({ overrideHeaders: false });
const importOptions = importOptionsState[0];
const onModalOk = () => {
setModalVisible(false);
setInsertLoading(true);
if (
!(
estData.data &&
estData.data.available_jobs_by_pk &&
estData.data.available_jobs_by_pk.est_data
)
) {
//We don't have the right data. Error!
setInsertLoading(false);
notification["error"]({
message: t("jobs.errors.creating", { error: "No job data present." })
});
} else {
//create upsert job
let supp = estData.data.available_jobs_by_pk.est_data;
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.
}
updateJob({
variables: {
jobId: selectedJob,
job: supp
}
})
.then(r => {
notification["success"]({
message: t("jobs.successes.supplemented"),
onClick: () => {
history.push(
`/manage/jobs/${r.data.update_jobs.returning[0].id}`
);
}
});
//Job has been inserted. Clean up the available jobs record.
deleteJob({
variables: { id: estData.data.available_jobs_by_pk.id }
}).then(r => {
refetch();
setInsertLoading(false);
});
})
.catch(r => {
//error while inserting
notification["error"]({
message: t("jobs.errors.creating", { error: r.message })
});
refetch();
setInsertLoading(false);
});
}
};
const onModalCancel = () => {
setModalVisible(false);
setSelectedJob(null);
};
if (error) return <AlertComponent type='error' message={error.message} />;
return ( return (
<JobsAvailableSupplementComponent <LoadingSpinner
loading={loading} loading={insertLoading}
data={data} message={t("jobs.labels.creating_new_job")}>
refetch={refetch} <JobsAvailableSupplementComponent
deleteJob={deleteJob} loading={loading}
deleteAllNewJobs={deleteAllNewJobs} data={data}
estDataLazyLoad={estDataLazyLoad} refetch={refetch}
/> deleteJob={deleteJob}
updateJob={updateJob}
onModalOk={onModalOk}
onModalCancel={onModalCancel}
modalVisible={modalVisible}
setModalVisible={setModalVisible}
selectedJob={selectedJob}
setSelectedJob={setSelectedJob}
deleteAllNewJobs={deleteAllNewJobs}
loadEstData={loadEstData}
estData={estData}
importOptionsState={importOptionsState}
/>
</LoadingSpinner>
); );
} });

View File

@@ -20,7 +20,7 @@ export default function JobsDetailClaims({ job }) {
initialValue: job.loss_desc initialValue: job.loss_desc
})(<Input name='loss_desc' />)} })(<Input name='loss_desc' />)}
</Form.Item> </Form.Item>
TODO: How to handle different taxes and marking them as exempt? TODO How to handle different taxes and marking them as exempt?
{ {
// <Form.Item label={t("jobs.fields.exempt")}> // <Form.Item label={t("jobs.fields.exempt")}>
// {getFieldDecorator("exempt", { // {getFieldDecorator("exempt", {

View File

@@ -0,0 +1,90 @@
import { DatePicker, Form } from "antd";
import moment from "moment";
import React, { useContext } 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();
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>
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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Form, Input, InputNumber } from "antd"; import { Form, Input, InputNumber, Divider } from "antd";
import React, { useContext } from "react"; import React, { useContext } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context"; import JobDetailFormContext from "../../pages/jobs-detail/jobs-detail.page.context";
@@ -25,13 +25,13 @@ export default function JobsDetailFinancials({ job }) {
initialValue: job.depreciation_taxes initialValue: job.depreciation_taxes
})(<InputNumber name="depreciation_taxes" />)} })(<InputNumber name="depreciation_taxes" />)}
</Form.Item> </Form.Item>
TODO: This is equivalent of GST payable. TODO This is equivalent of GST payable.
<Form.Item label={t("jobs.fields.federal_tax_payable")}> <Form.Item label={t("jobs.fields.federal_tax_payable")}>
{getFieldDecorator("federal_tax_payable", { {getFieldDecorator("federal_tax_payable", {
initialValue: job.federal_tax_payable initialValue: job.federal_tax_payable
})(<InputNumber name="federal_tax_payable" />)} })(<InputNumber name="federal_tax_payable" />)}
</Form.Item> </Form.Item>
TODO: equivalent of other customer amount TODO equivalent of other customer amount
<Form.Item label={t("jobs.fields.other_amount_payable")}> <Form.Item label={t("jobs.fields.other_amount_payable")}>
{getFieldDecorator("other_amount_payable", { {getFieldDecorator("other_amount_payable", {
initialValue: job.other_amount_payable initialValue: job.other_amount_payable
@@ -52,6 +52,130 @@ export default function JobsDetailFinancials({ job }) {
initialValue: job.adjustment_bottom_line initialValue: job.adjustment_bottom_line
})(<InputNumber name="adjustment_bottom_line" />)} })(<InputNumber name="adjustment_bottom_line" />)}
</Form.Item> </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>
<Form.Item label={t("jobs.fields.rate_lab")}>
{getFieldDecorator("rate_lab", {
initialValue: job.rate_lab
})(<InputNumber name="rate_lab" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lad")}>
{getFieldDecorator("rate_lad", {
initialValue: job.rate_lad
})(<InputNumber name="rate_lad" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lae")}>
{getFieldDecorator("rate_lae", {
initialValue: job.rate_lae
})(<InputNumber name="rate_lae" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lar")}>
{getFieldDecorator("rate_lar", {
initialValue: job.rate_lar
})(<InputNumber name="rate_lar" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_las")}>
{getFieldDecorator("rate_las", {
initialValue: job.rate_las
})(<InputNumber name="rate_las" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_laf")}>
{getFieldDecorator("rate_laf", {
initialValue: job.rate_laf
})(<InputNumber name="rate_laf" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lam")}>
{getFieldDecorator("rate_lam", {
initialValue: job.rate_lam
})(<InputNumber name="rate_lam" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_lag")}>
{getFieldDecorator("rate_lag", {
initialValue: job.rate_lag
})(<InputNumber name="rate_lag" />)}
</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>
<Form.Item label={t("jobs.fields.rate_lau")}>
{getFieldDecorator("rate_lau", {
initialValue: job.rate_lau
})(<InputNumber name="rate_lau" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la1")}>
{getFieldDecorator("rate_la1", {
initialValue: job.rate_la1
})(<InputNumber name="rate_la1" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la2")}>
{getFieldDecorator("rate_la2", {
initialValue: job.rate_la2
})(<InputNumber name="rate_la2" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la3")}>
{getFieldDecorator("rate_la3", {
initialValue: job.rate_la3
})(<InputNumber name="rate_la3" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_la4")}>
{getFieldDecorator("rate_la4", {
initialValue: job.rate_la4
})(<InputNumber name="rate_la4" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mapa")}>
{getFieldDecorator("rate_mapa", {
initialValue: job.rate_mapa
})(<InputNumber name="rate_mapa" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mash")}>
{getFieldDecorator("rate_mash", {
initialValue: job.rate_mash
})(<InputNumber name="rate_mash" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mahw")}>
{getFieldDecorator("rate_mahw", {
initialValue: job.rate_mahw
})(<InputNumber name="rate_mahw" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_ma2s")}>
{getFieldDecorator("rate_ma2s", {
initialValue: job.rate_ma2s
})(<InputNumber name="rate_ma2s" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_ma3s")}>
{getFieldDecorator("rate_ma3s", {
initialValue: job.rate_ma3s
})(<InputNumber name="rate_ma3s" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_mabl")}>
{getFieldDecorator("rate_mabl", {
initialValue: job.rate_mabl
})(<InputNumber name="rate_mabl" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_macs")}>
{getFieldDecorator("rate_macs", {
initialValue: job.rate_macs
})(<InputNumber name="rate_macs" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_matd")}>
{getFieldDecorator("rate_matd", {
initialValue: job.rate_matd
})(<InputNumber name="rate_matd" />)}
</Form.Item>
<Form.Item label={t("jobs.fields.rate_laa")}>
{getFieldDecorator("rate_laa", {
initialValue: job.rate_laa
})(<InputNumber name="rate_laa" />)}
</Form.Item>
</div> </div>
); );
} }

View File

@@ -1,8 +1,12 @@
import { import {
Avatar, Avatar,
Badge,
Button, Button,
Checkbox, Checkbox,
Descriptions, Descriptions,
Dropdown,
Icon,
Menu,
notification, notification,
PageHeader, PageHeader,
Tag Tag
@@ -10,57 +14,71 @@ import {
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Moment from "react-moment"; import Moment from "react-moment";
import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import CarImage from "../../assets/car.svg"; import CarImage from "../../assets/car.svg";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import BarcodePopup from "../barcode-popup/barcode-popup.component";
export default function JobsDetailHeader({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function JobsDetailHeader({
job, job,
mutationConvertJob, mutationConvertJob,
refetch, refetch,
getFieldDecorator handleSubmit,
scheduleModalState,
bodyshop,
updateJobStatus
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const setscheduleModalVisible = scheduleModalState[1];
const tombstoneTitle = ( const tombstoneTitle = (
<div> <div>
<Avatar size="large" alt="Vehicle Image" src={CarImage} /> <Avatar size='large' alt='Vehicle Image' src={CarImage} />
{`${t("jobs.fields.ro_number")} ${ {`${t("jobs.fields.ro_number")} ${
job.ro_number ? job.ro_number : t("general.labels.na") job.ro_number ? job.ro_number : t("general.labels.na")
}`} }`}
</div> </div>
); );
const tombstoneSubtitle = ( const statusmenu = (
<div> <Menu
<Tag color="red"> onClick={e => {
{job.owner ? ( updateJobStatus(e.key);
<Link to={`/manage/owners/${job.owner.id}`}> }}>
{`${job.ownr_co_nm || ""}${job.ownr_fn || ""} ${job.ownr_ln || ""}`} {bodyshop.md_ro_statuses.statuses.map(item => (
</Link> <Menu.Item key={item}>{item}</Menu.Item>
) : ( ))}
t("jobs.errors.noowner") </Menu>
)}
</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>
</div>
); );
const menuExtra = [ const menuExtra = [
<Dropdown overlay={statusmenu} key='changestatus'>
<Button>
{t("jobs.actions.changestatus")} <Icon type='down' />
</Button>
</Dropdown>,
<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 <Button
key="convert" key='convert'
type="dashed" type='dashed'
disabled={job.converted} disabled={job.converted}
onClick={() => { onClick={() => {
mutationConvertJob({ mutationConvertJob({
@@ -72,11 +90,14 @@ export default function JobsDetailHeader({
message: t("jobs.successes.converted") message: t("jobs.successes.converted")
}); });
}); });
}} }}>
>
{t("jobs.actions.convert")} {t("jobs.actions.convert")}
</Button>, </Button>,
<Button type="primary" key="submit" htmlType="submit"> <Button
type='primary'
key='submit'
htmlType='button'
onClick={handleSubmit}>
{t("general.labels.save")} {t("general.labels.save")}
</Button> </Button>
]; ];
@@ -87,19 +108,38 @@ export default function JobsDetailHeader({
border: "1px solid rgb(235, 237, 240)" border: "1px solid rgb(235, 237, 240)"
}} }}
title={tombstoneTitle} title={tombstoneTitle}
subTitle={tombstoneSubtitle} //subTitle={tombstoneSubtitle}
tags={ tags={
<span key="job-status"> <span key='job-status'>
{job.job_status ? ( {job.status ? <Tag color='blue'>{job.status}</Tag> : null}
<Tag color="blue">{job.job_status.name}</Tag> <Tag color='red'>
) : null} {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>
<BarcodePopup value={job.id} />
</span> </span>
} }
extra={menuExtra} extra={menuExtra}>
> <Descriptions size='small' column={5}>
<Descriptions size="small" column={5}>
<Descriptions.Item label={t("jobs.fields.repairtotal")}> <Descriptions.Item label={t("jobs.fields.repairtotal")}>
<CurrencyFormatter>{job.claim_total}</CurrencyFormatter> <CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.customerowing")}> <Descriptions.Item label={t("jobs.fields.customerowing")}>
@@ -112,7 +152,7 @@ export default function JobsDetailHeader({
<Descriptions.Item label={t("jobs.fields.scheduled_completion")}> <Descriptions.Item label={t("jobs.fields.scheduled_completion")}>
{job.scheduled_completion ? ( {job.scheduled_completion ? (
<Moment format="MM/DD/YYYY">{job.scheduled_completion}</Moment> <Moment format='MM/DD/YYYY'>{job.scheduled_completion}</Moment>
) : null} ) : null}
</Descriptions.Item> </Descriptions.Item>
@@ -122,4 +162,4 @@ export default function JobsDetailHeader({
</Descriptions> </Descriptions>
</PageHeader> </PageHeader>
); );
} });

View File

@@ -11,8 +11,6 @@ export default function JobsDetailInsurance({ job }) {
const { getFieldDecorator, getFieldValue } = form; const { getFieldDecorator, getFieldValue } = form;
const { t } = useTranslation(); const { t } = useTranslation();
console.log("job.loss_date", job.loss_date);
return ( return (
<div> <div>
<Form.Item label={t("jobs.fields.ins_co_id")}> <Form.Item label={t("jobs.fields.ins_co_id")}>

View File

@@ -0,0 +1,41 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
toggleModalVisible,
setModalContext
} from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter")),
setInvoiceEnterContext: context =>
dispatch(setModalContext({ context: context, modal: "invoiceEnter" }))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function JobsDetailPliComponent({
toggleModalVisible,
setInvoiceEnterContext,
job
}) {
return (
<div>
<div
onClick={() => {
setInvoiceEnterContext({
actions: { refetch: null },
context: {
job
}
});
}}
>
Enter Invoice
</div>
</div>
);
});

View File

@@ -0,0 +1,7 @@
import React from "react";
import JobsDetailPliComponent from "./jobs-detail-pli.component";
export default function JobsDetailPliContainer({ job }) {
console.log("job", job);
return <JobsDetailPliComponent job={job} />;
}

View File

@@ -1,13 +1,22 @@
import React from "react"; import React from "react";
import { useQuery } from "react-apollo"; import { useQuery } from "react-apollo";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_SHOP_ID } from "../../graphql/bodyshop.queries"; import { QUERY_SHOP_ID } from "../../graphql/bodyshop.queries";
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries"; import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobDocuments from "./jobs-documents.component"; import JobDocuments from "./jobs-documents.component";
import { GET_CURRENT_USER } from "../../graphql/local.queries";
export default function JobsDocumentsContainer({ jobId }) { const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
export default connect(
mapStateToProps,
null
)(function JobsDocumentsContainer({ jobId, currentUser }) {
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, { const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
variables: { jobId: jobId }, variables: { jobId: jobId },
fetchPolicy: "network-only" fetchPolicy: "network-only"
@@ -17,14 +26,12 @@ export default function JobsDocumentsContainer({ jobId }) {
fetchPolicy: "network-only" fetchPolicy: "network-only"
}); });
const user = useQuery(GET_CURRENT_USER); if (loading || shopData.loading) return <LoadingSpinner />;
if (error || shopData.error)
if (loading || shopData.loading || user.loading) return <LoadingSpinner />;
if (error || shopData.error || user.error)
return ( return (
<AlertComponent <AlertComponent
type='error' type="error"
message={error.message || shopData.error.message || user.error.message} message={error.message || shopData.error.message}
/> />
); );
@@ -32,12 +39,10 @@ export default function JobsDocumentsContainer({ jobId }) {
<JobDocuments <JobDocuments
data={data.documents} data={data.documents}
jobId={jobId} jobId={jobId}
currentUser={user.data.currentUser} currentUser={currentUser}
shopId={ shopId={
shopData.data?.bodyshops[0]?.id shopData.data.bodyshops[0].id ? shopData.data.bodyshops[0].id : "error"
? shopData.data?.bodyshops[0]?.id
: "error"
} }
/> />
); );
} });

View File

@@ -0,0 +1,172 @@
import { Checkbox, Divider, Table } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter";
export default function JobsFindModalComponent({
selectedJob,
setSelectedJob,
jobsList,
jobsListLoading,
importOptionsState
}) {
const { t } = useTranslation();
const [importOptions, setImportOptions] = importOptionsState;
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
width: "8%",
render: (text, record) => (
<span>
<Link to={"/manage/jobs/" + record.id}>
{record.ro_number ? record.ro_number : "EST-" + record.est_number}
</Link>
</span>
)
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
width: "25%",
render: (text, record) => {
return record.owner ? (
<Link to={"/manage/owners/" + record.owner.id}>
{record.ownr_fn} {record.ownr_ln}
</Link>
) : (
// t("jobs.errors.noowner")
<span>{`${record.ownr_fn} ${record.ownr_ln}`}</span>
);
}
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
width: "12%",
ellipsis: true,
render: (text, record) => {
return record.ownr_ph1 ? (
<PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
) : (
t("general.labels.unknown")
);
}
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
width: "10%",
ellipsis: true,
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.vehicle ? (
<Link to={"/manage/vehicles/" + record.vehicle.id}>
{record.vehicle.v_model_yr} {record.vehicle.v_make_desc}{" "}
{record.vehicle.v_model_desc}
</Link>
) : (
t("jobs.errors.novehicle")
);
}
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
width: "8%",
ellipsis: true,
render: (text, record) => {
return record.vehicle.plate_no ? (
<span>{record.vehicle.plate_no}</span>
) : (
t("general.labels.unknown")
);
}
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
width: "12%",
ellipsis: true,
render: (text, record) => {
return record.clm_no ? (
<span>{record.clm_no}</span>
) : (
t("general.labels.unknown")
);
}
}
];
const handleOnRowClick = record => {
if (record) {
if (record.id) {
setSelectedJob(record.id);
return;
}
}
setSelectedJob(null);
};
return (
<div>
<Table
title={() => t("jobs.labels.existing_jobs")}
size="small"
pagination={{ position: "bottom" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
loading={jobsListLoading}
dataSource={jobsList}
rowSelection={{
onSelect: props => {
setSelectedJob(props.id);
},
type: "radio",
selectedRowKeys: [selectedJob]
}}
onRow={(record, rowIndex) => {
return {
onClick: event => {
handleOnRowClick(record);
}
};
}}
/>
<Divider />
<Checkbox
defaultChecked={importOptions.overrideHeader}
onChange={e =>
setImportOptions({
...importOptions,
overrideHeader: e.target.checked
})
}
>
{t("jobs.labels.override_header")}
</Checkbox>
</div>
);
}

View File

@@ -0,0 +1,58 @@
import { Modal } from "antd";
import React from "react";
import { useQuery } from "react-apollo";
import { useTranslation } from "react-i18next";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobsFindModalComponent from "./jobs-find-modal.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function JobsFindModalContainer({
bodyshop,
loading,
error,
selectedJob,
setSelectedJob,
importOptionsState,
...modalProps
}) {
const { t } = useTranslation();
const jobsList = useQuery(QUERY_ALL_ACTIVE_JOBS, {
fetchPolicy: "network-only",
variables: {
statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"]
}
});
return (
<Modal
title={t("jobs.labels.existing_jobs")}
width={"80%"}
okButtonProps={{ disabled: selectedJob ? false : true }}
{...modalProps}>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type='error' /> : null}
{true ? (
<JobsFindModalComponent
selectedJob={selectedJob}
setSelectedJob={setSelectedJob}
importOptionsState={importOptionsState}
jobsListLoading={jobsList.loading}
jobsList={
jobsList.data && jobsList.data.jobs ? jobsList.data.jobs : null
}
/>
) : null}
</Modal>
);
});

View File

@@ -1,12 +1,15 @@
import { Input, Table, Icon } from "antd"; import { Button, Icon, Input, Table } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link, withRouter } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import { withRouter } from "react-router-dom"; import StartChatButton from "../chat-open-button/chat-open-button.component";
export default withRouter(function JobsList({ export default withRouter(function JobsList({
searchTextState,
refetch,
loading, loading,
jobs, jobs,
selectedJob, selectedJob,
@@ -20,6 +23,7 @@ export default withRouter(function JobsList({
const { t } = useTranslation(); const { t } = useTranslation();
const setSearchText = searchTextState[1];
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
@@ -28,7 +32,11 @@ export default withRouter(function JobsList({
width: "8%", width: "8%",
// onFilter: (value, record) => record.ro_number.includes(value), // onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null, // filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a, b), 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: sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
@@ -56,14 +64,12 @@ export default withRouter(function JobsList({
</Link> </Link>
) : ( ) : (
// t("jobs.errors.noowner") // t("jobs.errors.noowner")
<span> <span>{`${record.ownr_fn} ${record.ownr_ln}`}</span>
{record.ownr_fn} {record.ownr_ln}
</span>
); );
} }
}, },
{ {
title: t("jobs.fields.phone1"), title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1", dataIndex: "ownr_ph1",
key: "ownr_ph1", key: "ownr_ph1",
width: "12%", width: "12%",
@@ -72,13 +78,7 @@ export default withRouter(function JobsList({
return record.ownr_ph1 ? ( return record.ownr_ph1 ? (
<span> <span>
<PhoneFormatter>{record.ownr_ph1}</PhoneFormatter> <PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
<Icon <StartChatButton phone={record.ownr_ph1} />
style={{ margin: 4 }}
type='message'
onClick={() => {
alert("SMSing will happen here.");
}}
/>
</span> </span>
) : ( ) : (
t("general.labels.unknown") t("general.labels.unknown")
@@ -91,11 +91,11 @@ export default withRouter(function JobsList({
key: "status", key: "status",
width: "10%", width: "10%",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a, b), sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.job_status?.name || t("general.labels.na"); return record.status || t("general.labels.na");
} }
}, },
@@ -107,7 +107,7 @@ export default withRouter(function JobsList({
ellipsis: true, ellipsis: true,
render: (text, record) => { render: (text, record) => {
return record.vehicle ? ( return record.vehicle ? (
<Link to={"manage/vehicles/" + record.vehicle.id}> <Link to={"/manage/vehicles/" + record.vehicle.id}>
{record.vehicle.v_model_yr} {record.vehicle.v_make_desc}{" "} {record.vehicle.v_model_yr} {record.vehicle.v_make_desc}{" "}
{record.vehicle.v_model_desc} {record.vehicle.v_model_desc}
</Link> </Link>
@@ -122,11 +122,11 @@ export default withRouter(function JobsList({
key: "plate_no", key: "plate_no",
width: "8%", width: "8%",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a, b), sorter: (a, b) => alphaSort(a.vehicle.plate_no, b.vehicle.plate_no),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order, state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicle?.plate_no ? ( return record.vehicle.plate_no ? (
<span>{record.vehicle.plate_no}</span> <span>{record.vehicle.plate_no}</span>
) : ( ) : (
t("general.labels.unknown") t("general.labels.unknown")
@@ -139,7 +139,7 @@ export default withRouter(function JobsList({
key: "clm_no", key: "clm_no",
width: "12%", width: "12%",
ellipsis: true, ellipsis: true,
sorter: (a, b) => alphaSort(a, b), sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -154,15 +154,13 @@ export default withRouter(function JobsList({
title: t("jobs.fields.clm_total"), title: t("jobs.fields.clm_total"),
dataIndex: "clm_total", dataIndex: "clm_total",
key: "clm_total", key: "clm_total",
width: "8%", width: "10%",
// sorter: (a, b) => { sorter: (a, b) => a.clm_total - b.clm_total,
// return a > b; sortOrder:
// }, state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
// sortOrder:
// state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.clm_total ? ( return record.clm_total ? (
<span>{record.clm_total}</span> <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
) : ( ) : (
t("general.labels.unknown") t("general.labels.unknown")
); );
@@ -196,7 +194,6 @@ export default withRouter(function JobsList({
if (record) { if (record) {
if (record.id) { if (record.id) {
setSelectedJob(record.id); setSelectedJob(record.id);
history.push(`#${record.id}`);
return; return;
} }
} }
@@ -209,19 +206,24 @@ export default withRouter(function JobsList({
loading={loading} loading={loading}
title={() => { title={() => {
return ( return (
<Input.Search <div style={{ display: "flex" }}>
placeholder='Search...' <Button onClick={() => refetch()}>
onSearch={value => { <Icon type="sync" />
console.log(value); </Button>
}} <Input.Search
enterButton placeholder="Search..."
/> onChange={e => {
setSearchText(e.target.value);
}}
enterButton
/>
</div>
); );
}} }}
size='small' size="small"
pagination={{ position: "top" }} pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))} columns={columns.map(item => ({ ...item }))}
rowKey='id' rowKey="id"
dataSource={jobs} dataSource={jobs}
rowSelection={{ selectedRowKeys: [selectedJob] }} rowSelection={{ selectedRowKeys: [selectedJob] }}
onChange={handleTableChange} onChange={handleTableChange}

View File

@@ -4,5 +4,5 @@ import "./loading-skeleton.styles.scss";
import { Skeleton } from "antd"; import { Skeleton } from "antd";
export default function LoadingSkeleton(props) { export default function LoadingSkeleton(props) {
return <Skeleton {...props} className='loading-skeleton' active />; return <Skeleton {...props} className="loading-skeleton" active />;
} }

View File

@@ -8,7 +8,11 @@ export default function LoadingSpinner({ loading = true, message, ...props }) {
spinning={loading} spinning={loading}
className="loading-spinner" className="loading-spinner"
size="large" size="large"
//delay="500" style={{
position: "relative",
alignContent: "center"
}}
delay={200}
tip={message ? message : null} tip={message ? message : null}
> >
{props.children} {props.children}

View File

@@ -1,3 +1,2 @@
.loading-spinner { .loading-spinner {
text-align: center;
} }

View File

@@ -1,34 +1,27 @@
import React from "react";
import { useQuery } from "react-apollo";
import { Link } from "react-router-dom";
import { GET_CURRENT_USER } from "../../graphql/local.queries";
import { Icon } from "antd"; import { Icon } from "antd";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors";
export default function ManageSignInButton() { const mapStateToProps = createStructuredSelector({
const { currentUser: selectCurrentUser
loading, });
error,
data: { currentUser }
} = useQuery(GET_CURRENT_USER);
if (loading) return <LoadingSpinner />; export default connect(
if (error) return error.message; mapStateToProps,
null
return currentUser ? ( )(function ManageSignInButton({ currentUser }) {
<div> return currentUser.authorized ? (
{" "} <Link to="/manage">
<Link to="/manage"> <Icon type="build" />
<Icon type="build" /> Manage
Manage </Link>
</Link>
</div>
) : ( ) : (
<div> <Link to="/signin">
<Link to="/signin"> <Icon type="login" />
<Icon type="login" /> Sign In
Sign In </Link>
</Link>
</div>
); );
} });

View File

@@ -14,7 +14,7 @@ export default function NoteUpsertModalComponent({
return ( return (
<Modal <Modal
title={noteState?.id ? t("notes.actions.edit") : t("notes.actions.new")} title={noteState.id ? t("notes.actions.edit") : t("notes.actions.new")}
visible={visible} visible={visible}
okText={t("general.labels.save")} okText={t("general.labels.save")}
onOk={() => { onOk={() => {
@@ -22,7 +22,8 @@ export default function NoteUpsertModalComponent({
}} }}
onCancel={() => { onCancel={() => {
changeVisibility(false); changeVisibility(false);
}}> }}
>
<div> <div>
{t("notes.fields.critical")} {t("notes.fields.critical")}
<Switch <Switch

View File

@@ -5,12 +5,20 @@ import { useTranslation } from "react-i18next";
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries"; import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
import NoteUpsertModalComponent from "./note-upsert-modal.component"; import NoteUpsertModalComponent from "./note-upsert-modal.component";
export default function NoteUpsertModalContainer({
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
export default connect (mapStateToProps,null)( function NoteUpsertModalContainer({
jobId, jobId,
visible, visible,
changeVisibility, changeVisibility,
refetch, refetch,
existingNote existingNote,currentUser
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [insertNote] = useMutation(INSERT_NEW_NOTE); const [insertNote] = useMutation(INSERT_NEW_NOTE);
@@ -33,7 +41,7 @@ export default function NoteUpsertModalContainer({
insertNote({ insertNote({
variables: { variables: {
noteInput: [ noteInput: [
{ ...noteState, jobid: jobId, created_by: "patrick@bodyshop.app" } //TODO: Fix the created by. { ...noteState, jobid: jobId, created_by: currentUser.email }
] ]
} }
}).then(r => { }).then(r => {
@@ -73,3 +81,4 @@ export default function NoteUpsertModalContainer({
/> />
); );
} }
);

View File

@@ -0,0 +1,106 @@
import { Button, Col, Form, Input, Row, Switch } 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";
import ResetForm from "../form-items-formatted/reset-form-item.component";
export default function OwnerDetailFormComponent({ form, owner }) {
const { t } = useTranslation();
const {
isFieldsTouched,
resetFields,
getFieldDecorator,
getFieldValue
} = form;
return (
<div>
{isFieldsTouched() ? <ResetForm resetFields={resetFields} /> : null}
<Button type="primary" key="submit" htmlType="submit">
{t("general.labels.save")}
</Button>
<Row>
<Col span={8}>
<Form.Item label={t("owners.fields.ownr_ln")}>
{getFieldDecorator("ownr_ln", {
initialValue: owner.ownr_ln
})(<Input name="ownr_ln" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_fn")}>
{getFieldDecorator("ownr_fn", {
initialValue: owner.ownr_fn
})(<Input name="ownr_fn" />)}
</Form.Item>
<Form.Item label={t("owners.fields.allow_text_message")}>
{getFieldDecorator("allow_text_message", {
initialValue: owner.allow_text_message,
valuePropName: "checked"
})(<Switch name="allow_text_message" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_addr1")}>
{getFieldDecorator("ownr_addr1", {
initialValue: owner.ownr_addr1
})(<Input name="ownr_addr1" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_addr2")}>
{getFieldDecorator("ownr_addr2", {
initialValue: owner.ownr_addr2
})(<Input name="ownr_addr2" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_city")}>
{getFieldDecorator("ownr_city", {
initialValue: owner.ownr_city
})(<Input name="ownr_city" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_ctry")}>
{getFieldDecorator("ownr_ctry", {
initialValue: owner.ownr_ctry
})(<Input name="ownr_ctry" />)}
</Form.Item>
</Col>
<Col span={8}>
{" "}
<Form.Item label={t("owners.fields.ownr_ea")}>
{getFieldDecorator("ownr_ea", {
initialValue: owner.ownr_ea,
rules: [
{
type: "email",
message: "This is not a valid email address."
}
]
})(
<FormItemEmail name="ownr_ea" email={getFieldValue("ownr_ea")} />
)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_ph1")}>
{getFieldDecorator("ownr_ph1", {
initialValue: owner.ownr_ph1
})(<FormItemPhone customInput={Input} name="ownr_ph1" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_st")}>
{getFieldDecorator("ownr_st", {
initialValue: owner.ownr_st
})(<Input name="ownr_st" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_zip")}>
{getFieldDecorator("ownr_zip", {
initialValue: owner.ownr_zip
})(<Input name="ownr_zip" />)}
</Form.Item>
<Form.Item label={t("owners.fields.preferred_contact")}>
{getFieldDecorator("preferred_contact", {
initialValue: owner.preferred_contact
})(<Input name="preferred_contact" />)}
</Form.Item>
<Form.Item label={t("owners.fields.ownr_title")}>
{getFieldDecorator("ownr_title", {
initialValue: owner.ownr_title
})(<Input name="ownr_title" />)}
</Form.Item>
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import { Form, notification } from "antd";
import React from "react";
import { useMutation } from "react-apollo";
import { useTranslation } from "react-i18next";
import { UPDATE_OWNER } from "../../graphql/owners.queries";
import OwnerDetailFormComponent from "./owner-detail-form.component";
function OwnerDetailFormContainer({ form, owner, refetch }) {
const { t } = useTranslation();
const [updateOwner] = useMutation(UPDATE_OWNER);
const handleSubmit = e => {
e.preventDefault();
form.validateFieldsAndScroll((err, values) => {
if (err) {
notification["error"]({
message: t("owners.errors.validationtitle"),
description: t("owners.errors.validation")
});
}
if (!err) {
updateOwner({
variables: { ownerId: owner.id, owner: values }
}).then(r => {
notification["success"]({
message: t("owners.successes.save")
});
//TODO Better way to reset the field decorators?
if (refetch) refetch().then();
form.resetFields();
});
}
});
};
return (
<Form onSubmit={handleSubmit} autoComplete="off">
<OwnerDetailFormComponent form={form} owner={owner} />
</Form>
);
}
export default Form.create({ name: "OwnerDetailFormContainer" })(
OwnerDetailFormContainer
);

View File

@@ -0,0 +1,59 @@
import React from "react";
import { Table } from "antd";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
export default function OwnerDetailJobsComponent({ owner }) {
const { t } = useTranslation();
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
ellipsis: true,
render: (text, record) => (
<Link to={`/manage/jobs/${record.id}`}>
{record.ro_number ? record.ro_number : `EST ${record.est_number}`}
</Link>
)
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "owner",
key: "owner",
render: (text, record) => (
<Link to={`/manage/vehicles/${record.vehicle.id}`}>
{`${record.vehicle.v_model_yr} ${record.vehicle.v_make_desc} ${record.vehicle.v_model_desc}`}
</Link>
)
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no"
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status"
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
)
}
];
return (
<Table
pagination={{ position: "bottom" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={owner.jobs}
/>
);
}

View File

@@ -118,7 +118,8 @@ export default function OwnerFindModalComponent({
checked={selectedOwner ? false : true} checked={selectedOwner ? false : true}
onClick={() => setSelectedOwner(null)} onClick={() => setSelectedOwner(null)}
> >
Create a new Owner record for this job.
{t("owners.labels.create_new")}
</Checkbox> </Checkbox>
</div> </div>
); );

View File

@@ -20,9 +20,7 @@ export default function OwnerFindModalContainer({
const ownersList = useQuery(QUERY_SEARCH_OWNER_BY_IDX, { const ownersList = useQuery(QUERY_SEARCH_OWNER_BY_IDX, {
variables: { variables: {
search: owner search: owner ? `${owner.ownr_fn || ""} ${owner.ownr_ln || ""}` : null
? `${owner.ownr_fn} ${owner.ownr_ln} ${owner.ownr_addr1} ${owner.ownr_city} ${owner.ownr_zip} ${owner.ownr_ea} ${owner.ownr_ph1} ${owner.ownr_ph2}`
: null
}, },
skip: !owner, skip: !owner,
fetchPolicy: "network-only" fetchPolicy: "network-only"
@@ -32,18 +30,17 @@ export default function OwnerFindModalContainer({
<Modal <Modal
title={t("owners.labels.existing_owners")} title={t("owners.labels.existing_owners")}
width={"80%"} width={"80%"}
{...modalProps} {...modalProps}>
>
{loading ? <LoadingSpinner /> : null} {loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null} {error ? <AlertComponent message={error.message} type='error' /> : null}
{owner ? ( {owner ? (
<OwnerFindModalComponent <OwnerFindModalComponent
selectedOwner={selectedOwner} selectedOwner={selectedOwner}
setSelectedOwner={setSelectedOwner} setSelectedOwner={setSelectedOwner}
ownersListLoading={ownersList.loading} ownersListLoading={ownersList.loading}
ownersList={ ownersList={
ownersList.data && ownersList.data.search_owners ownersList.data && ownersList.data.search_owner
? ownersList.data.search_owners ? ownersList.data.search_owner
: null : null
} }
/> />

View File

@@ -0,0 +1,89 @@
import { Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { alphaSort } from "../../utils/sorters";
export default function OwnersListComponent({ loading, owners, refetch }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" }
});
const { t } = useTranslation();
const columns = [
{
title: t("owners.fields.name"),
dataIndex: "name",
key: "name",
// onFilter: (value, record) => record.ro_number.includes(value),
// filteredValue: state.filteredInfo.text || null,
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "name" && state.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/owners/" + record.id}>
{`${record.ownr_fn} ${record.ownr_ln}`}
</Link>
)
},
{
title: t("owners.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
sorter: (a, b) => alphaSort(a.ownr_ph1, b.ownr_ph1),
sortOrder:
state.sortedInfo.columnKey === "ownr_ph1" && state.sortedInfo.order,
render: (text, record) => {
return <PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>;
}
},
{
title: t("owners.fields.ownr_ea"),
dataIndex: "ownr_ea",
key: "ownr_ea"
},
{
title: t("owners.fields.address"),
dataIndex: "address",
key: "address",
render: (text, record) => {
return (
<div>{`${record.ownr_addr1 || ""} ${record.ownr_addr2 ||
""} ${record.ownr_city || ""} ${record.ownr_st ||
""} ${record.ownr_zip || ""}`}</div>
);
}
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
//TODO Implement searching & pagination
return (
<Table
loading={loading}
title={() => {
return (
<Input.Search
placeholder="Search..."
onSearch={value => {
console.log(value);
}}
enterButton
/>
);
}}
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={owners}
onChange={handleTableChange}
/>
);
}

View File

@@ -0,0 +1,20 @@
import React from "react";
import { useQuery } from "react-apollo";
import { QUERY_ALL_OWNERS } from "../../graphql/owners.queries";
import AlertComponent from "../alert/alert.component";
import OwnersListComponent from "./owners-list.component";
export default function OwnersListContainer() {
const { loading, error, data, refetch } = useQuery(QUERY_ALL_OWNERS, {
fetchPolicy: "network-only"
});
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<OwnersListComponent
loading={loading}
owners={data ? data.owners : null}
refetch={refetch}
/>
);
}

View File

@@ -0,0 +1,90 @@
import { AutoComplete, DatePicker, Icon, Input, List, Radio } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
export default function PartsOrderModalComponent({
vendorList,
state,
sendTypeState,
orderLinesState
}) {
const [partsOrder, setPartsOrder] = state;
const [sendType, setSendType] = sendTypeState;
const orderLines = orderLinesState[0];
const [vendorComplete, setVendorComplete] = useState(vendorList);
const { t } = useTranslation();
const handleSearch = value => {
if (value === "") setVendorComplete(vendorList);
else
setVendorComplete(
vendorList.filter(v =>
v.name.toLowerCase().includes(value.toLowerCase())
)
);
};
const handleSelect = (value, option) => {
setPartsOrder({ ...partsOrder, vendorid: option.key });
};
return (
<div>
<AutoComplete
onSearch={handleSearch}
onSelect={handleSelect}
defaultOpen
backfill
optionLabelProp='value'
dataSource={vendorComplete}
placeholder={t("vendors.labels.search")}>
{vendorComplete.map(v => (
<AutoComplete.Option value={v.name} key={v.id}>
<div>{v.name}</div>
<div> {v.favorite ? <Icon type='heart' /> : null}</div>
</AutoComplete.Option>
))}
</AutoComplete>
{t("parts_orders.fields.deliver_by")}
<DatePicker
defaultValue={partsOrder.deliver_by}
onChange={e => {
setPartsOrder({ ...partsOrder, deliver_by: e });
}}
/>
{t("parts_orders.labels.inthisorder")}
<List
itemLayout='horizontal'
dataSource={orderLines}
renderItem={item => (
<List.Item
actions={[
<Input placeholder={t("parts_orders.fields.lineremarks")} />
//TODO Editable table/adding line remarks to the order.
]}>
{
// <List.Item.Meta
// avatar={
// <Avatar src='https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png' />
// }
// title={<a href='https://ant.design'>{item.name.last}</a>}
// description='Ant Design, a design language for background applications, is refined by Ant UED Team'
// />
}
<div>{`${item.line_desc}${
item.oem_partno ? " | " + item.oem_partno : ""
}`}</div>
</List.Item>
)}
/>
<Radio.Group
defaultValue={sendType}
onChange={e => setSendType(e.target.value)}>
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
</Radio.Group>
</div>
);
}

View File

@@ -0,0 +1,188 @@
import { Modal, notification } from "antd";
import React, { useState, useEffect } from "react";
import { useMutation, useQuery } from "react-apollo";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_PARTS_ORDERS } from "../../graphql/parts-orders.queries";
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
import { QUERY_ALL_VENDORS_FOR_ORDER } from "../../graphql/vendors.queries";
import {
selectBodyshop,
selectCurrentUser
} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsOrderModalComponent from "./parts-order-modal.component";
import {
setEmailOptions,
toggleEmailOverlayVisible
} from "../../redux/email/email.actions";
import PartsOrderEmailTemplate from "../../emails/parts-order/parts-order.email";
import { REPORT_QUERY_PARTS_ORDER_BY_PK } from "../../emails/parts-order/parts-order.query";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop
});
const mapDispatchToProps = dispatch => ({
setEmailOptions: e => dispatch(setEmailOptions(e)),
toggleEmailOverlayVisible: () => dispatch(toggleEmailOverlayVisible())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function PartsOrderModalContainer({
partsOrderModalVisible,
linesToOrder,
jobId,
currentUser,
bodyshop,
refetch,
setEmailOptions,
toggleEmailOverlayVisible
}) {
const { t } = useTranslation();
const [modalVisible, setModalVisible] = partsOrderModalVisible;
//set order lines to be a version of the incoming lines.
const orderLinesState = useState(
linesToOrder.reduce((acc, value) => {
acc.push({
line_desc: value.line_desc,
oem_partno: value.oem_partno,
db_price: value.db_price,
act_price: value.act_price,
line_remarks: "Alalala",
job_line_id: value.id,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*"
});
return acc;
}, [])
);
const [orderLines, setOrderLinesState] = orderLinesState;
useEffect(() => {
if (modalVisible)
setOrderLinesState(
linesToOrder.reduce((acc, value) => {
acc.push({
line_desc: value.line_desc,
oem_partno: value.oem_partno,
db_price: value.db_price,
act_price: value.act_price,
line_remarks: "Alalala",
job_line_id: value.id,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*"
});
return acc;
}, [])
);
}, [
modalVisible,
setOrderLinesState,
linesToOrder,
bodyshop.md_order_statuses.default_ordered
]);
const sendTypeState = useState("e");
const sendType = sendTypeState[0];
const partsOrderState = useState({
vendorid: null,
jobid: jobId,
user_email: currentUser.email
});
const partsOrder = partsOrderState[0];
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
fetchPolicy: "network-only",
skip: !modalVisible
});
const [insertPartOrder] = useMutation(INSERT_NEW_PARTS_ORDERS);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
const handleOk = () => {
insertPartOrder({
variables: {
po: [
{
...partsOrder,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*",
parts_order_lines: {
data: orderLines
}
}
]
}
})
.then(r => {
updateJobLines({
variables: {
ids: linesToOrder.map(item => item.id),
status: bodyshop.md_order_statuses.default_ordered || "Ordered*"
}
})
.then(response => {
notification["success"]({
message: t("parts_orders.successes.created")
});
if (refetch) refetch();
setModalVisible(false);
if (sendType === "e") {
//Show the email modal and set the data.
//TODO Remove some of the options below.
setEmailOptions({
messageOptions: {
from: {
name: "Kavia Autobdoy",
address: "noreply@bodyshop.app"
},
to: "patrickwf@gmail.com",
replyTo: "snaptsoft@gmail.com"
},
template: PartsOrderEmailTemplate,
queryConfig: [
REPORT_QUERY_PARTS_ORDER_BY_PK,
{
variables: {
id: r.data.insert_parts_orders.returning[0].id
}
}
]
});
toggleEmailOverlayVisible();
}
})
.catch(error => {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: error.message
});
});
})
.catch(error => {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: error.message
});
});
};
return (
<Modal
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleOk}>
{error ? <AlertComponent message={error.message} type='error' /> : null}
<LoadingSpinner loading={loading}>
<PartsOrderModalComponent
vendorList={(data && data.vendors) || []}
state={partsOrderState}
sendTypeState={sendTypeState}
orderLinesState={orderLinesState}
/>
</LoadingSpinner>
</Modal>
);
});

View File

@@ -1,15 +1,17 @@
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import ProfileMyComponent from "../profile-my/profile-my.component";
import ProfileShopsContainer from "../profile-shops/profile-shops.container";
export default function ProfileContent({ sidebarSelection }) { export default function ProfileContent({ sidebarSelection }) {
const { t } = useTranslation(); const { t } = useTranslation();
switch (sidebarSelection.key) { switch (sidebarSelection.key) {
case "profile": case "profile":
return <div>Profile stuff</div>; return <ProfileMyComponent />;
case "shop": case "shops":
return <div>Shop stuff</div>; return <ProfileShopsContainer />;
default: default:
return ( return (
<AlertComponent message={t("profile.errors.state")} type="error" /> <AlertComponent message={t("profile.errors.state")} type="error" />

View File

@@ -0,0 +1,76 @@
import { Button, Form, Input, notification } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { updateUserDetails } from "../../redux/user/user.actions";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import ResetForm from "../form-items-formatted/reset-form-item.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser
});
const mapDispatchToProps = dispatch => ({
updateUserDetails: userDetails => dispatch(updateUserDetails(userDetails))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(
Form.create({ name: "ProfileMyComponentForm" })(function ProfileMyComponent({
currentUser,
form,
updateUserDetails
}) {
const { isFieldsTouched, resetFields, getFieldDecorator } = form;
const { t } = useTranslation();
const handleSubmit = e => {
e.preventDefault();
form.validateFieldsAndScroll((err, values) => {
if (err) {
notification["error"]({
message: t("jobs.errors.validationtitle"),
description: t("jobs.errors.validation")
});
}
if (!err) {
console.log("values", values);
updateUserDetails({
displayName: values.displayname,
photoURL: values.photoURL
});
}
});
};
return (
<div>
{isFieldsTouched() ? <ResetForm resetFields={resetFields} /> : null}
<Form onSubmit={handleSubmit} autoComplete={"no"}>
<Form.Item label={t("user.fields.displayname")}>
{getFieldDecorator("displayname", {
initialValue: currentUser.displayName,
rules: [{ required: true }]
})(<Input name='displayname' />)}
</Form.Item>
<Form.Item label={t("user.fields.photourl")}>
{getFieldDecorator("photoURL", {
initialValue: currentUser.photoURL
})(<Input name='photoURL' />)}
</Form.Item>
<Button
type='primary'
key='submit'
htmlType='submit'
onClick={handleSubmit}>
{t("user.actions.updateprofile")}
</Button>
</Form>
</div>
);
})
);

View File

@@ -0,0 +1,52 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Table, Button } from "antd";
export default function ProfileShopsComponent({
loading,
data,
updateActiveShop
}) {
const { t } = useTranslation();
const columns = [
{
title: t("associations.fields.shopname"),
dataIndex: "shopname",
key: "shopname",
width: "25%",
render: (text, record) => <span>{record.bodyshop.shopname}</span>
},
{
title: t("associations.fields.active"),
dataIndex: "active",
key: "active",
width: "25%",
render: (text, record) => <span>{record.active ? "Yes" : "No"}</span>
},
{
title: t("associations.labels.actions"),
dataIndex: "actions",
key: "actions",
width: "25%",
render: (text, record) => (
<span>
{record.active ? null : (
<Button onClick={() => updateActiveShop(record.id)}>
Activate
</Button>
)}
</span>
)
}
];
return (
<Table
loading={loading}
size="small"
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={data}
/>
);
}

View File

@@ -0,0 +1,34 @@
import React from "react";
import { useQuery, useMutation } from "react-apollo";
import {
QUERY_ALL_ASSOCIATIONS,
UPDATE_ASSOCIATION
} from "../../graphql/associations.queries";
import AlertComponent from "../alert/alert.component";
import ProfileShopsComponent from "./profile-shops.component";
export default function ProfileShopsContainer() {
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ASSOCIATIONS);
const [updateAssocation] = useMutation(UPDATE_ASSOCIATION);
const updateActiveShop = activeShopId => {
data.associations.forEach(record => {
updateAssocation({
variables: {
assocId: record.id,
assocActive: record.id === activeShopId ? true : false
}
});
});
refetch();
};
if (error) return <AlertComponent type="error" message={error.message} />;
return (
<ProfileShopsComponent
loading={loading}
data={data ? data.associations : null}
updateActiveShop={updateActiveShop}
/>
);
}

View File

@@ -0,0 +1,67 @@
import { Checkbox, Col, DatePicker, Modal, Row, TimePicker, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
export default function ScheduleJobModalComponent({
appData,
setAppData,
formData,
setFormData,
...props
}) {
const { t } = useTranslation();
//TODO Existing appointments list only refreshes sometimes after modal close. May have to do with the container class.
return (
<Modal
{...props}
width={"80%"}
maskClosable={false}
destroyOnClose={true}
okButtonProps={{ disabled: appData.start ? false : true }}
>
<Row>
<Col span={14}>
<Row>
Manual Job Selection Scheduled Time
<Input
placeholder={t("appointments.fields.title")}
onChange={e => {
setAppData({ ...appData, title: e.target.value });
}}
/>
<DatePicker
value={appData.start}
onChange={e => {
setAppData({ ...appData, start: e });
}}
/>
<TimePicker
value={appData.start}
format={"HH:mm"}
minuteStep={15}
onChange={e => {
setAppData({ ...appData, start: e });
}}
/>
</Row>
{
//TODO Build out notifications.
}
<Checkbox
defaultChecked={formData.notifyCustomer}
onChange={e =>
setFormData({ ...formData, notifyCustomer: e.target.checked })
}
>
{t("jobs.labels.appointmentconfirmation")}
</Checkbox>
</Col>
<Col span={10}>
<ScheduleDayViewContainer day={appData.start} />
</Col>
</Row>
</Modal>
);
}

View File

@@ -0,0 +1,74 @@
import { notification } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useMutation } from "react-apollo";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_APPOINTMENT } from "../../graphql/appointments.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ScheduleAppointmentModalComponent from "./schedule-appointment-modal.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export default connect(
mapStateToProps,
null
)(function ScheduleAppointmentModalContainer({
scheduleModalState,
jobId,
bodyshop,
refetch
}) {
const [scheduleModalVisible, setscheduleModalVisible] = scheduleModalState;
const [appData, setAppData] = useState({
jobid: jobId,
bodyshopid: bodyshop.id,
isintake: false,
start: null
});
const [insertAppointment] = useMutation(INSERT_APPOINTMENT);
const [formData, setFormData] = useState({ notifyCustomer: false });
const { t } = useTranslation();
return (
<ScheduleAppointmentModalComponent
appData={appData}
setAppData={setAppData}
formData={formData}
setFormData={setFormData}
//Spreadable Modal Props
visible={scheduleModalVisible}
onCancel={() => setscheduleModalVisible(false)}
onOk={() => {
//TODO Customize the amount of minutes it will add.
insertAppointment({
variables: {
app: { ...appData, end: moment(appData.start).add(60, "minutes") }
}
})
.then(r => {
notification["success"]({
message: t("appointments.successes.created")
});
if (formData.notifyCustomer) {
//TODO Implement customer reminder on scheduling.
alert("Chosed to notify the customer somehow!");
}
setscheduleModalVisible(false);
if (refetch) refetch();
})
.catch(error => {
notification["error"]({
message: t("appointments.errors.saving", {
message: error.message
})
});
});
}}
/>
);
});

View File

@@ -0,0 +1 @@
@import 'react-big-calendar/lib/sass/styles';

View File

@@ -0,0 +1,34 @@
import moment from "moment";
import React from "react";
import { Calendar, momentLocalizer } from "react-big-calendar";
//import "react-big-calendar/lib/css/react-big-calendar.css";
import "./schedule-calendar.styles.scss";
import DateCellWrapper from "../schedule-datecellwrapper/schedule-datecellwrapper.component";
import Event from "../schedule-event/schedule-event.container";
const localizer = momentLocalizer(moment);
export default function ScheduleCalendarWrapperComponent({
data,
refetch,
defaultView,
...otherProps
}) {
return (
<Calendar
events={data}
defaultView={defaultView}
step={30}
showMultiDayTimes
localizer={localizer}
min={new Date("2020-01-01T06:00:00")} //TODO Read from business settings.
max={new Date("2020-01-01T20:00:00")}
components={{
event: e => {
return Event({ event: e.event, refetch: refetch });
},
dateCellWrapper: DateCellWrapper
}}
{...otherProps}
/>
);
}

View File

@@ -0,0 +1,46 @@
import React from "react";
//import "react-big-calendar/lib/css/react-big-calendar.css";
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
import { Button, Icon } from "antd";
import { useTranslation } from "react-i18next";
import ScheduleAppointmentModalContainer from "../schedule-appointment-modal/schedule-appointment-modal.container";
export default function ScheduleCalendarComponent({
data,
refetch,
scheduleModalState
}) {
const { t } = useTranslation();
return (
<div>
<Button
onClick={() => {
refetch();
}}
>
<Icon type="sync" />
</Button>
<Button
onClick={() => {
scheduleModalState[1](true);
}}
>
{t("appointments.actions.new")}
</Button>
<ScheduleAppointmentModalContainer
scheduleModalState={scheduleModalState}
jobId={null}
refetch={refetch}
/>
<ScheduleCalendarWrapperComponent
data={data}
defaultView="week"
refetch={refetch}
/>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import React, { useState } from "react";
import { useQuery } from "react-apollo";
import ScheduleCalendarComponent from "./schedule-calendar.component";
import { QUERY_ALL_ACTIVE_APPOINTMENTS } from "../../graphql/appointments.queries";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import AlertComponent from "../alert/alert.component";
export default function ScheduleCalendarContainer() {
const { loading, error, data, refetch } = useQuery(
QUERY_ALL_ACTIVE_APPOINTMENTS,
{
fetchPolicy: "network-only"
}
);
const scheduleModalState = useState(false);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
let normalizedData = data.appointments.map(e => {
//Required becuase Hasura returns a string instead of a date object.
return Object.assign(
{},
e,
{ start: new Date(e.start) },
{ end: new Date(e.end) }
);
});
return (
<ScheduleCalendarComponent
scheduleModalState={scheduleModalState}
refetch={refetch}
data={data ? normalizedData : null}
/>
);
}

View File

@@ -0,0 +1,18 @@
import React from "react";
export default function ScheduleDateCellWrapper(dateCellWrapperProps) {
// Show 'click me' text in arbitrary places by using the range prop
const style = {
display: "flex",
flex: 1,
borderLeft: "1px solid #DDD",
backgroundColor: "#fff"
};
return (
<div style={style}>
PLACEHOLDER:DATA
{dateCellWrapperProps.children}
</div>
);
}

View File

@@ -0,0 +1,21 @@
import React from "react";
import "react-big-calendar/lib/css/react-big-calendar.css";
import { useTranslation } from "react-i18next";
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
export default function ScheduleDayViewComponent({ data, day }) {
const { t } = useTranslation();
if (data)
//TODO Remove addtional calendar elements from day view.
return (
<ScheduleCalendarWrapperComponent
events={data}
defaultView="day"
views={["day"]}
style={{ height: "40vh" }}
defaultDate={new Date(day)}
//onNavigate={e => console.log("e", e)}
/>
);
else return <div>{t("appointments.labels.nodateselected")}</div>;
}

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