From 50926547b31282cd7690970dc266fb6693aad24f Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Wed, 5 Jan 2022 11:45:37 -0800 Subject: [PATCH] IO-1612 Add employee vacation tracking. --- bodyshop_translations.babel | 110 +++++++++++ .../schedule-calendar.container.jsx | 40 +++- .../schedule-day-view.container.jsx | 34 +++- .../schedule-job-modal.component.jsx | 1 - .../shop-employees-add-vacation.component.jsx | 124 +++++++++++++ .../shop-employees-form.component.jsx | 172 +++++++++++++++--- .../shop-employees-list.component.jsx | 41 ++--- .../shop-employees.component.jsx | 29 --- .../shop-employees.container.jsx | 114 ++---------- client/src/graphql/appointments.queries.js | 33 +++- client/src/graphql/employees.queries.js | 44 +++++ client/src/translations/en_us/common.json | 9 +- client/src/translations/es/common.json | 9 +- client/src/translations/fr/common.json | 9 +- hasura/metadata/tables.yaml | 89 +++++++++ .../down.sql | 1 + .../up.sql | 18 ++ 17 files changed, 678 insertions(+), 199 deletions(-) create mode 100644 client/src/components/shop-employees/shop-employees-add-vacation.component.jsx delete mode 100644 client/src/components/shop-employees/shop-employees.component.jsx create mode 100644 hasura/migrations/1641339745427_create_table_public_employee_vacation/down.sql create mode 100644 hasura/migrations/1641339745427_create_table_public_employee_vacation/up.sql diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index d6c6d4c7f..b142996b8 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -13183,6 +13183,27 @@ actions + + addvacation + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + new false @@ -13571,6 +13592,74 @@ + + vacation + + + end + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + length + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + start + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + @@ -13597,6 +13686,27 @@ + + endmustbeafterstart + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + flat_rate false diff --git a/client/src/components/schedule-calendar/schedule-calendar.container.jsx b/client/src/components/schedule-calendar/schedule-calendar.container.jsx index 04a89d99a..0d0178207 100644 --- a/client/src/components/schedule-calendar/schedule-calendar.container.jsx +++ b/client/src/components/schedule-calendar/schedule-calendar.container.jsx @@ -10,6 +10,7 @@ import ScheduleCalendarComponent from "./schedule-calendar.component"; import { calculateScheduleLoad } from "../../redux/application/application.actions"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import moment from "moment"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser }); @@ -26,7 +27,12 @@ export function ScheduleCalendarContainer({ calculateScheduleLoad }) { const { loading, error, data, refetch } = useQuery( QUERY_ALL_ACTIVE_APPOINTMENTS, { - variables: { start: range.start.toDate(), end: range.end.toDate() }, + variables: { + start: range.start.toDate(), + end: range.end.toDate(), + startd: range.start, + endd: range.end, + }, skip: !!!range.start || !!!range.end, fetchPolicy: "network-only", nextFetchPolicy: "network-only", @@ -39,15 +45,29 @@ export function ScheduleCalendarContainer({ calculateScheduleLoad }) { if (loading) return ; if (error) return ; - let normalizedData = data.appointments.map((e) => { - //Required becuase Hasura returns a string instead of a date object. - return Object.assign( - {}, - e, - { start: new Date(e.start) }, - { end: new Date(e.end) } - ); - }); + let normalizedData = [ + ...data.appointments.map((e) => { + //Required becuase Hasura returns a string instead of a date object. + return Object.assign( + {}, + e, + { start: new Date(e.start) }, + { end: new Date(e.end) } + ); + }), + ...data.employee_vacation.map((e) => { + //Required becuase Hasura returns a string instead of a date object. + return { + ...e, + title: `${ + (e.employee.first_name && e.employee.first_name.substr(0, 1)) || "" + } ${e.employee.last_name || ""} OUT`, + color: "red", + start: moment(e.start).startOf("day").toDate(), + end: moment(e.end).startOf("day").toDate(), + }; + }), + ]; return ( { - //Required becuase Hasura returns a string instead of a date object. - return Object.assign( - {}, - e, - { start: new Date(e.start) }, - { end: new Date(e.end) } - ); - }); + normalizedData = [ + ...data.appointments.map((e) => { + //Required becuase Hasura returns a string instead of a date object. + return Object.assign( + {}, + e, + { start: new Date(e.start) }, + { end: new Date(e.end) } + ); + }), + ...data.employee_vacation.map((e) => { + //Required becuase Hasura returns a string instead of a date object. + return { + ...e, + title: `${ + (e.employee.first_name && e.employee.first_name.substr(0, 1)) || "" + } ${e.employee.last_name || ""} OUT`, + color: "red", + start: moment(e.start).startOf("day").toDate(), + end: moment(e.end).startOf("day").toDate(), + }; + }), + ]; } return ( diff --git a/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx b/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx index b31682f89..121291c89 100644 --- a/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx +++ b/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx @@ -190,7 +190,6 @@ export function ScheduleJobModalComponent({ prev.start !== cur.start}> {() => { - console.log("render"); const values = form.getFieldsValue(); return (
diff --git a/client/src/components/shop-employees/shop-employees-add-vacation.component.jsx b/client/src/components/shop-employees/shop-employees-add-vacation.component.jsx new file mode 100644 index 000000000..d12bfcc03 --- /dev/null +++ b/client/src/components/shop-employees/shop-employees-add-vacation.component.jsx @@ -0,0 +1,124 @@ +import { useMutation } from "@apollo/client"; +import { Button, Card, Form, notification, Popover, Space } from "antd"; +import moment from "moment"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { INSERT_VACATION } from "../../graphql/employees.queries"; +import FormDatePicker from "../form-date-picker/form-date-picker.component"; + +export default function ShopEmployeeAddVacation({ employee }) { + const { t } = useTranslation(); + const [insertVacation] = useMutation(INSERT_VACATION); + + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + const [visibility, setVisibility] = useState(false); + + const handleFinish = async (values) => { + logImEXEvent("employee_add_vacation"); + + setLoading(true); + let result; + + result = await insertVacation({ + variables: { vacation: { ...values, employeeid: employee.id } }, + update(cache, { data }) { + cache.modify({ + id: cache.identify({ id: employee.id, __typename: "employees" }), + fields: { + employee_vacations(ex) { + return [data.insert_employee_vacation_one, ...ex]; + }, + }, + }); + }, + }); + + if (!!result.errors) { + notification["error"]({ + message: t("employees.errors.adding", { + message: JSON.stringify(result.errors), + }), + }); + } else { + notification["success"]({ + message: t("employees.successes.added"), + }); + } + setLoading(false); + setVisibility(false); + }; + + const overlay = ( + +
+ + + + ({ + async validator(rule, value) { + if (value) { + const { start } = form.getFieldsValue(); + if (moment(start).isAfter(moment(value))) { + return Promise.reject( + t("employees.labels.endmustbeafterstart") + ); + } else { + return Promise.resolve(); + } + } else { + return Promise.resolve(); + } + }, + }), + ]} + > + + + + + + + +
+
+ ); + + const handleClick = (e) => { + setVisibility(true); + }; + + return ( + + + + ); +} diff --git a/client/src/components/shop-employees/shop-employees-form.component.jsx b/client/src/components/shop-employees/shop-employees-form.component.jsx index acfadbcde..2282d62ef 100644 --- a/client/src/components/shop-employees/shop-employees-form.component.jsx +++ b/client/src/components/shop-employees/shop-employees-form.component.jsx @@ -1,20 +1,41 @@ import { DeleteFilled } from "@ant-design/icons"; -import { Button, Card, Form, Input, InputNumber, Select, Switch } from "antd"; +import { useApolloClient, useMutation, useQuery } from "@apollo/client"; +import { + Button, + Card, + Form, + Input, + InputNumber, + notification, + Select, + Switch, + Table, +} from "antd"; +import { useForm } from "antd/es/form/Form"; import moment from "moment"; +import querystring from "query-string"; import React, { useEffect } from "react"; -import { useApolloClient } from "@apollo/client"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; +import { useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.utils"; import { CHECK_EMPLOYEE_NUMBER, + DELETE_VACATION, + INSERT_EMPLOYEES, + QUERY_EMPLOYEE_BY_ID, QUERY_USERS_BY_EMAIL, + UPDATE_EMPLOYEE, } from "../../graphql/employees.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import CiecaSelect from "../../utils/Ciecaselect"; +import { DateFormatter } from "../../utils/DateFormatter"; +import AlertComponent from "../alert/alert.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; -import CiecaSelect from "../../utils/Ciecaselect"; +import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -23,20 +44,129 @@ const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export function ShopEmployeesFormComponent({ - bodyshop, - form, - selectedEmployee, - handleFinish, -}) { +export function ShopEmployeesFormComponent({ bodyshop }) { const { t } = useTranslation(); + const [form] = useForm(); + const history = useHistory(); + const search = querystring.parse(useLocation().search); + const [deleteVacation] = useMutation(DELETE_VACATION); + const { error, data } = useQuery(QUERY_EMPLOYEE_BY_ID, { + variables: { id: search.employeeId }, + skip: !search.employeeId || search.employeeId === "new", + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); const client = useApolloClient(); useEffect(() => { - if (selectedEmployee) form.resetFields(); - }, [selectedEmployee, form]); + if (data && data.employees_by_pk) form.setFieldsValue(data.employees_by_pk); + else { + form.resetFields(); + } + }, [form, data, search.employeeId]); - if (!selectedEmployee) return null; + const [updateEmployee] = useMutation(UPDATE_EMPLOYEE); + const [insertEmployees] = useMutation(INSERT_EMPLOYEES); + + const handleFinish = (values) => { + if (search.employeeId && search.employeeId !== "new") { + //Update a record. + logImEXEvent("shop_employee_update"); + + updateEmployee({ + variables: { + id: search.employeeId, + employee: { + ...values, + user_email: values.user_email === "" ? null : values.user_email, + }, + }, + }) + .then((r) => { + notification["success"]({ + message: t("employees.successes.save"), + }); + }) + .catch((error) => { + notification["error"]({ + message: t("employees.errors.save", { + message: JSON.stringify(error), + }), + }); + }); + } else { + //New record, insert it. + logImEXEvent("shop_employee_insert"); + + insertEmployees({ + variables: { employees: [{ ...values, shopid: bodyshop.id }] }, + refetchQueries: ["QUERY_EMPLOYEES"], + }).then((r) => { + search.employeeId = r.data.insert_employees.returning[0].id; + history.push({ search: querystring.stringify(search) }); + notification["success"]({ + message: t("employees.successes.save"), + }); + }); + } + }; + + if (!search.employeeId) return null; + if (error) return ; + + const columns = [ + { + title: t("employees.fields.vacation.start"), + dataIndex: "start", + key: "start", + render: (text, record) => {text}, + }, + { + title: t("employees.fields.vacation.end"), + dataIndex: "end", + key: "end", + render: (text, record) => {text}, + }, + { + title: t("employees.fields.vacation.length"), + dataIndex: "length", + key: "length", + render: (text, record) => + moment(record.end).diff(moment(record.start), "days", true).toFixed(1), + }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", + render: (text, record) => ( + + ), + }, + ]; return ( + + ( + + )} + columns={columns} + rowKey={"id"} + dataSource={data ? data.employees_by_pk.employee_vacations : []} + /> ); } diff --git a/client/src/components/shop-employees/shop-employees-list.component.jsx b/client/src/components/shop-employees/shop-employees-list.component.jsx index e6e3374d7..416e875f9 100644 --- a/client/src/components/shop-employees/shop-employees-list.component.jsx +++ b/client/src/components/shop-employees/shop-employees-list.component.jsx @@ -1,19 +1,22 @@ import { Button, Table } from "antd"; +import queryString from "query-string"; import React from "react"; import { useTranslation } from "react-i18next"; -export default function ShopEmployeesListComponent({ - loading, - employees, - selectedEmployee, - setSelectedEmployee, - handleDelete, -}) { +import { useHistory, useLocation } from "react-router-dom"; + +export default function ShopEmployeesListComponent({ loading, employees }) { const { t } = useTranslation(); + const history = useHistory(); + const search = queryString.parse(useLocation().search); const handleOnRowClick = (record) => { if (record) { - setSelectedEmployee(record); - } else setSelectedEmployee({}); + search.employeeId = record.id; + history.push({ search: queryString.stringify(search) }); + } else { + delete search.employeeId; + history.push({ search: queryString.stringify(search) }); + } }; const columns = [ { @@ -41,18 +44,6 @@ export default function ShopEmployeesListComponent({ ? t("employees.labels.flat_rate") : t("employees.labels.straight_time"), }, - // { - // title: t("employees.labels.actions"), - // dataIndex: "actions", - // key: "actions", - // render: (text, record) => ( - //
- // - //
- // ) - // } ]; return (
@@ -62,7 +53,8 @@ export default function ShopEmployeesListComponent({