Merge pull request #12 from snaptsoft/dev-patrick

Dev patrick
This commit is contained in:
2020-02-25 10:15:43 -08:00
committed by GitHub
95 changed files with 3506 additions and 467 deletions

View File

@@ -47,6 +47,74 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>errors</name>
<children>
<concept_node>
<name>deleting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>saving</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>validation</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>fields</name>
<children>
@@ -73,6 +141,53 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>successes</name>
<children>
<concept_node>
<name>deleted</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>save</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
@@ -722,6 +837,63 @@
</folder_node>
</children>
</folder_node>
<folder_node>
<name>emails</name>
<children>
<folder_node>
<name>errors</name>
<children>
<concept_node>
<name>notsent</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>successes</name>
<children>
<concept_node>
<name>sent</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
<name>employees</name>
<children>
@@ -1578,6 +1750,79 @@
<folder_node>
<name>joblines</name>
<children>
<folder_node>
<name>actions</name>
<children>
<concept_node>
<name>new</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>errors</name>
<children>
<concept_node>
<name>creating</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>updating</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>fields</name>
<children>
@@ -1644,6 +1889,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>line_ind</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>mod_lb_hrs</name>
<definition_loaded>false</definition_loaded>
@@ -1665,6 +1931,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>mod_lbr_ty</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>oem_partno</name>
<definition_loaded>false</definition_loaded>
@@ -1686,6 +1973,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>op_code_desc</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>part_type</name>
<definition_loaded>false</definition_loaded>
@@ -1707,6 +2015,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>status</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>unq_seq</name>
<definition_loaded>false</definition_loaded>
@@ -1730,6 +2059,100 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>edit</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>new</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>successes</name>
<children>
<concept_node>
<name>created</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>updated</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
@@ -5173,6 +5596,58 @@
</folder_node>
</children>
</folder_node>
<folder_node>
<name>messaging</name>
<children>
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>messaging</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>typeamessage</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
<name>notes</name>
<children>
@@ -5979,6 +6454,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>lineremarks</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -6005,6 +6501,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>inthisorder</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>orderhistory</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>print</name>
<definition_loaded>false</definition_loaded>

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

