BOD-23 Added schema changes for time tickets + redux config for time ticket modal + scaffolding for time ticket modal.

This commit is contained in:
Patrick Fic
2020-04-14 17:38:05 -07:00
parent 91af10eef2
commit 325a82ac86
27 changed files with 829 additions and 9 deletions

View File

@@ -10863,6 +10863,126 @@
</folder_node>
</children>
</folder_node>
<folder_node>
<name>timetickets</name>
<children>
<folder_node>
<name>fields</name>
<children>
<concept_node>
<name>employee</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>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>flat_rate</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>
<concept_node>
<name>straight_time</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>titles</name>
<children>

View File

@@ -0,0 +1,54 @@
import { Select, Tag } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
const { Option } = Select;
//To be used as a form element only.
const EmployeeSearchSelect = ({ value, onChange, options, onSelect }) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
useEffect(() => {
if (onChange) {
onChange(option);
}
}, [option, onChange]);
return (
<Select
showSearch
value={option}
style={{
width: 400,
}}
onChange={setOption}
optionFilterProp="search"
onSelect={onSelect}
>
{options
? options.map((o) => (
<Option
key={o.id}
value={o.id}
search={`${o.employee_number} ${o.first_name} ${o.last_name}`}
discount={o.discount}
>
<div style={{ display: "flex" }}>
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
<Tag color="blue">{o.cost_center}</Tag>
<Tag color="red">
<CurrencyFormatter>{o.base_rate}</CurrencyFormatter>
</Tag>
<Tag color="red">
{o.flat_rate
? t("timetickets.labels.flat_rate")
: t("timetickets.labels.straight_time")}
</Tag>
</div>
</Option>
))
: null}
</Select>
);
};
export default EmployeeSearchSelect;

View File

@@ -0,0 +1,45 @@
import React from "react";
import { Button } from "antd";
import { connect } from "react-redux";
import { setModalContext } from "../../redux/modals/modals.actions";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import { useQuery } from "@apollo/react-hooks";
import { QUERY_TICKETS_BY_JOBID } from "../../graphql/timetickets.queries";
const mapDispatchToProps = (dispatch) => ({
setTimeTicketContext: (context) =>
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
});
export function JobsDetailLaborContainer({ jobId, setTimeTicketContext }) {
const { loading, error, data, refetch } = useQuery(QUERY_TICKETS_BY_JOBID, {
variables: { jobid: jobId },
skip: !!!jobId,
});
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
{jobId}
<Button
onClick={() => {
setTimeTicketContext({
actions: { refetch },
context: {
jobId,
},
});
}}
>
TEST - Open Time Ticket Modal
</Button>
{JSON.stringify(data)}
</div>
);
}
export default connect(null, mapDispatchToProps)(JobsDetailLaborContainer);

View File

@@ -0,0 +1,109 @@
import { DatePicker, Form, Input, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component";
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
export default function TimeTicketModalComponent({
form,
roAutoCompleteOptions,
employeeAutoCompleteOptions,
loadJobLines,
responsibilityCenters,
}) {
const { t } = useTranslation();
return (
<div>
<div style={{ display: "flex" }}>
<Form.Item
name="jobid"
label={t("invoices.fields.ro_number")}
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
options={roAutoCompleteOptions}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadJobLines({
variables: { id: form.getFieldValue("jobid") },
});
}
}}
/>
</Form.Item>
<Form.Item
name="employeeid"
label={t("timetickets.fields.employee")}
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<EmployeeSearchSelect options={employeeAutoCompleteOptions} />
</Form.Item>
</div>
<div style={{ display: "flex" }}>
<Form.Item
label={t("invoices.fields.invoice_number")}
name="invoice_number"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("invoices.fields.date")}
name="date"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t("invoices.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label={t("invoices.fields.total")}
name="total"
rules={[
{
required: true,
message: t("general.validation.required"),
},
]}
>
<CurrencyInput />
</Form.Item>
</div>
<button
onClick={() => {
console.log(form.getFieldsValue());
}}
>
Get Them Values
</button>
</div>
);
}

View File