@@ -20,14 +20,19 @@ export default function AllocationsAssignmentContainer({
const [insertAllocation] = useMutation(INSERT_ALLOCATION);
const handleAssignment = () => {
insertAllocation({ variables: { alloc: { ...assignment } } }).then(r => {
notification["success"]({
message: t("employees.successes.save")
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 })
});
});
//TODO Better way to reset the field decorators?
visibilityState[1](false);
if (refetch) refetch();
});
};
return (

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,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

@@ -20,7 +20,7 @@
.messages ul li {
display: inline-block;
clear: both;
float: left;
//float: left;
margin: 5px 15px 5px 15px;
width: calc(100% - 25px);
font-size: 0.9em;

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,53 +0,0 @@
import { Card } from "antd";
import React, { useState, useEffect } from "react";
import "./chat-window.styles.scss"; //https://bootsnipp.com/snippets/exR5v
import twilio from "twilio";
// const client = require("twilio")(
// "ACf1b1aaf0e04740828b49b6e58467d787",
// "0bea5e29a6d77593183ab1caa01d23de"
// );
const client = twilio(
"ACf1b1aaf0e04740828b49b6e58467d787",
"0bea5e29a6d77593183ab1caa01d23de"
);
export default function ChatWindowComponent({ toggleChatVisible }) {
const [conversations, setConversations] = useState([]);
useEffect(() => {
client.messages.list({ limit: 20 }, (error, items) => {
setConversations(
items.reduce((acc, value) => {
acc.push({
sid: value.sid,
direction: value.direction,
body: value.body
});
return acc;
}, [])
);
});
return () => {};
}, [setConversations]);
console.log(conversations);
return (
<Card style={{ width: "400px" }}>
<div>
<button onClick={() => toggleChatVisible()}>X</button>
<div className='messages' style={{ height: "400px" }}>
<ul>
{conversations.map(item => (
<li
key={item.sid}
className={`${
item.direction === "inbound" ? "replies" : "sent"
}`}>
<p> {item.body}</p>
</li>
))}
</ul>
</div>
</div>
</Card>
);
}

View File

@@ -1,37 +0,0 @@
import { Affix, Button, 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 } from "../../redux/messaging/messaging.selectors";
import ChatWindowComponent from "./chat-window.component";
const mapStateToProps = createStructuredSelector({
chatVisible: selectChatVisible
});
const mapDispatchToProps = dispatch => ({
toggleChatVisible: () => dispatch(toggleChatVisible())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(function ChatWindowContainer({ chatVisible, toggleChatVisible }) {
return (
<Affix offsetBottom={25}>
{chatVisible ? (
<ChatWindowComponent toggleChatVisible={toggleChatVisible} />
) : (
<Badge count={5}>
<Button
type='primary'
shape='circle'
icon='message'
onClick={() => toggleChatVisible()}
/>
</Badge>
)}
</Affix>
);
});

View File

@@ -4,41 +4,34 @@ import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
export default function SendEmailButtonComponent({
emailConfig,
messageOptions,
handleConfigChange,
handleHtmlChange
}) {
return (
<div>
THis is where the text editing will happen To
<Input
defaultValue={emailConfig.to}
defaultValue={messageOptions.to}
onChange={handleConfigChange}
name='to'
/>
CC
<Input
defaultValue={emailConfig.cc}
defaultValue={messageOptions.cc}
onChange={handleConfigChange}
name='cc'
/>
Subject
<Input
defaultValue={emailConfig.subject}
defaultValue={messageOptions.subject}
onChange={handleConfigChange}
name='subject'
/>
<CKEditor
editor={ClassicEditor}
data={emailConfig.html}
onInit={editor => {
// You can store the "editor" and use when it is needed.
console.log("Editor is ready to use!", editor);
}}
data={messageOptions.html}
onChange={(event, editor) => {
const data = editor.getData();
console.log({ event, editor, data });
handleHtmlChange(data);
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

@@ -10,7 +10,7 @@ export default function ResetForm({ resetFields }) {
message={
<div>
{t("general.messages.unsavedchanges")}
<Button onClick={() => resetFields()}>
<Button style={{ marginLeft: "20px" }} onClick={() => resetFields()}>
{t("general.actions.reset")}
</Button>
</div>

View File

@@ -15,107 +15,168 @@ export default ({
}) => {
const { t } = useTranslation();
//TODO Add
return (
<Row type='flex' justify='space-around' align='middle'>
<Row type="flex" justify="space-around" align="middle">
{logo ? (
<Col span={4}>
<img alt='Shop Logo' src={logo} style={{ height: "40px" }} />
<Col span={3}>
<img alt="Shop Logo" src={logo} style={{ height: "40px" }} />
</Col>
) : null}
<Col span={14}>
<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>
</Menu.Item>
<Menu.SubMenu title={t("menus.header.jobs")}>
<Menu.Item key='schedule'>
<Link to='/manage/schedule'>
<Icon type='calendar' />
{t("menus.header.schedule")}
</Link>
</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>
{landingHeader ? (
<Menu
theme="dark"
className="header"
selectedKeys={selectedNavItem}
mode="horizontal"
onClick={handleMenuClick}
>
<ManageSignInButton />
<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
title={
<div>
<Avatar
size='medium'
alt='Avatar'
src={currentUser.photoURL ? currentUser.photoURL : UserImage}
style={{ margin: "10px" }}
/>
{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")}
<div>
<Avatar
size="medium"
alt="Avatar"
src={
currentUser.photoURL ? currentUser.photoURL : UserImage
}
style={{ margin: "10px" }}
/>
{currentUser.displayName || t("general.labels.unknown")}
</div>
}
>
<Menu.Item onClick={signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item actiontype='lang-select' key='fr'>
{t("general.languages.french")}
<Menu.Item>
<Link to="/manage/profile">
{t("menus.currentuser.profile")}
</Link>
</Menu.Item>
<Menu.Item actiontype='lang-select' key='es'>
{t("general.languages.spanish")}
<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>
</Menu.Item>
<Menu.SubMenu title={t("menus.header.jobs")}>
<Menu.Item key="schedule">
<Link to="/manage/schedule">
<Icon type="calendar" />
{t("menus.header.schedule")}
</Link>
</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>
</Menu>
<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
title={
<div>
<Avatar
size="medium"
alt="Avatar"
src={
currentUser.photoURL ? currentUser.photoURL : UserImage
}
style={{ margin: "10px" }}
/>
{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>
)}
</Col>
<Col span={4}>{!landingHeader ? null : <ManageSignInButton />}</Col>
</Row>
);
};

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

@@ -1,10 +1,12 @@
import { Button, Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
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({
@@ -15,7 +17,8 @@ export default function JobLinesComponent({
selectedLines,
setSelectedLines,
partsOrderModalVisible,
jobId
jobId,
setJobLineEditContext
}) {
const [state, setState] = useState({
sortedInfo: {}
@@ -44,7 +47,8 @@ export default function JobLinesComponent({
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true,
editable: true
editable: true,
width: "20%"
},
{
title: t("joblines.fields.oem_partno"),
@@ -59,7 +63,7 @@ export default function JobLinesComponent({
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
editable: true,
width: "20%",
width: "10%",
render: (text, record) => (
<span>
{record.oem_partno ? record.oem_partno : record.op_code_desc}
@@ -75,7 +79,15 @@ export default function JobLinesComponent({
state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
ellipsis: true,
editable: true,
width: "10%"
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"),
@@ -85,7 +97,7 @@ export default function JobLinesComponent({
sortOrder:
state.sortedInfo.columnKey === "db_price" && state.sortedInfo.order,
ellipsis: true,
width: "10%",
width: "8%",
render: (text, record) => (
<CurrencyFormatter>{record.db_price}</CurrencyFormatter>
)
@@ -98,7 +110,7 @@ export default function JobLinesComponent({
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true,
width: "10%",
width: "8%",
render: (text, record) => (
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
)
@@ -111,21 +123,39 @@ export default function JobLinesComponent({
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",
sorter: (a, b) => a.act_price - b.act_price, //TODO Fix employee sorting.
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 => (
<div
key={
item.id
}>{`${item.employee.first_name} ${item.employee.last_name} (${item.hours})`}</div>
<AllocationsEmployeeLabelContainer
key={item.id}
refetch={refetch}
allocation={item}
/>
))
: null}
<AllocationsAssignmentContainer
@@ -143,7 +173,16 @@ export default function JobLinesComponent({
key: "actions",
render: (text, record) => (
<span>
<Button>{t("general.actions.edit")}</Button>
<Button
onClick={() => {
setJobLineEditContext({
actions: { refetch: refetch },
context: record
});
}}
>
{t("general.actions.edit")}
</Button>
</span>
)
}
@@ -169,6 +208,7 @@ export default function JobLinesComponent({
<PartsOrderModalContainer
partsOrderModalVisible={partsOrderModalVisible}
linesToOrder={selectedLines}
refetch={refetch}
jobId={jobId}
/>
@@ -185,23 +225,55 @@ export default function JobLinesComponent({
/>
<Button
disabled={selectedLines.length > 0 ? false : true}
onClick={() => setPartsModalVisible(true)}>
onClick={() => setPartsModalVisible(true)}
>
{t("parts.actions.order")}
</Button>
<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'
pagination={{ position: "bottom", defaultPageSize: 50 }}
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'
rowKey="id"
dataSource={jobLines}
onChange={handleTableChange}
/>

View File

@@ -5,9 +5,16 @@ 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 Form.create({ name: "JobsDetailJobLines" })(
export default function JobLinesContainer({ jobId }) {
import { connect } from "react-redux";
import { setModalContext } from "../../redux/modals/modals.actions";
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 },
fetchPolicy: "network-only"
@@ -16,7 +23,8 @@ export default function JobLinesContainer({ jobId }) {
const [searchText, setSearchText] = useState("");
const [selectedLines, setSelectedLines] = useState([]);
const partsOrderModalVisible = useState(false);
if (error) return <AlertComponent message={error.message} type='error' />;
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<JobLinesComponent
@@ -58,7 +66,7 @@ export default function JobLinesContainer({ jobId }) {
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

@@ -54,12 +54,13 @@ export default withRouter(function JobsAvailableSupplementContainer({
//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"];
//Strip out the header options
//TODO Remove all required fields.
}
updateJob({
@@ -101,12 +102,11 @@ export default withRouter(function JobsAvailableSupplementContainer({
setSelectedJob(null);
};
if (error) return <AlertComponent type="error" message={error.message} />;
if (error) return <AlertComponent type='error' message={error.message} />;
return (
<LoadingSpinner
loading={insertLoading}
message={t("jobs.labels.creating_new_job")}
>
message={t("jobs.labels.creating_new_job")}>
<JobsAvailableSupplementComponent
loading={loading}
data={data}

View File

@@ -0,0 +1,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,11 +1,11 @@
import { Input, Table, Icon, Button } from "antd";
import { Button, Icon, Input, Table } from "antd";
import React, { useState } from "react";
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 { alphaSort } from "../../utils/sorters";
import { withRouter } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import StartChatButton from "../chat-open-button/chat-open-button.component";
export default withRouter(function JobsList({
searchTextState,
@@ -78,13 +78,7 @@ export default withRouter(function JobsList({
return record.ownr_ph1 ? (
<span>
<PhoneFormatter>{record.ownr_ph1}</PhoneFormatter>
<Icon
style={{ margin: 4 }}
type='message'
onClick={() => {
alert("SMSing will happen here.");
}}
/>
<StartChatButton phone={record.ownr_ph1} />
</span>
) : (
t("general.labels.unknown")
@@ -214,10 +208,10 @@ export default withRouter(function JobsList({
return (
<div style={{ display: "flex" }}>
<Button onClick={() => refetch()}>
<Icon type='sync' />
<Icon type="sync" />
</Button>
<Input.Search
placeholder='Search...'
placeholder="Search..."
onChange={e => {
setSearchText(e.target.value);
}}
@@ -226,10 +220,10 @@ export default withRouter(function JobsList({
</div>
);
}}
size='small'
size="small"
pagination={{ position: "top" }}
columns={columns.map(item => ({ ...item }))}
rowKey='id'
rowKey="id"
dataSource={jobs}
rowSelection={{ selectedRowKeys: [selectedJob] }}
onChange={handleTableChange}

View File

@@ -5,12 +5,20 @@ import { useTranslation } from "react-i18next";
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
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,
visible,
changeVisibility,
refetch,
existingNote
existingNote,currentUser
}) {
const { t } = useTranslation();
const [insertNote] = useMutation(INSERT_NEW_NOTE);
@@ -33,7 +41,7 @@ export default function NoteUpsertModalContainer({
insertNote({
variables: {
noteInput: [
{ ...noteState, jobid: jobId, created_by: "patrick@bodyshop.app" } //TODO Fix the created by.
{ ...noteState, jobid: jobId, created_by: currentUser.email }
]
}
}).then(r => {
@@ -73,3 +81,4 @@ export default function NoteUpsertModalContainer({
/>
);
}
);

View File

@@ -1,13 +1,15 @@
import { AutoComplete, DatePicker, Icon, Input, List, Radio } from "antd";
import React, { useState } from "react";
import { AutoComplete, Icon, DatePicker, Radio } from "antd";
import { useTranslation } from "react-i18next";
export default function PartsOrderModalComponent({
vendorList,
state,
sendTypeState
sendTypeState,
orderLinesState
}) {
const [partsOrder, setPartsOrder] = state;
const [sendType, setSendType] = sendTypeState;
const orderLines = orderLinesState[0];
const [vendorComplete, setVendorComplete] = useState(vendorList);
const { t } = useTranslation();
@@ -22,8 +24,6 @@ export default function PartsOrderModalComponent({
};
const handleSelect = (value, option) => {
console.log("value", value);
console.log("option", option);
setPartsOrder({ ...partsOrder, vendorid: option.key });
};
@@ -52,6 +52,33 @@ export default function PartsOrderModalComponent({
}}
/>
{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)}>

View File

@@ -1,50 +1,105 @@
import { Modal, notification } from "antd";
import React, { useState } from "react";
import { useQuery, useMutation } from "react-apollo";
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 { QUERY_ALL_VENDORS_FOR_ORDER } from "../../graphql/vendors.queries";
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 {
selectCurrentUser,
selectBodyshop
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 { useTranslation } from "react-i18next";
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,
null
mapDispatchToProps
)(function PartsOrderModalContainer({
partsOrderModalVisible,
linesToOrder,
jobId,
currentUser,
bodyshop
bodyshop,
refetch,
setEmailOptions,
toggleEmailOverlayVisible
}) {
const [modalVisible, setModalVisible] = partsOrderModalVisible;
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
fetchPolicy: "network-only",
skip: !modalVisible
});
const { t } = useTranslation();
const [insertPartOrder] = useMutation(INSERT_NEW_PARTS_ORDERS);
const sendTypeState = useState("e");
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
});
console.log("sendTypeState[0]", sendTypeState[0]);
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: {
@@ -53,29 +108,58 @@ export default connect(
...partsOrder,
status: bodyshop.md_order_statuses.default_ordered || "Ordered*",
parts_order_lines: {
data: 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",
joblineid: value.joblineid,
status:
bodyshop.md_order_statuses.default_ordered || "Ordered*"
});
return acc;
}, [])
data: orderLines
}
}
]
}
})
.then(r => {
notification["success"]({
message: t("parts_orders.successes.created")
});
setModalVisible(false);
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"]({
@@ -96,6 +180,7 @@ export default connect(
vendorList={(data && data.vendors) || []}
state={partsOrderState}
sendTypeState={sendTypeState}
orderLinesState={orderLinesState}
/>
</LoadingSpinner>
</Modal>

View File

@@ -1,72 +0,0 @@
import { Button, Modal } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useQuery } from "react-apollo";
//Message options has the to & from details
//Query Config is what can get popped into UseQuery(QUERY_NAME, {variables: {vars}, fetchonly})
//Template Which template should be used to send the email.
import ReactDOMServer from "react-dom/server";
import { renderEmail } from "react-html-email";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import SendEmailButtonComponent from "./send-email-button.component";
export default function SendEmail({
MessageOptions,
QueryConfig,
Template,
...otherProps
}) {
const [modalVisible, setModalVisible] = useState(false);
const [emailConfig, setEmailConfig] = useState({ ...MessageOptions });
const [gqlQuery, vars] = QueryConfig;
const { loading, data } = useQuery(gqlQuery, {
...vars,
fetchPolicy: "network-only",
skip: !modalVisible
});
if (data && !emailConfig.html) {
console.log(ReactDOMServer.renderToStaticMarkup(<Template data={data} />));
setEmailConfig({
...emailConfig,
//html: ReactDOMServer.renderToStaticMarkup(<Template data={data} />)
html: renderEmail(<Template data={data} />)
});
}
const handleConfigChange = event => {
const { name, value } = event.target;
setEmailConfig({ ...emailConfig, [name]: value });
};
const handleHtmlChange = text => {
setEmailConfig({ ...emailConfig, html: text });
};
const sendEmail = () => {
axios.post("/sendemail", emailConfig).then(response => {
alert(JSON.stringify(response));
});
};
return (
<div>
<Modal
destroyOnClose={true}
visible={modalVisible}
width={"80%"}
onOk={sendEmail}
onCancel={() => setModalVisible(false)}>
<LoadingSpinner loading={loading}>
<SendEmailButtonComponent
handleConfigChange={handleConfigChange}
emailConfig={emailConfig}
handleHtmlChange={handleHtmlChange}
/>
</LoadingSpinner>
</Modal>
<Button onClick={() => setModalVisible(true)}>
{otherProps.children}
</Button>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import React from "react";
function Cell({ children }) {
function Column({ children }) {
return <td>{children}</td>;
}
@@ -8,7 +8,7 @@ function Row({ children }) {
return (
<tr>
{React.Children.map(children, el => {
if (el.type === Cell) return el;
if (el.type === Column) return el;
return <td>{el}</td>;
})}
@@ -25,7 +25,7 @@ function Grid({ children }) {
if (el.type === Row) return el;
if (el.type === Cell) {
if (el.type === Column) {
return <tr>{el}</tr>;
}
@@ -41,6 +41,6 @@ function Grid({ children }) {
}
Grid.Row = Row;
Grid.Cell = Cell;
Grid.Column = Column;
export default Grid;

View File

@@ -0,0 +1,23 @@
import React from "react";
export default function Header({ bodyshop }) {
return (
<table>
<tr>
<td>
<img alt='' src={bodyshop.logo_img_path} />
</td>
<td>
<div>
<strong>{`${bodyshop.shopname}`}</strong>
</div>
<div>{`${bodyshop.address1} ${bodyshop.address2} ${bodyshop.city} ${bodyshop.state} ${bodyshop.zip_post}`}</div>
<div>
<a href={`mailto:${bodyshop.email}`}>{bodyshop.email}</a>{" "}
{` | ${bodyshop.ph1}`}
</div>
</td>
</tr>
</table>
);
}

View File

@@ -1,30 +1,53 @@
import React from "react";
import { A, Box, Email, Item, Span } from "react-html-email";
import styled from "styled-components";
import Header from "../components/header/header.component";
const D = styled.div`
table {
font-family: arial, sans-serif;
border-collapse: collapse;
width: 100%;
}
td,
th {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
}
tr:nth-child(even) {
background-color: #dddddd;
}
`;
export default function PartsOrderEmail({ data }) {
console.log("Data", data);
const order = data.parts_orders_by_pk;
return (
<Email title='Hello World!'>
<Box>
<Item align='center'>
<Span fontSize={20}>
This is an example email made with:
<A href='https://github.com/chromakode/react-html-email'>
react-html-email
</A>
.
</Span>
</Item>
<Item align='center'>
<Span fontSize={20}>
This is an example email made with:
<A href='https://github.com/chromakode/react-html-email'>
react-html-email
</A>
.
</Span>
</Item>
</Box>
</Email>
<D>
<Header bodyshop={data.bodyshops[0]} />
<table>
<tr>
<td>{`Deliver By: ${order.deliver_by}`}</td>
<td>{`Ordered By: ${order.user_email}`}</td>
</tr>
</table>
<table>
<tr>
<th>Line Description</th>
<th>Part #</th>
<th>Price</th>
<th>Line Remarks</th>
</tr>
{order.parts_order_lines.map(item => (
<tr key={item.id}>
<td>{item.line_desc}</td>
<td>{item.oem_partno}</td>
<td>{item.act_price}</td>
<td>{item.line_remarks}</td>
</tr>
))}
</table>
</D>
);
}

View File

@@ -9,3 +9,13 @@ export const INSERT_ALLOCATION = gql`
}
}
`;
export const DELETE_ALLOCATION = gql`
mutation DELETE_ALLOCATION($id: uuid!) {
delete_allocations(where: { id: { _eq: $id } }) {
returning {
id
}
}
}
`;

View File

@@ -18,6 +18,20 @@ export const GET_JOB_LINES_BY_PK = gql`
lbr_op
lbr_amt
op_code_desc
status
parts_order_lines {
id
parts_order {
id
order_number
order_date
user_email
vendor {
id
name
}
}
}
allocations {
id
hours
@@ -30,3 +44,30 @@ export const GET_JOB_LINES_BY_PK = gql`
}
}
`;
export const UPDATE_JOB_LINE_STATUS = gql`
mutation UPDATE_JOB_LINE_STATUS($ids: [uuid!]!, $status: String!) {
update_joblines(where: { id: { _in: $ids } }, _set: { status: $status }) {
affected_rows
}
}
`;
export const INSERT_NEW_JOB_LINE = gql`
mutation INSERT_NEW_JOB_LINE($lineInput: [joblines_insert_input!]!) {
insert_joblines(objects: $lineInput) {
returning {
id
}
}
}
`;
export const UPDATE_JOB_LINE = gql`
mutation UPDATE_JOB_LINE($lineId: uuid!, $line: joblines_set_input!) {
update_joblines(where: { id: { _eq: $lineId } }, _set: $line) {
returning {
id
}
}
}
`;

View File

@@ -1,18 +1,67 @@
import { Form, Icon, Tabs } from "antd";
import React, { useContext } from "react";
import React, { lazy, Suspense, useContext } from "react";
import { useTranslation } from "react-i18next";
import { FaHardHat, FaInfo, FaRegStickyNote, FaShieldAlt } from "react-icons/fa";
import {
FaHardHat,
FaInfo,
FaRegStickyNote,
FaShieldAlt
} from "react-icons/fa";
import ResetForm from "../../components/form-items-formatted/reset-form-item.component";
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
import JobsDetailClaims from "../../components/jobs-detail-claims/jobs-detail-claims.component";
import JobsDetailDatesComponent from "../../components/jobs-detail-dates/jobs-detail-dates.component";
import JobsDetailFinancials from "../../components/jobs-detail-financial/jobs-detail-financial.component";
import JobsDetailHeader from "../../components/jobs-detail-header/jobs-detail-header.component";
import JobsDetailInsurance from "../../components/jobs-detail-insurance/jobs-detail-insurance.component";
import JobsDocumentsContainer from "../../components/jobs-documents/jobs-documents.container";
import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container";
import ScheduleJobModalContainer from "../../components/schedule-job-modal/schedule-job-modal.container";
//import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
//import JobsDetailClaims from "../../components/jobs-detail-claims/jobs-detail-claims.component";
//import JobsDetailDatesComponent from "../../components/jobs-detail-dates/jobs-detail-dates.component";
//import JobsDetailFinancials from "../../components/jobs-detail-financial/jobs-detail-financial.component";
//import JobsDetailHeader from "../../components/jobs-detail-header/jobs-detail-header.component";
//import JobsDetailInsurance from "../../components/jobs-detail-insurance/jobs-detail-insurance.component";
//import JobsDocumentsContainer from "../../components/jobs-documents/jobs-documents.container";
//import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container";
//import ScheduleJobModalContainer from "../../components/schedule-job-modal/schedule-job-modal.container";
//import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
//import EnterInvoiceModalContainer from "../../components/invoice-enter-modal/invoice-enter-modal.container";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import JobDetailFormContext from "./jobs-detail.page.context";
import JobsDetailPliContainer from "../../components/jobs-detail-pli/jobs-detail-pli.container";
const JobsLinesContainer = lazy(() =>
import("../../components/job-detail-lines/job-lines.container")
);
const JobsDetailClaims = lazy(() =>
import("../../components/jobs-detail-claims/jobs-detail-claims.component")
);
const JobsDetailDatesComponent = lazy(() =>
import("../../components/jobs-detail-dates/jobs-detail-dates.component")
);
const JobsDetailFinancials = lazy(() =>
import(
"../../components/jobs-detail-financial/jobs-detail-financial.component"
)
);
const JobsDetailHeader = lazy(() =>
import("../../components/jobs-detail-header/jobs-detail-header.component")
);
const JobsDetailInsurance = lazy(() =>
import(
"../../components/jobs-detail-insurance/jobs-detail-insurance.component"
)
);
const JobsDocumentsContainer = lazy(() =>
import("../../components/jobs-documents/jobs-documents.container")
);
const JobNotesContainer = lazy(() =>
import("../../components/jobs-notes/jobs-notes.container")
);
const ScheduleJobModalContainer = lazy(() =>
import("../../components/schedule-job-modal/schedule-job-modal.container")
);
const JobLineUpsertModalContainer = lazy(() =>
import(
"../../components/job-lines-upsert-modal/job-lines-upsert-modal.container"
)
);
const EnterInvoiceModalContainer = lazy(() =>
import("../../components/invoice-enter-modal/invoice-enter-modal.container")
);
export default function JobsDetailPage({
job,
@@ -39,13 +88,18 @@ export default function JobsDetailPage({
};
return (
<div>
<Suspense
fallback={<LoadingSpinner message={t("general.labels.loadingapp")} />}
>
<ScheduleJobModalContainer
scheduleModalState={scheduleModalState}
jobId={job.id}
refetch={refetch}
/>
<JobLineUpsertModalContainer />
<EnterInvoiceModalContainer />
<Form onSubmit={handleSubmit} {...formItemLayout} autoComplete={"off"}>
<JobsDetailHeader
job={job}
@@ -116,7 +170,7 @@ export default function JobsDetailPage({
}
key="partssublet"
>
Partssublet
<JobsDetailPliContainer job={job} />
</Tabs.TabPane>
<Tabs.TabPane
@@ -167,6 +221,6 @@ export default function JobsDetailPage({
</Tabs.TabPane>
</Tabs>
</Form>
</div>
</Suspense>
);
}

View File

@@ -34,10 +34,8 @@ export default connect(
const [selectedJob, setSelectedJob] = useState(hash ? hash.substr(1) : null);
const searchTextState = useState("");
const searchText = searchTextState[0];
if (error) return <AlertComponent message={error.message} type='error' />;
//TODO Implement pagination for this.
if (error) return <AlertComponent message={error.message} type="error" />;
console.log(typeof searchText);
return (
<div>
<JobsList

View File

@@ -1,29 +1,28 @@
import React from "react";
import SendEmailButton from "../../components/send-email-button/send-email-button.container";
import PartsOrderEmail from "../../emails/parts-order/parts-order.email";
import { REPORT_QUERY_PARTS_ORDER_BY_PK } from "../../emails/parts-order/parts-order.query";
export default function ManageRootPageComponent() {
//const client = useApolloClient();
return (
<div>
<SendEmailButton
MessageOptions={{
from: {
name: "Kavia"
},
to: "patrickwf@gmail.com",
replyTo: "patrickwf@gmail.com"
}}
Template={PartsOrderEmail}
QueryConfig={[
REPORT_QUERY_PARTS_ORDER_BY_PK,
{
variables: { id: "ebe0fb6b-6ec4-4ae0-8fdc-49bdf1e37ff3" }
}
]}>
Send an Email in new Window
</SendEmailButton>
{
// <SendEmailButton
// MessageOptions={{
// from: {
// name: "Kavia"
// },
// to: "patrickwf@gmail.com",
// replyTo: "patrickwf@gmail.com"
// }}
// Template={PartsOrderEmail}
// QueryConfig={[
// REPORT_QUERY_PARTS_ORDER_BY_PK,
// {
// variables: { id: "ebe0fb6b-6ec4-4ae0-8fdc-49bdf1e37ff3" }
// }
// ]}>
// Send an Email in new Window
// </SendEmailButton>
}
</div>
);
}

View File

@@ -8,6 +8,7 @@ import FooterComponent from "../../components/footer/footer.component";
import HeaderContainer from "../../components/header/header.container";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import "./manage.page.styles.scss";
import Test from "../../components/_test/test.component";
const ManageRootPage = lazy(() =>
import("../manage-root/manage-root.page.container")
@@ -24,7 +25,7 @@ const JobsAvailablePage = lazy(() =>
import("../jobs-available/jobs-available.page.container")
);
const ChatWindowContainer = lazy(() =>
import("../../components/chat-window/chat-window.container")
import("../../components/chat-overlay/chat-overlay.container")
);
const ScheduleContainer = lazy(() =>
import("../schedule/schedule.page.container")
@@ -44,6 +45,10 @@ const ShopVendorPageContainer = lazy(() =>
import("../shop-vendor/shop-vendor.page.container")
);
const EmailOverlayContainer = lazy(() =>
import("../../components/email-overlay/email-overlay.container.jsx")
);
const { Header, Content, Footer } = Layout;
export default function Manage({ match }) {
@@ -60,17 +65,20 @@ export default function Manage({ match }) {
</Header>
<Layout>
<Content
className='content-container'
style={{ padding: "0em 4em 4em" }}>
className="content-container"
style={{ padding: "0em 4em 4em" }}
>
<ErrorBoundary>
<Suspense
fallback={
<LoadingSpinner message={t("general.labels.loadingapp")} />
}>
}
>
DELETE THIS
<Test />
<EmailOverlayContainer />
<Route exact path={`${match.path}`} component={ManageRootPage} />
<Route exact path={`${match.path}/jobs`} component={JobsPage} />
<Route
exact
path={`${match.path}/jobs/:jobId`}
@@ -111,13 +119,11 @@ export default function Manage({ match }) {
path={`${match.path}/schedule`}
component={ScheduleContainer}
/>
<Route
exact
path={`${match.path}/available`}
component={JobsAvailablePage}
/>
<Route exact path={`${match.path}/shop/`} component={ShopPage} />
<Route
exact
@@ -130,13 +136,8 @@ export default function Manage({ match }) {
</Layout>
<Footer>
<FooterComponent />
{
// <Affix offsetBottom={20}>
// <ChatWindowContainer />
// </Affix>
}
<ChatWindowContainer />
</Footer>
<ChatWindowContainer />
<BackTop />
</Layout>
);

View File

@@ -0,0 +1,24 @@
import EmailActionTypes from "./email.types";
export const toggleEmailOverlayVisible = () => ({
type: EmailActionTypes.TOGGLE_EMAIL_OVERLAY_VISIBLE
});
export const setEmailOptions = options => ({
type: EmailActionTypes.SET_EMAIL_OPTIONS,
payload: options
});
export const sendEmail = email => ({
type: EmailActionTypes.SEND_EMAIL,
payload: email
});
export const sendEmailSuccess = options => ({
type: EmailActionTypes.SEND_EMAIL_SUCCESS
});
export const sendEmailFailure = error => ({
type: EmailActionTypes.SEND_EMAIL_FAILURE,
payload: error
});

View File

@@ -0,0 +1,35 @@
import EmailActionTypes from "./email.types";
const INITIAL_STATE = {
emailConfig: {
messageOptions: {
from: { name: "ShopName", address: "noreply@bodyshop.app" },
to: null,
replyTo: null
},
template: null,
queryConfig: [null, { variables: null }]
},
visible: false,
error: null
};
const emailReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case EmailActionTypes.TOGGLE_EMAIL_OVERLAY_VISIBLE:
return {
...state,
visible: !state.visible
};
case EmailActionTypes.SET_EMAIL_OPTIONS:
return {
...state,
emailConfig: { ...action.payload }
};
default:
return state;
}
};
export default emailReducer;

View File

@@ -0,0 +1,51 @@
import { all, call, put, takeLatest } from "redux-saga/effects";
import { sendEmailFailure, sendEmailSuccess } from "./email.actions";
import EmailActionTypes from "./email.types";
import axios from "axios";
export function* onSendEmail() {
yield takeLatest(EmailActionTypes.SEND_EMAIL, sendEmail);
}
export function* sendEmail(payload) {
try {
console.log("Sending thta email", payload);
axios.post("/sendemail", payload).then(response => {
console.log(JSON.stringify(response));
put(sendEmailSuccess());
});
} catch (error) {
console.log("Error in sendEmail saga.");
yield put(sendEmailFailure(error.message));
}
}
export function* onSendEmailSuccess() {
yield takeLatest(EmailActionTypes.SEND_EMAIL_SUCCESS, sendEmailSuccessSaga);
}
export function* sendEmailSuccessSaga() {
try {
console.log("Send email success.");
} catch (error) {
console.log("Error in sendEmailSuccess saga.");
yield put(sendEmailFailure(error.message));
}
}
export function* onSendEmailFailure() {
yield takeLatest(EmailActionTypes.SEND_EMAIL_FAILURE, sendEmailFailureSaga);
}
export function* sendEmailFailureSaga(error) {
try {
yield console.log(error);
} catch (error) {
console.log("Error in sendEmailFailure saga.", error.message);
}
}
export function* emailSagas() {
yield all([
call(onSendEmail),
call(onSendEmailFailure),
call(onSendEmailSuccess)
]);
}

View File

@@ -0,0 +1,25 @@
import { createSelector } from "reselect";
const selectEmail = state => state.email;
const selectEmailConfigMessageOptions = state =>
state.email.emailConfig.messageOptions;
const selectEmailConfigTemplate = state => state.email.emailConfig.template;
const selectEmailConfigQuery = state => state.email.emailConfig.queryConfig;
export const selectEmailVisible = createSelector(
[selectEmail],
email => email.visible
);
export const selectEmailConfig = createSelector(
[
selectEmailConfigMessageOptions,
selectEmailConfigTemplate,
selectEmailConfigQuery
],
(messageOptions, template, queryConfig) => ({
messageOptions,
template,
queryConfig
})
);

View File

@@ -0,0 +1,8 @@
const EmailActionTypes = {
TOGGLE_EMAIL_OVERLAY_VISIBLE: "TOGGLE_EMAIL_OVERLAY_VISIBLE",
SET_EMAIL_OPTIONS: "SET_EMAIL_OPTIONS",
SEND_EMAIL: "SEND_EMAIL",
SEND_EMAIL_SUCCESS: "SEND_EMAIL_SUCCESS",
SEND_EMAIL_FAILURE: "SEND_EMAIL_FAILURE"
};
export default EmailActionTypes;

View File

@@ -1,7 +1,21 @@
import MessagingActionTypes from './messaging.types'
import MessagingActionTypes from "./messaging.types";
export const toggleChatVisible = () => ({
type: MessagingActionTypes.TOGGLE_CHAT_VISIBLE,
type: MessagingActionTypes.TOGGLE_CHAT_VISIBLE
//payload: user
});
export const toggleConversationVisible = conversationId => ({
type: MessagingActionTypes.TOGGLE_CONVERSATION_VISIBLE,
payload: conversationId
});
export const openConversation = phone => ({
type: MessagingActionTypes.OPEN_CONVERSATION,
payload: phone
});
export const closeConversation = phone => ({
type: MessagingActionTypes.CLOSE_CONVERSATION,
payload: phone
});

View File

@@ -1,7 +1,11 @@
import MessagingActionTypes from "./messaging.types";
const INITIAL_STATE = {
visible: false
visible: false,
conversations: [
{ phone: "6049992002", open: false },
{ phone: "6049992991", open: false }
]
};
const messagingReducer = (state = INITIAL_STATE, action) => {
@@ -16,6 +20,36 @@ const messagingReducer = (state = INITIAL_STATE, action) => {
...state,
visible: true
};
case MessagingActionTypes.OPEN_CONVERSATION:
if (state.conversations.find(c => c.phone === action.payload))
return {
...state,
conversations: state.conversations.map(c =>
c.phone === action.payload ? { ...c, open: true } : c
)
};
else
return {
...state,
conversations: [
...state.conversations,
{ phone: action.payload, open: true }
]
};
case MessagingActionTypes.CLOSE_CONVERSATION:
return {
...state,
conversations: state.conversations.filter(
c => c.phone !== action.payload
)
};
case MessagingActionTypes.TOGGLE_CONVERSATION_VISIBLE:
return {
...state,
conversations: state.conversations.map(c =>
c.phone === action.payload ? { ...c, open: !c.open } : c
)
};
default:
return state;
}

View File

@@ -6,3 +6,8 @@ export const selectChatVisible = createSelector(
[selectMessaging],
messaging => messaging.visible
);
export const selectConversations = createSelector(
[selectMessaging],
messaging => messaging.conversations
);

View File

@@ -1,5 +1,9 @@
const MessagingActionTypes = {
TOGGLE_CHAT_VISIBLE: "TOGGLE_CHAT_VISIBLE",
SET_CHAT_VISIBLE: "SET_CHAT_VISIBLE"
SET_CHAT_VISIBLE: "SET_CHAT_VISIBLE",
OPEN_CONVERSATION: "OPEN_CONVERSATION",
CLOSE_CONVERSATION: "CLOSE_CONVERSATION",
TOGGLE_CONVERSATION_VISIBLE: "TOGGLE_CONVERSATION_VISIBLE",
SEND_MESSAGE: "SEND_MESSAGE"
};
export default MessagingActionTypes;

View File

@@ -0,0 +1,12 @@
import ModalsActionTypes from "./modals.types";
export const toggleModalVisible = modalName => ({
type: ModalsActionTypes.TOGGLE_MODAL_VISIBLE,
payload: modalName
});
//Modal Context: {context (context object), modal(name of modal)}
export const setModalContext = modalContext => ({
type: ModalsActionTypes.SET_MODAL_CONTEXT,
payload: modalContext
});

View File

@@ -0,0 +1,40 @@
import ModalsActionTypes from "./modals.types";
const baseModal = {
visible: false,
context: {},
actions: {
refetch: null
}
};
const INITIAL_STATE = {
jobLineEdit: { ...baseModal },
invoiceEnter: { ...baseModal }
};
const modalsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case ModalsActionTypes.TOGGLE_MODAL_VISIBLE:
return {
...state,
[action.payload]: {
...state[action.payload],
visible: !state[action.payload].visible
}
};
case ModalsActionTypes.SET_MODAL_CONTEXT:
return {
...state,
[action.payload.modal]: {
...state[action.payload.modal],
...action.payload.context,
visible: true
}
};
default:
return state;
}
};
export default modalsReducer;

View File

@@ -0,0 +1,24 @@
import { all } from "redux-saga/effects";
// export function* onSendEmail() {
// yield takeLatest(EmailActionTypes.SEND_EMAIL, sendEmail);
// }
// export function* sendEmail(payload) {
// try {
// console.log("Sending thta email", payload);
// axios.post("/sendemail", payload).then(response => {
// console.log(JSON.stringify(response));
// put(sendEmailSuccess());
// });
// } catch (error) {
// console.log("Error in sendEmail saga.");
// yield put(sendEmailFailure(error.message));
// }
// }
export function* modalsSagas() {
yield all([
//call(onSendEmail),
]);
}

View File

@@ -0,0 +1,14 @@
import { createSelector } from "reselect";
const selectModals = state => state.modals;
export const selectJobLineEditModal = createSelector(
[selectModals],
modals => modals.jobLineEdit
);
export const selectInvoiceEnterModal = createSelector(
[selectModals],
modals => modals.invoiceEnter
);

View File

@@ -0,0 +1,5 @@
const ModalActionTypes = {
TOGGLE_MODAL_VISIBLE: "TOGGLE_MODAL_VISIBLE",
SET_MODAL_CONTEXT: "SET_JOBLINEEDIT_CONTEXT"
};
export default ModalActionTypes;

View File

@@ -4,23 +4,20 @@ import storage from "redux-persist/lib/storage";
import userReducer from "./user/user.reducer";
import messagingReducer from "./messaging/messaging.reducer";
// import cartReducer from './cart/cart.reducer';
// import directoryReducer from './directory/directory.reducer';
// import shopReducer from './shop/shop.reducer';
import emailReducer from "./email/email.reducer";
import modalsReducer from './modals/modals.reducer'
const persistConfig = {
key: "root",
storage,
//whitelist: ["cart"]
blacklist: ["user"]
//whitelist: ["user"]
blacklist: ["user", "email", "messaging", "modals"]
};
const rootReducer = combineReducers({
user: userReducer,
messaging: messagingReducer
// cart: cartReducer,
// directory: directoryReducer,
// shop: shopReducer
messaging: messagingReducer,
email: emailReducer,
modals: modalsReducer
});
export default persistReducer(persistConfig, rootReducer);

View File

@@ -1,12 +1,10 @@
import { all, call } from "redux-saga/effects";
//List of all Sagas
// import { shopSagas } from "./shop/shop.sagas";
import { userSagas } from "./user/user.sagas";
import { messagingSagas } from "./messaging/messaging.sagas";
//import { cartSagas } from "./cart/cart.sagas";
import { emailSagas } from "./email/email.sagas";
import { modalsSagas } from "./modals/modals.sagas";
export default function* rootSaga() {
//All starts all the Sagas concurrently.
yield all([call(userSagas), call(messagingSagas)]);
yield all([call(userSagas), call(messagingSagas), call(emailSagas),
call(modalsSagas)]);
}

View File

@@ -1,6 +1,6 @@
import { createStore, applyMiddleware } from "redux";
import { persistStore } from "redux-persist";
import logger from "redux-logger";
import { createLogger } from "redux-logger";
import createSagaMiddleware from "redux-saga";
import rootReducer from "./root.reducer";
import rootSaga from "./root.saga";
@@ -8,7 +8,7 @@ import rootSaga from "./root.saga";
const sagaMiddleWare = createSagaMiddleware();
const middlewares = [sagaMiddleWare];
if (process.env.NODE_ENV === "development") {
middlewares.push(logger);
middlewares.push(createLogger({ collapsed: true, diff: true }));
}
export const store = createStore(rootReducer, applyMiddleware(...middlewares));

View File

@@ -4,8 +4,17 @@
"actions": {
"assign": "Assign"
},
"errors": {
"deleting": "Error encountered while deleting allocation. {{message}}",
"saving": "Error while allocating. {{message}}",
"validation": "Please ensure all fields are entered correctly. "
},
"fields": {
"employee": "Allocated To"
},
"successes": {
"deleted": "Allocation deleted successfully.",
"save": "Allocated successfully. "
}
},
"appointments": {
@@ -67,6 +76,14 @@
"insert": "Uploaded document successfully. "
}
},
"emails": {
"errors": {
"notsent": "Email not sent. Error encountered while sending {{message}}"
},
"successes": {
"sent": "Email sent successfully."
}
},
"employees": {
"actions": {
"new": "New Employee"
@@ -128,14 +145,33 @@
}
},
"joblines": {
"actions": {
"new": "New Line"
},
"errors": {
"creating": "Error encountered while creating job line. {{message}}",
"updating": "Error encountered updating job line. {{message}}"
},
"fields": {
"act_price": "Actual Price",
"db_price": "Database Price",
"line_desc": "Line Description",
"line_ind": "S#",
"mod_lb_hrs": "Labor Hours",
"mod_lbr_ty": "Labor Type",
"oem_partno": "OEM Part #",
"op_code_desc": "Operation Code Description",
"part_type": "Part Type",
"status": "Status",
"unq_seq": "Seq #"
},
"labels": {
"edit": "Edit Line",
"new": "New Line"
},
"successes": {
"created": "Job line created successfully.",
"updated": "Job line updated successfully."
}
},
"jobs": {
@@ -323,6 +359,12 @@
"shops": "My Shops"
}
},
"messaging": {
"labels": {
"messaging": "Messaging",
"typeamessage": "Send a message..."
}
},
"notes": {
"actions": {
"actions": "Actions",
@@ -385,10 +427,13 @@
"creating": "Error encountered when creating parts order. "
},
"fields": {
"deliver_by": "Deliver By"
"deliver_by": "Deliver By",
"lineremarks": "Line Remarks"
},
"labels": {
"email": "Send by Email",
"inthisorder": "Parts in this Order",
"orderhistory": "Order History",
"print": "Show Printed Form"
},
"successes": {

View File

@@ -4,8 +4,17 @@
"actions": {
"assign": "Asignar"
},
"errors": {
"deleting": "",
"saving": "",
"validation": ""
},
"fields": {
"employee": "Asignado a"
},
"successes": {
"deleted": "",
"save": ""
}
},
"appointments": {
@@ -67,6 +76,14 @@
"insert": "Documento cargado con éxito."
}
},
"emails": {
"errors": {
"notsent": "Correo electrónico no enviado Se encontró un error al enviar {{message}}"
},
"successes": {
"sent": "Correo electrónico enviado con éxito."
}
},
"employees": {
"actions": {
"new": "Nuevo empleado"
@@ -128,14 +145,33 @@
}
},
"joblines": {
"actions": {
"new": ""
},
"errors": {
"creating": "",
"updating": ""
},
"fields": {
"act_price": "Precio actual",
"db_price": "Precio de base de datos",
"line_desc": "Descripción de línea",
"line_ind": "S#",
"mod_lb_hrs": "Horas laborales",
"mod_lbr_ty": "Tipo de trabajo",
"oem_partno": "OEM parte #",
"op_code_desc": "",
"part_type": "Tipo de parte",
"status": "Estado",
"unq_seq": "Seq #"
},
"labels": {
"edit": "Línea de edición",
"new": "Nueva línea"
},
"successes": {
"created": "",
"updated": ""
}
},
"jobs": {
@@ -323,6 +359,12 @@
"shops": "Mis tiendas"
}
},
"messaging": {
"labels": {
"messaging": "Mensajería",
"typeamessage": "Enviar un mensaje..."
}
},
"notes": {
"actions": {
"actions": "Comportamiento",
@@ -385,10 +427,13 @@
"creating": "Se encontró un error al crear el pedido de piezas."
},
"fields": {
"deliver_by": "Entregado por"
"deliver_by": "Entregado por",
"lineremarks": "Comentarios de línea"
},
"labels": {
"email": "Enviar por correo electrónico",
"inthisorder": "Partes en este pedido",
"orderhistory": "Historial de pedidos",
"print": "Mostrar formulario impreso"
},
"successes": {

View File

@@ -4,8 +4,17 @@
"actions": {
"assign": "Attribuer"
},
"errors": {
"deleting": "",
"saving": "",
"validation": ""
},
"fields": {
"employee": "Alloué à"
},
"successes": {
"deleted": "",
"save": ""
}
},
"appointments": {
@@ -67,6 +76,14 @@
"insert": "Document téléchargé avec succès."
}
},
"emails": {
"errors": {
"notsent": "Courriel non envoyé. Erreur rencontrée lors de l'envoi de {{message}}"
},
"successes": {
"sent": "E-mail envoyé avec succès."
}
},
"employees": {
"actions": {
"new": "Nouvel employé"
@@ -128,14 +145,33 @@
}
},
"joblines": {
"actions": {
"new": ""
},
"errors": {
"creating": "",
"updating": ""
},
"fields": {
"act_price": "Prix actuel",
"db_price": "Prix de la base de données",
"line_desc": "Description de la ligne",
"line_ind": "S#",
"mod_lb_hrs": "Heures de travail",
"mod_lbr_ty": "Type de travail",
"oem_partno": "Pièce OEM #",
"op_code_desc": "",
"part_type": "Type de pièce",
"status": "Statut",
"unq_seq": "Seq #"
},
"labels": {
"edit": "Ligne d'édition",
"new": "Nouvelle ligne"
},
"successes": {
"created": "",
"updated": ""
}
},
"jobs": {
@@ -323,6 +359,12 @@
"shops": "Mes boutiques"
}
},
"messaging": {
"labels": {
"messaging": "Messagerie",
"typeamessage": "Envoyer un message..."
}
},
"notes": {
"actions": {
"actions": "actes",
@@ -385,10 +427,13 @@
"creating": "Erreur rencontrée lors de la création de la commande de pièces."
},
"fields": {
"deliver_by": "Livrer par"
"deliver_by": "Livrer par",
"lineremarks": "Remarques sur la ligne"
},
"labels": {
"email": "Envoyé par email",
"inthisorder": "Pièces dans cette commande",
"orderhistory": "Historique des commandes",
"print": "Afficher le formulaire imprimé"
},
"successes": {

View File

@@ -2,10 +2,7 @@ export function alphaSort(a, b) {
let A;
let B;
A = a ? a.toLowerCase() : "";
B = b ? b.toLowerCase() : "";
console.log("Objects", A, B, A < B, A > B);
if (A < B)
//sort string ascending
return -1;

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."joblines" DROP COLUMN "status";
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."joblines" ADD COLUMN "status" text NULL;
type: run_sql

View File

@@ -0,0 +1,75 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_insert_permission
- args:
permission:
check:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- jobid
- unq_seq
- line_ind
- line_desc
- part_type
- oem_partno
- est_seq
- db_ref
- line_ref
- tax_part
- db_price
- act_price
- part_qty
- alt_partno
- mod_lbr_ty
- db_hrs
- mod_lb_hrs
- lbr_op
- lbr_amt
- glass_flag
- price_inc
- alt_part_i
- price_j
- cert_part
- alt_co_id
- alt_overrd
- alt_partm
- prt_dsmk_p
- prt_dsmk_m
- lbr_inc
- lbr_hrs_j
- lbr_typ_j
- lbr_op_j
- paint_stg
- paint_tone
- lbr_tax
- misc_amt
- misc_sublt
- misc_tax
- bett_type
- bett_pctg
- bett_amt
- bett_tax
- op_code_desc
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: joblines
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,76 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_insert_permission
- args:
permission:
check:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- jobid
- unq_seq
- line_ind
- line_desc
- part_type
- oem_partno
- est_seq
- db_ref
- line_ref
- tax_part
- db_price
- act_price
- part_qty
- alt_partno
- mod_lbr_ty
- db_hrs
- mod_lb_hrs
- lbr_op
- lbr_amt
- glass_flag
- price_inc
- alt_part_i
- price_j
- cert_part
- alt_co_id
- alt_overrd
- alt_partm
- prt_dsmk_p
- prt_dsmk_m
- lbr_inc
- lbr_hrs_j
- lbr_typ_j
- lbr_op_j
- paint_stg
- paint_tone
- lbr_tax
- misc_amt
- misc_sublt
- misc_tax
- bett_type
- bett_pctg
- bett_amt
- bett_tax
- op_code_desc
- status
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: joblines
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,73 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- alt_overrd
- alt_part_i
- bett_tax
- cert_part
- glass_flag
- lbr_hrs_j
- lbr_inc
- lbr_op_j
- lbr_tax
- lbr_typ_j
- misc_sublt
- misc_tax
- price_inc
- price_j
- tax_part
- est_seq
- paint_stg
- paint_tone
- part_qty
- unq_seq
- act_price
- bett_amt
- bett_pctg
- db_hrs
- db_price
- lbr_amt
- line_ref
- misc_amt
- mod_lb_hrs
- prt_dsmk_m
- prt_dsmk_p
- alt_co_id
- alt_partm
- alt_partno
- bett_type
- db_ref
- lbr_op
- line_desc
- line_ind
- mod_lbr_ty
- oem_partno
- op_code_desc
- part_type
- created_at
- updated_at
- id
- jobid
computed_fields: []
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: joblines
schema: public
type: create_select_permission

View File

@@ -0,0 +1,74 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- alt_overrd
- alt_part_i
- bett_tax
- cert_part
- glass_flag
- lbr_hrs_j
- lbr_inc
- lbr_op_j
- lbr_tax
- lbr_typ_j
- misc_sublt
- misc_tax
- price_inc
- price_j
- tax_part
- est_seq
- paint_stg
- paint_tone
- part_qty
- unq_seq
- act_price
- bett_amt
- bett_pctg
- db_hrs
- db_price
- lbr_amt
- line_ref
- misc_amt
- mod_lb_hrs
- prt_dsmk_m
- prt_dsmk_p
- alt_co_id
- alt_partm
- alt_partno
- bett_type
- db_ref
- lbr_op
- line_desc
- line_ind
- mod_lbr_ty
- oem_partno
- op_code_desc
- part_type
- status
- created_at
- updated_at
- id
- jobid
computed_fields: []
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: joblines
schema: public
type: create_select_permission

View File

@@ -0,0 +1,75 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_update_permission
- args:
permission:
columns:
- alt_overrd
- alt_part_i
- bett_tax
- cert_part
- glass_flag
- lbr_hrs_j
- lbr_inc
- lbr_op_j
- lbr_tax
- lbr_typ_j
- misc_sublt
- misc_tax
- price_inc
- price_j
- tax_part
- est_seq
- paint_stg
- paint_tone
- part_qty
- unq_seq
- act_price
- bett_amt
- bett_pctg
- db_hrs
- db_price
- lbr_amt
- line_ref
- misc_amt
- mod_lb_hrs
- prt_dsmk_m
- prt_dsmk_p
- alt_co_id
- alt_partm
- alt_partno
- bett_type
- db_ref
- lbr_op
- line_desc
- line_ind
- mod_lbr_ty
- oem_partno
- op_code_desc
- part_type
- created_at
- updated_at
- id
- jobid
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: joblines
schema: public
type: create_update_permission

View File

@@ -0,0 +1,76 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_update_permission
- args:
permission:
columns:
- alt_overrd
- alt_part_i
- bett_tax
- cert_part
- glass_flag
- lbr_hrs_j
- lbr_inc
- lbr_op_j
- lbr_tax
- lbr_typ_j
- misc_sublt
- misc_tax
- price_inc
- price_j
- tax_part
- est_seq
- paint_stg
- paint_tone
- part_qty
- unq_seq
- act_price
- bett_amt
- bett_pctg
- db_hrs
- db_price
- lbr_amt
- line_ref
- misc_amt
- mod_lb_hrs
- prt_dsmk_m
- prt_dsmk_p
- alt_co_id
- alt_partm
- alt_partno
- bett_type
- db_ref
- lbr_op
- line_desc
- line_ind
- mod_lbr_ty
- oem_partno
- op_code_desc
- part_type
- status
- created_at
- updated_at
- id
- jobid
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: joblines
schema: public
type: create_update_permission

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: DROP TABLE "public"."invoices";
type: run_sql

View File

@@ -0,0 +1,28 @@
- args:
cascade: false
read_only: false
sql: CREATE EXTENSION IF NOT EXISTS pgcrypto;
type: run_sql
- args:
cascade: false
read_only: false
sql: "CREATE TABLE \"public\".\"invoices\"(\"id\" uuid NOT NULL DEFAULT gen_random_uuid(),
\"created_at\" timestamptz NOT NULL DEFAULT now(), \"updated_at\" timestamptz
NOT NULL DEFAULT now(), \"vendorid\" uuid NOT NULL, \"jobid\" uuid NOT NULL,
\"date\" date NOT NULL DEFAULT now(), \"due_date\" date, \"exported\" boolean
NOT NULL DEFAULT false, \"exported_at\" timestamptz, \"is_credit_memo\" boolean
NOT NULL DEFAULT false, \"total\" numeric NOT NULL DEFAULT 0, \"invoice_number\"
text NOT NULL, PRIMARY KEY (\"id\") , FOREIGN KEY (\"jobid\") REFERENCES \"public\".\"jobs\"(\"id\")
ON UPDATE restrict ON DELETE cascade, FOREIGN KEY (\"vendorid\") REFERENCES
\"public\".\"vendors\"(\"id\") ON UPDATE restrict ON DELETE restrict, UNIQUE
(\"jobid\"));\nCREATE OR REPLACE FUNCTION \"public\".\"set_current_timestamp_updated_at\"()\nRETURNS
TRIGGER AS $$\nDECLARE\n _new record;\nBEGIN\n _new := NEW;\n _new.\"updated_at\"
= NOW();\n RETURN _new;\nEND;\n$$ LANGUAGE plpgsql;\nCREATE TRIGGER \"set_public_invoices_updated_at\"\nBEFORE
UPDATE ON \"public\".\"invoices\"\nFOR EACH ROW\nEXECUTE PROCEDURE \"public\".\"set_current_timestamp_updated_at\"();\nCOMMENT
ON TRIGGER \"set_public_invoices_updated_at\" ON \"public\".\"invoices\" \nIS
'trigger to set value of column \"updated_at\" to current timestamp on row update';"
type: run_sql
- args:
name: invoices
schema: public
type: add_existing_table_or_view

View File

@@ -0,0 +1,24 @@
- args:
relationship: job
table:
name: invoices
schema: public
type: drop_relationship
- args:
relationship: vendor
table:
name: invoices
schema: public
type: drop_relationship
- args:
relationship: invoice
table:
name: jobs
schema: public
type: drop_relationship
- args:
relationship: invoices
table:
name: vendors
schema: public
type: drop_relationship

View File

@@ -0,0 +1,41 @@
- args:
name: job
table:
name: invoices
schema: public
using:
foreign_key_constraint_on: jobid
type: create_object_relationship
- args:
name: vendor
table:
name: invoices
schema: public
using:
foreign_key_constraint_on: vendorid
type: create_object_relationship
- args:
name: invoice
table:
name: jobs
schema: public
using:
manual_configuration:
column_mapping:
id: jobid
remote_table:
name: invoices
schema: public
type: create_object_relationship
- args:
name: invoices
table:
name: vendors
schema: public
using:
foreign_key_constraint_on:
column: vendorid
table:
name: invoices
schema: public
type: create_array_relationship

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: invoices
schema: public
type: drop_insert_permission

View File

@@ -0,0 +1,35 @@
- args:
permission:
allow_upsert: true
check:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- vendorid
- jobid
- date
- due_date
- exported
- exported_at
- is_credit_memo
- total
- invoice_number
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: invoices
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: invoices
schema: public
type: drop_select_permission

View File

@@ -0,0 +1,33 @@
- args:
permission:
allow_aggregations: false
columns:
- exported
- is_credit_memo
- date
- due_date
- total
- invoice_number
- created_at
- exported_at
- updated_at
- id
- jobid
- vendorid
computed_fields: []
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
limit: null
role: user
table:
name: invoices
schema: public
type: create_select_permission

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: invoices
schema: public
type: drop_update_permission

View File

@@ -0,0 +1,34 @@
- args:
permission:
columns:
- exported
- is_credit_memo
- date
- due_date
- total
- invoice_number
- created_at
- exported_at
- updated_at
- id
- jobid
- vendorid
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: invoices
schema: public
type: create_update_permission

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: DROP TABLE "public"."invoicelines";
type: run_sql

View File

@@ -0,0 +1,26 @@
- args:
cascade: false
read_only: false
sql: CREATE EXTENSION IF NOT EXISTS pgcrypto;
type: run_sql
- args:
cascade: false
read_only: false
sql: "CREATE TABLE \"public\".\"invoicelines\"(\"id\" uuid NOT NULL DEFAULT gen_random_uuid(),
\"created_at\" timestamptz NOT NULL DEFAULT now(), \"updated_at\" timestamptz
NOT NULL DEFAULT now(), \"invoiceid\" uuid NOT NULL, \"line_desc\" text, \"actual_price\"
numeric NOT NULL DEFAULT 0, \"actual_cost\" numeric NOT NULL DEFAULT 0, \"cost_center\"
text NOT NULL, \"estlindid\" uuid, PRIMARY KEY (\"id\") , FOREIGN KEY (\"invoiceid\")
REFERENCES \"public\".\"invoices\"(\"id\") ON UPDATE restrict ON DELETE cascade);\nCREATE
OR REPLACE FUNCTION \"public\".\"set_current_timestamp_updated_at\"()\nRETURNS
TRIGGER AS $$\nDECLARE\n _new record;\nBEGIN\n _new := NEW;\n _new.\"updated_at\"
= NOW();\n RETURN _new;\nEND;\n$$ LANGUAGE plpgsql;\nCREATE TRIGGER \"set_public_invoicelines_updated_at\"\nBEFORE
UPDATE ON \"public\".\"invoicelines\"\nFOR EACH ROW\nEXECUTE PROCEDURE \"public\".\"set_current_timestamp_updated_at\"();\nCOMMENT
ON TRIGGER \"set_public_invoicelines_updated_at\" ON \"public\".\"invoicelines\"
\nIS 'trigger to set value of column \"updated_at\" to current timestamp on
row update';"
type: run_sql
- args:
name: invoicelines
schema: public
type: add_existing_table_or_view

View File

@@ -0,0 +1,12 @@
- args:
relationship: invoice
table:
name: invoicelines
schema: public
type: drop_relationship
- args:
relationship: invoicelines
table:
name: invoices
schema: public
type: drop_relationship

View File

@@ -0,0 +1,20 @@
- args:
name: invoice
table:
name: invoicelines
schema: public
using:
foreign_key_constraint_on: invoiceid
type: create_object_relationship
- args:
name: invoicelines
table:
name: invoices
schema: public
using:
foreign_key_constraint_on:
column: invoiceid
table:
name: invoicelines
schema: public
type: create_array_relationship

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: invoicelines
schema: public
type: drop_insert_permission

View File

@@ -0,0 +1,33 @@
- args:
permission:
allow_upsert: true
check:
invoice:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- invoiceid
- line_desc
- actual_price
- actual_cost
- cost_center
- estlindid
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: invoicelines
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: invoicelines
schema: public
type: drop_select_permission

View File

@@ -0,0 +1,31 @@
- args:
permission:
allow_aggregations: false
columns:
- actual_cost
- actual_price
- cost_center
- line_desc
- created_at
- updated_at
- estlindid
- id
- invoiceid
computed_fields: []
filter:
invoice:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
limit: null
role: user
table:
name: invoicelines
schema: public
type: create_select_permission

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: invoicelines
schema: public
type: drop_update_permission

View File

@@ -0,0 +1,32 @@
- args:
permission:
columns:
- actual_cost
- actual_price
- cost_center
- line_desc
- created_at
- updated_at
- estlindid
- id
- invoiceid
filter:
invoice:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: invoicelines
schema: public
type: create_update_permission

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: invoicelines
schema: public
type: drop_delete_permission

View File

@@ -0,0 +1,18 @@
- args:
permission:
filter:
invoice:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: invoicelines
schema: public
type: create_delete_permission

View File

@@ -31,6 +31,6 @@
"concurrently": "^4.0.1",
"eslint": "^6.7.2",
"eslint-plugin-promise": "^4.2.1",
"hasura-cli": "^1.0.0-beta.10"
"hasura-cli": "^1.1.0"
}
}

View File

@@ -1950,10 +1950,10 @@ has-flag@^3.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
hasura-cli@^1.0.0-beta.10:
version "1.0.0-rc.1"
resolved "https://registry.yarnpkg.com/hasura-cli/-/hasura-cli-1.0.0-rc.1.tgz#481453f88e7624f468f329c75a88fbbde3407f00"
integrity sha512-w6DGAhJZ6l7U89SD6QIxYetP3/dDxJc4jEVzjMAYsueeYWQKDeGgtZBFBW1sdgAlUtRmtIa5wbiFLXuagB6JqA==
hasura-cli@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/hasura-cli/-/hasura-cli-1.1.0.tgz#a095e94c654d30354d8979602b8c0047c7b8a9e1"
integrity sha512-D1qXoYydx9Mgq7VQdCmOOvTlYhd1RcjQtn4s7pN1wb5w1ORIcDFLm1rS3w97bsx7wPRotIl0reyhc3+FDq+FFg==
dependencies:
axios "^0.19.0"
chalk "^2.4.2"