@@ -0,0 +1,150 @@
import { useLazyQuery, useMutation, useQuery } from "@apollo/react-hooks";
import { Form, Modal, notification, Button } from "antd";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_NEW_INVOICE } from "../../graphql/invoices.queries";
import { ACTIVE_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectTimeTicket } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import TimeTicketModalComponent from "./time-ticket-modal.component";
import { QUERY_EMPLOYEES } from "../../graphql/employees.queries";
import { GET_JOB_LINES_BY_PK_MINIMAL } from "../../graphql/jobs-lines.queries";
const mapStateToProps = createStructuredSelector({
timeTicketModal: selectTimeTicket,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("timeTicket")),
});
function TimeTicketModalContainer({
timeTicketModal,
toggleModalVisible,
bodyshop,
}) {
const [form] = Form.useForm();
const { t } = useTranslation();
const [enterAgain, setEnterAgain] = useState(false);
// const [insertInvoice] = useMutation(INSERT_NEW_INVOICE);
const { data: RoAutoCompleteData } = useQuery(ACTIVE_JOBS_FOR_AUTOCOMPLETE, {
variables: { statuses: bodyshop.md_ro_statuses.open_statuses || ["Open"] },
skip: !timeTicketModal.visible,
});
const { data: EmployeeAutoCompleteData } = useQuery(QUERY_EMPLOYEES, {
skip: !timeTicketModal.visible,
});
const [loadJobLines, { data: jobLinesData }] = useLazyQuery(
GET_JOB_LINES_BY_PK_MINIMAL
);
const handleFinish = (values) => {
// insertInvoice({
// variables: {
// invoice: [
// Object.assign({}, values, {
// invoicelines: { data: values.invoicelines },
// }),
// ],
// },
// })
// .then((r) => {
// notification["success"]({
// message: t("invoices.successes.created"),
// });
// if (timeTicketModal.actions.refetch)
// timeTicketModal.actions.refetch();
// if (enterAgain) {
// form.resetFields();
// } else {
// toggleModalVisible();
// }
// setEnterAgain(false);
// })
// .catch((error) => {
// setEnterAgain(false);
// notification["error"]({
// message: t("invoices.errors.creating", {
// message: JSON.stringify(error),
// }),
// });
// });
};
const handleCancel = () => {
toggleModalVisible();
};
useEffect(() => {
if (enterAgain) form.submit();
}, [enterAgain, form]);
return (
<Modal
title={
timeTicketModal.context && timeTicketModal.context.id
? t("timetickets.labels.edit")
: t("timetickets.labels.new")
}
width={"90%"}
visible={timeTicketModal.visible}
okText={t("general.actions.save")}
onOk={() => form.submit()}
onCancel={handleCancel}
afterClose={() => form.resetFields()}
footer={
<span>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button onClick={() => form.submit()}>
{t("general.actions.save")}
</Button>
{timeTicketModal.context && timeTicketModal.context.id ? null : (
<Button
type="primary"
onClick={() => {
setEnterAgain(true);
}}
>
{t("general.actions.saveandnew")}
</Button>
)}
</span>
}
destroyOnClose
>
<Form
onFinish={handleFinish}
autoComplete={"off"}
form={form}
onFinishFailed={() => {
setEnterAgain(false);
console.log("Finish failed");
}}
initialValues={{
jobid: timeTicketModal.context.jobId || null,
}}
>
<TimeTicketModalComponent
form={form}
roAutoCompleteOptions={RoAutoCompleteData && RoAutoCompleteData.jobs}
employeeAutoCompleteOptions={
EmployeeAutoCompleteData && EmployeeAutoCompleteData.employees
}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
loadJobLines={loadJobLines}
/>
</Form>
{JSON.stringify(jobLinesData)}
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TimeTicketModalContainer);

View File

@@ -45,6 +45,26 @@ export const GET_JOB_LINES_BY_PK = gql`
}
`;
export const GET_JOB_LINES_BY_PK_MINIMAL = gql`
query GET_JOB_LINES_BY_PK_MINIMAL($id: uuid!) {
joblines(where: { jobid: { _eq: $id } }, order_by: { unq_seq: asc }) {
id
line_desc
part_type
oem_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
}
}
`;
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 }) {

View File

@@ -0,0 +1,32 @@
import { gql } from "apollo-boost";
export const QUERY_TICKETS_BY_JOBID = gql`
query QUERY_TICKETS_BY_JOBID($jobid: uuid!) {
timetickets(where: { jobid: { _eq: $jobid } }) {
actualhrs
cost_center
ciecacode
rate
productivehrs
id
employee {
employee_number
first_name
last_name
id
}
}
}
`;
export const INSERT_NEW_TIME_TICKET = gql`
mutation INSERT_NEW_TIME_TICKET(
$timeTicketInput: [timetickets_insert_input!]!
) {
insert_timetickets(objects: $timeTicketInput) {
returning {
id
}
}
}
`;

View File

@@ -46,7 +46,7 @@ export function ContractDetailPageContainer({ setBreadcrumbs }) {
{
link: "/manage/courtesycars/contracts/new",
label: t("titles.bc.contracts-detail", {
number: data.cccontracts_by_pk.agreementnumber || "",
number: (data && data.cccontracts_by_pk.agreementnumber) || "",
}),
},
]);

View File

@@ -69,6 +69,9 @@ const JobsDetailPliContainer = lazy(() =>
const JobsDetailAuditContainer = lazy(() =>
import("../../components/audit-trail-list/audit-trail-list.container")
);
const JobsDetailLaborContainer = lazy(() =>
import("../../components/jobs-detail-labor/jobs-detail-labor.container")
);
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -237,7 +240,7 @@ export function JobsDetailPage({
}
key="labor"
>
Labor
<JobsDetailLaborContainer jobId={job.id} />
</Tabs.TabPane>
<Tabs.TabPane
tab={

View File

@@ -74,6 +74,9 @@ const InvoiceDetailPage = lazy(() =>
const EnterInvoiceModalContainer = lazy(() =>
import("../../components/invoice-enter-modal/invoice-enter-modal.container")
);
const TimeTicketModalContainer = lazy(() =>
import("../../components/time-ticket-modal/time-ticket-modal.container")
);
const { Header, Content, Footer } = Layout;
export default function Manage({ match }) {
@@ -102,6 +105,7 @@ export default function Manage({ match }) {
<BreadCrumbs />
<EnterInvoiceModalContainer />
<EmailOverlayContainer />
<TimeTicketModalContainer />
<Route exact path={`${match.path}`} component={ManageRootPage} />
<Route exact path={`${match.path}/jobs`} component={JobsPage} />
<Switch>

View File

@@ -4,8 +4,8 @@ const baseModal = {
visible: false,
context: {},
actions: {
refetch: null
}
refetch: null,
},
};
const INITIAL_STATE = {
@@ -14,7 +14,8 @@ const INITIAL_STATE = {
courtesyCarReturn: { ...baseModal },
noteUpsert: { ...baseModal },
schedule: { ...baseModal },
partsOrder: { ...baseModal }
partsOrder: { ...baseModal },
timeTicket: { ...baseModal },
};
const modalsReducer = (state = INITIAL_STATE, action) => {
@@ -24,8 +25,8 @@ const modalsReducer = (state = INITIAL_STATE, action) => {
...state,
[action.payload]: {
...state[action.payload],
visible: !state[action.payload].visible
}
visible: !state[action.payload].visible,
},
};
case ModalsActionTypes.SET_MODAL_CONTEXT:
return {
@@ -33,8 +34,8 @@ const modalsReducer = (state = INITIAL_STATE, action) => {
[action.payload.modal]: {
...state[action.payload.modal],
...action.payload.context,
visible: true
}
visible: true,
},
};
default:
return state;

View File

@@ -31,3 +31,8 @@ export const selectPartsOrder = createSelector(
[selectModals],
modals => modals.partsOrder
);
export const selectTimeTicket = createSelector(
[selectModals],
modals => modals.timeTicket
);

View File

@@ -712,6 +712,17 @@
"state": "Error reading page state. Please refresh."
}
},
"timetickets": {
"fields": {
"employee": "Employee"
},
"labels": {
"edit": "Edit Time Ticket",
"flat_rate": "Flat Rate",
"new": "New Time Ticket",
"straight_time": "Straight Time"
}
},
"titles": {
"app": "Bodyshop by ImEX Systems",
"bc": {

View File

@@ -712,6 +712,17 @@
"state": "Error al leer el estado de la página. Porfavor refresca."
}
},
"timetickets": {
"fields": {
"employee": ""
},
"labels": {
"edit": "",
"flat_rate": "",
"new": "",
"straight_time": ""
}
},
"titles": {
"app": "Carrocería de ImEX Systems",
"bc": {

View File

@@ -712,6 +712,17 @@
"state": "Erreur lors de la lecture de l'état de la page. Rafraichissez, s'il vous plait."
}
},
"timetickets": {
"fields": {
"employee": ""
},
"labels": {
"edit": "",
"flat_rate": "",
"new": "",
"straight_time": ""
}
},
"titles": {
"app": "Carrosserie par ImEX Systems",
"bc": {

View File

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

View File

@@ -0,0 +1,29 @@
- 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\".\"timetickets\"(\"id\" uuid NOT NULL DEFAULT gen_random_uuid(),
\"created_at\" timestamptz NOT NULL DEFAULT now(), \"updated_at\" timestamptz
NOT NULL DEFAULT now(), \"date\" date NOT NULL DEFAULT now(), \"cost_center\"
text NOT NULL, \"employeeid\" uuid NOT NULL, \"jobid\" uuid NOT NULL, \"rate\"
numeric NOT NULL DEFAULT 0, \"productivehrs\" numeric NOT NULL DEFAULT 0, \"actualhrs\"
numeric NOT NULL DEFAULT 0, \"clockon\" timestamptz, \"clockoff\" timestamptz,
\"ciecacode\" text NOT NULL, PRIMARY KEY (\"id\") , FOREIGN KEY (\"employeeid\")
REFERENCES \"public\".\"employees\"(\"id\") ON UPDATE restrict ON DELETE restrict,
FOREIGN KEY (\"jobid\") REFERENCES \"public\".\"jobs\"(\"id\") ON UPDATE restrict
ON DELETE restrict);\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_timetickets_updated_at\"\nBEFORE
UPDATE ON \"public\".\"timetickets\"\nFOR EACH ROW\nEXECUTE PROCEDURE \"public\".\"set_current_timestamp_updated_at\"();\nCOMMENT
ON TRIGGER \"set_public_timetickets_updated_at\" ON \"public\".\"timetickets\"
\nIS 'trigger to set value of column \"updated_at\" to current timestamp on
row update';"
type: run_sql
- args:
name: timetickets
schema: public
type: add_existing_table_or_view

View File

@@ -0,0 +1,24 @@
- args:
relationship: timetickets
table:
name: employees
schema: public
type: drop_relationship
- args:
relationship: timetickets
table:
name: jobs
schema: public
type: drop_relationship
- args:
relationship: job
table:
name: timetickets
schema: public
type: drop_relationship
- args:
relationship: employee
table:
name: timetickets
schema: public
type: drop_relationship

View File

@@ -0,0 +1,40 @@
- args:
name: timetickets
table:
name: employees
schema: public
using:
foreign_key_constraint_on:
column: employeeid
table:
name: timetickets
schema: public
type: create_array_relationship
- args:
name: timetickets
table:
name: jobs
schema: public
using:
foreign_key_constraint_on:
column: jobid
table:
name: timetickets
schema: public
type: create_array_relationship
- args:
name: job
table:
name: timetickets
schema: public
using:
foreign_key_constraint_on: jobid
type: create_object_relationship
- args:
name: employee
table:
name: timetickets
schema: public
using:
foreign_key_constraint_on: employeeid
type: create_object_relationship

View File

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

View File

@@ -0,0 +1,36 @@
- 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
- date
- cost_center
- employeeid
- jobid
- rate
- productivehrs
- actualhrs
- clockon
- clockoff
- ciecacode
localPresets:
- key: ""
value: ""
set: {}
role: user
table:
name: timetickets
schema: public
type: create_insert_permission

View File

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

View File

@@ -0,0 +1,34 @@
- args:
permission:
allow_aggregations: false
columns:
- date
- actualhrs
- productivehrs
- rate
- ciecacode
- cost_center
- clockoff
- clockon
- created_at
- updated_at
- employeeid
- id
- jobid
computed_fields: []
filter:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
limit: null
role: user
table:
name: timetickets
schema: public
type: create_select_permission

View File

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

View File

@@ -0,0 +1,35 @@
- args:
permission:
columns:
- date
- actualhrs
- productivehrs
- rate
- ciecacode
- cost_center
- clockoff
- clockon
- created_at
- updated_at
- employeeid
- 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: timetickets
schema: public
type: create_update_permission

View File

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

View File

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