diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel
index 81acd4f2e..ecb15e1e8 100644
--- a/bodyshop_translations.babel
+++ b/bodyshop_translations.babel
@@ -6963,6 +6963,53 @@
+
+ ttapprovals
+
+
+ approve
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ view
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+
users
@@ -16464,6 +16511,27 @@
+
+ excel
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
exceptiontitle
false
@@ -32429,6 +32497,27 @@
+
+ ttapprovals
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
vehicles
false
@@ -37714,6 +37803,27 @@
+
+ dms_posting_sheet
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
envelope_return_address
false
@@ -41367,6 +41477,48 @@
+
+ exported_gsr_by_ro
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ exported_gsr_by_ro_labor
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
gsr_by_atp
false
@@ -46228,6 +46380,27 @@
+
+ ttapprovals
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
vehicle-details
false
@@ -47154,6 +47327,27 @@
+
+ ttapprovals
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
vehicledetail
false
diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx
index 1ed3f526d..d39169401 100644
--- a/client/src/components/header/header.component.jsx
+++ b/client/src/components/header/header.component.jsx
@@ -246,6 +246,11 @@ function Header({
{t("menus.header.timetickets")}
+ }>
+
+ {t("menus.header.ttapprovals")}
+
+
}
diff --git a/client/src/components/shop-info/shop-info.rbac.component.jsx b/client/src/components/shop-info/shop-info.rbac.component.jsx
index 5d7c244cc..2a32cca54 100644
--- a/client/src/components/shop-info/shop-info.rbac.component.jsx
+++ b/client/src/components/shop-info/shop-info.rbac.component.jsx
@@ -532,6 +532,30 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
>
+
+
+
+
+
+
- _.omit(ticket, "pay")
- ),
- },
- });
- if (result.errors) {
- notification.open({
- type: "error",
- message: t("timetickets.errors.creating", {
- message: JSON.stringify(result.errors),
- }),
+ if (true) {
+ const result = await insertTimeTicketApproval({
+ variables: {
+ timeTicketInput: values.timetickets.map((ticket) => ({
+ ..._.omit(ticket, "pay"),
+ bodyshopid: bodyshop.id,
+ })),
+ },
});
+ if (result.errors) {
+ notification.open({
+ type: "error",
+ message: t("timetickets.errors.creating", {
+ message: JSON.stringify(result.errors),
+ }),
+ });
+ } else {
+ notification.open({
+ type: "success",
+ message: t("timetickets.successes.created"),
+ });
+ toggleModalVisible();
+ }
} else {
- notification.open({
- type: "success",
- message: t("timetickets.successes.created"),
+ const result = await insertTimeTickets({
+ variables: {
+ timeTicketInput: values.timetickets.map((ticket) =>
+ _.omit(ticket, "pay")
+ ),
+ },
});
- toggleModalVisible();
+ if (result.errors) {
+ notification.open({
+ type: "error",
+ message: t("timetickets.errors.creating", {
+ message: JSON.stringify(result.errors),
+ }),
+ });
+ } else {
+ notification.open({
+ type: "success",
+ message: t("timetickets.successes.created"),
+ });
+ toggleModalVisible();
+ }
}
} catch (error) {
} finally {
diff --git a/client/src/components/tt-approvals-list/tt-approvals-list.component.jsx b/client/src/components/tt-approvals-list/tt-approvals-list.component.jsx
new file mode 100644
index 000000000..9c1befc79
--- /dev/null
+++ b/client/src/components/tt-approvals-list/tt-approvals-list.component.jsx
@@ -0,0 +1,243 @@
+import { EditFilled } from "@ant-design/icons";
+import { Button, Card, Space, Table, Tag } from "antd";
+import Dinero from "dinero.js";
+import moment from "moment";
+import React, { useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { Link, useHistory, useLocation } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import { setModalContext } from "../../redux/modals/modals.actions";
+import {
+ selectAuthLevel,
+ selectBodyshop,
+} from "../../redux/user/user.selectors";
+import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
+import { onlyUnique } from "../../utils/arrayHelper";
+import { alphaSort, dateSort } from "../../utils/sorters";
+import RbacWrapper, {
+ HasRbacAccess,
+} from "../rbac-wrapper/rbac-wrapper.component";
+import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
+import queryString from "query-string";
+import TtApproveButtonComponent from "../tt-approve-button/tt-approve-button.component";
+
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop,
+ authLevel: selectAuthLevel,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ setTimeTicketTaskContext: (context) =>
+ dispatch(setModalContext({ context: context, modal: "timeTicketTask" })),
+});
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(TtApprovalsListComponent);
+
+export function TtApprovalsListComponent({
+ bodyshop,
+ setTimeTicketTaskContext,
+ authLevel,
+ disabled,
+ loading,
+ tt_approval_queue,
+ total,
+ refetch,
+ techConsole,
+ jobId,
+ extra,
+}) {
+ const [state, setState] = useState({
+ sortedInfo: {},
+ filteredInfo: { text: "" },
+ });
+
+ const { t } = useTranslation();
+ const history = useHistory();
+ const search = queryString.parse(useLocation().search);
+ const { page } = search;
+ const [selectedTickets, setSelectedTickets] = useState([]);
+
+ const columns = [
+ {
+ title: t("timetickets.fields.date"),
+ dataIndex: "date",
+ key: "date",
+ sorter: (a, b) => dateSort(a.date, b.date),
+ sortOrder:
+ state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
+ render: (text, record) => {record.date},
+ },
+ {
+ title: t("timetickets.fields.employee"),
+ dataIndex: "employeeid",
+ key: "employeeid",
+ sorter: (a, b) => alphaSort(a.employee.last_name, b.employee.last_name),
+ sortOrder:
+ state.sortedInfo.columnKey === "employee" && state.sortedInfo.order,
+ render: (text, record) =>
+ `${record.employee.first_name} ${record.employee.last_name}`,
+ filters:
+ tt_approval_queue
+ .map((l) => l.employeeid)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: (() => {
+ const emp = bodyshop.employees.find((e) => e.id === s);
+
+ return `${emp?.first_name} ${emp?.last_name}`;
+ })(), //
+ value: [s],
+ };
+ }) || [],
+ onFilter: (value, record) => value.includes(record.employeeid),
+ },
+ {
+ title: t("timetickets.fields.cost_center"),
+ dataIndex: "cost_center",
+ key: "cost_center",
+ sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
+ render: (text, record) =>
+ record.cost_center === "timetickets.labels.shift"
+ ? t(record.cost_center)
+ : record.cost_center,
+ sortOrder:
+ state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
+ filters:
+ tt_approval_queue
+ .map((l) => l.cost_center)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s === "timetickets.labels.shift" ? t(s) : s, //|| "No Status*",
+ value: [s],
+ };
+ }) || [],
+ onFilter: (value, record) => value.includes(record.cost_center),
+ },
+ {
+ title: t("jobs.fields.ro_number"),
+ dataIndex: "ro_number",
+ key: "ro_number",
+ sorter: (a, b) =>
+ alphaSort(a.job && a.job.ro_number, b.job && b.job.ro_number),
+ sortOrder:
+ state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
+ render: (text, record) =>
+ record.job && (
+
+
+ {record.job.ro_number || "N/A"}
+ {record.job.status}
+
+
+ ),
+ },
+ {
+ title: t("timetickets.fields.productivehrs"),
+ dataIndex: "productivehrs",
+ key: "productivehrs",
+ sorter: (a, b) => a.productivehrs - b.productivehrs,
+ sortOrder:
+ state.sortedInfo.columnKey === "productivehrs" &&
+ state.sortedInfo.order,
+ },
+ {
+ title: t("timetickets.fields.actualhrs"),
+ dataIndex: "actualhrs",
+ key: "actualhrs",
+ sorter: (a, b) => a.actualhrs - b.actualhrs,
+ sortOrder:
+ state.sortedInfo.columnKey === "actualhrs" && state.sortedInfo.order,
+ },
+ {
+ title: t("timetickets.fields.memo"),
+ dataIndex: "memo",
+ key: "memo",
+ sorter: (a, b) => alphaSort(a.memo, b.memo),
+ sortOrder:
+ state.sortedInfo.columnKey === "memo" && state.sortedInfo.order,
+ render: (text, record) =>
+ record.clockon || record.clockoff ? t(record.memo) : record.memo,
+ },
+ {
+ title: t("timetickets.fields.clockon"),
+ dataIndex: "clockon",
+ key: "clockon",
+
+ render: (text, record) => (
+ {record.clockon}
+ ),
+ },
+ {
+ title: "Pay",
+ dataIndex: "pay",
+ key: "pay",
+ render: (text, record) =>
+ Dinero({ amount: Math.round(record.rate * 100) })
+ .multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
+ .toFormat("$0.00"),
+ },
+ ];
+
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ search.page = pagination.current;
+ if (sorter && sorter.column && sorter.column.sortObject) {
+ search.searchObj = JSON.stringify(sorter.column.sortObject(sorter.order));
+ } else {
+ delete search.searchObj;
+ search.sortcolumn = sorter.order ? sorter.columnKey : null;
+ search.sortorder = sorter.order;
+ }
+
+ search.sort = JSON.stringify({ [sorter.columnKey]: sorter.order });
+ history.push({ search: queryString.stringify(search) });
+ };
+
+ return (
+
+ {extra}
+
+
+ }
+ >
+
+ setSelectedTickets(selectedRows.map((i) => i.id)),
+ onSelect: (record, selected, selectedRows, nativeEvent) => {
+ setSelectedTickets(selectedRows.map((i) => i.id));
+ },
+ selectedRowKeys: selectedTickets,
+ type: "checkbox",
+ }}
+ />
+
+ );
+}
diff --git a/client/src/components/tt-approvals-list/tt-approvals-list.container.jsx b/client/src/components/tt-approvals-list/tt-approvals-list.container.jsx
new file mode 100644
index 000000000..0ef860fe5
--- /dev/null
+++ b/client/src/components/tt-approvals-list/tt-approvals-list.container.jsx
@@ -0,0 +1,66 @@
+import { useQuery } from "@apollo/client";
+import React from "react";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import AlertComponent from "../alert/alert.component";
+import { QUERY_TIME_TICKETS_IN_RANGE } from "../../graphql/timetickets.queries";
+import {
+ setBreadcrumbs,
+ setSelectedHeader,
+} from "../../redux/application/application.actions";
+import TtApprovalsListComponent from "./tt-approvals-list.component";
+import { useLocation } from "react-router-dom";
+import queryString from "query-string";
+import { QUERY_ALL_TT_APPROVALS_PAGINATED } from "../../graphql/tt-approvals.queries";
+
+const mapStateToProps = createStructuredSelector({});
+
+const mapDispatchToProps = (dispatch) => ({
+ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
+ setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
+});
+
+export function TimeTicketsContainer({
+ bodyshop,
+ setBreadcrumbs,
+ setSelectedHeader,
+}) {
+ const searchParams = queryString.parse(useLocation().search);
+ const { page, sortcolumn, sortorder, search, searchObj } = searchParams;
+
+ const { loading, error, data } = useQuery(QUERY_ALL_TT_APPROVALS_PAGINATED, {
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only",
+ variables: {
+ search: search || "",
+ offset: page ? (page - 1) * 25 : 0,
+ limit: 25,
+ order: [
+ searchObj
+ ? JSON.parse(searchObj)
+ : {
+ [sortcolumn || "date"]: sortorder
+ ? sortorder === "descend"
+ ? "desc"
+ : "asc"
+ : "desc",
+ },
+ ],
+ },
+ });
+
+ if (error) return ;
+
+ return (
+
+ );
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(TimeTicketsContainer);
diff --git a/client/src/components/tt-approve-button/tt-approve-button.component.jsx b/client/src/components/tt-approve-button/tt-approve-button.component.jsx
new file mode 100644
index 000000000..a8ab89aa9
--- /dev/null
+++ b/client/src/components/tt-approve-button/tt-approve-button.component.jsx
@@ -0,0 +1,68 @@
+import { useApolloClient } from "@apollo/client";
+import { Button } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import {
+ QUERY_TT_APPROVALS_BY_IDS,
+ UPDATE_TT_BY_APPROVAL,
+} from "../../graphql/tt-approvals.queries";
+import {
+ selectBodyshop,
+ selectCurrentUser,
+} from "../../redux/user/user.selectors";
+
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop,
+ currentUser: selectCurrentUser,
+});
+
+export function TtApproveButton({
+ bodyshop,
+ currentUser,
+ selectedTickets,
+ disabled,
+ loadingCallback,
+ completedCallback,
+ refetch,
+}) {
+ const { t } = useTranslation();
+ const client = useApolloClient();
+
+ const [loading, setLoading] = useState(false);
+
+ const handleQbxml = async () => {
+ const { data } = await client.query({
+ query: QUERY_TT_APPROVALS_BY_IDS,
+ variables: { ids: selectedTickets },
+ });
+
+ const { data: insertData } = await client.query({
+ query: UPDATE_TT_BY_APPROVAL,
+ variables: {
+ ttApprovalUpdates: data.map((tta) => ({
+ _set: {},
+ where: { id: { _eq: tta.id } },
+ })),
+ },
+ });
+
+ // if (!!completedCallback) completedCallback([]);
+ // if (!!loadingCallback) loadingCallback(false);
+
+ // setLoading(false);
+ };
+
+ return (
+
+ );
+}
+
+export default connect(mapStateToProps, null)(TtApproveButton);
+
+const generateGqlUpdate = (ttapproval) => {
+ return;
+};
diff --git a/client/src/graphql/tt-approvals.queries.js b/client/src/graphql/tt-approvals.queries.js
new file mode 100644
index 000000000..48540d935
--- /dev/null
+++ b/client/src/graphql/tt-approvals.queries.js
@@ -0,0 +1,93 @@
+import { gql } from "@apollo/client";
+
+export const QUERY_ALL_TT_APPROVALS_PAGINATED = gql`
+ query QUERY_ALL_TT_APPROVALS_PAGINATED(
+ $offset: Int
+ $limit: Int
+ $order: [tt_approval_queue_order_by!]!
+ ) {
+ tt_approval_queue(offset: $offset, limit: $limit, order_by: $order) {
+ id
+ jobid
+ bodyshopid
+ employeeid
+ employee {
+ first_name
+ last_name
+ id
+ }
+ job {
+ ro_number
+ status
+ id
+ }
+ employeeid
+ actualhrs
+ productivehrs
+ ciecacode
+ cost_center
+ date
+ rate
+ }
+ tt_approval_queue_aggregate {
+ aggregate {
+ count(distinct: true)
+ }
+ }
+ }
+`;
+
+export const INSERT_NEW_TT_APPROVALS = gql`
+ mutation INSERT_NEW_TT_APPROVALS(
+ $timeTicketInput: [tt_approval_queue_insert_input!]!
+ ) {
+ insert_tt_approval_queue(objects: $timeTicketInput) {
+ returning {
+ id
+ employeeid
+ productivehrs
+ actualhrs
+ ciecacode
+ date
+ memo
+ flat_rate
+ }
+ }
+ }
+`;
+
+export const QUERY_TT_APPROVALS_BY_IDS = gql`
+ query QUERY_TT_APPROVALS_BY_IDS($ids: [uuid!]!) {
+ tt_approval_queue(where: { id: { _in: $ids } }) {
+ id
+ productivehrs
+ actualhrs
+ rate
+ memo
+ jobid
+ flat_rate
+ employeeid
+ date
+ ciecacode
+ bodyshopid
+ cost_center
+ }
+ }
+`;
+
+export const UPDATE_TT_BY_APPROVAL = gql`
+ mutation UPDATE_TT_BY_APPROVAL(
+ $ttApprovalUpdates: [tt_approval_queue_updates!]!
+ ) {
+ update_tt_approval_queue_many(updates: $ttApprovalUpdates) {
+ returning {
+ id
+ approved_at
+ approved_by
+ timeticket {
+ id
+ }
+ }
+ }
+ }
+`;
diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx
index d117aaeec..2f8b8ae37 100644
--- a/client/src/pages/manage/manage.page.component.jsx
+++ b/client/src/pages/manage/manage.page.component.jsx
@@ -170,6 +170,9 @@ const Dms = lazy(() => import("../dms/dms.container"));
const DmsPayables = lazy(() =>
import("../dms-payables/dms-payables.container")
);
+const TtApprovals = lazy(() =>
+ import("../tt-approvals/tt-approvals.page.container")
+);
const { Content, Footer } = Layout;
@@ -370,6 +373,7 @@ export function Manage({ match, conflict, bodyshop }) {
path={`${match.path}/accounting/exportlogs`}
component={ExportLogs}
/>
+
diff --git a/client/src/pages/tt-approvals/tt-approvals.page.container.jsx b/client/src/pages/tt-approvals/tt-approvals.page.container.jsx
new file mode 100644
index 000000000..1d8b689ef
--- /dev/null
+++ b/client/src/pages/tt-approvals/tt-approvals.page.container.jsx
@@ -0,0 +1,42 @@
+import React, { useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
+import TtApprovalsList from "../../components/tt-approvals-list/tt-approvals-list.container";
+import {
+ setBreadcrumbs,
+ setSelectedHeader,
+} from "../../redux/application/application.actions";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
+ setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
+});
+
+export function TtApprovalsPage({ setBreadcrumbs, setSelectedHeader }) {
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ document.title = t("titles.ttapprovals");
+ setSelectedHeader("ttapprovals");
+ setBreadcrumbs([
+ {
+ link: "/manage/ttapprovals",
+ label: t("titles.bc.ttapprovals"),
+ },
+ ]);
+ }, [t, setBreadcrumbs, setSelectedHeader]);
+
+ return (
+
+
+
+ );
+}
+export default connect(mapStateToProps, mapDispatchToProps)(TtApprovalsPage);
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index 9fe5d867e..400a413e0 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -439,6 +439,10 @@
"list": "Time Tickets -> List",
"shiftedit": "Time Tickets -> Shift Edit"
},
+ "ttapprovals": {
+ "approve": "Time Ticket Approval -> Approve",
+ "view": "Time Ticket Approval -> View"
+ },
"users": {
"editaccess": "Users -> Edit access"
}
@@ -1902,6 +1906,7 @@
"shop_vendors": "Vendors",
"temporarydocs": "Temporary Documents",
"timetickets": "Time Tickets",
+ "ttapprovals": "Time Ticket Approvals",
"vehicles": "Vehicles"
},
"jobsactions": {
@@ -2733,6 +2738,7 @@
"shop-vendors": "Vendors",
"temporarydocs": "Temporary Documents",
"timetickets": "Time Tickets",
+ "ttapprovals": "Time Ticket Approvals",
"vehicle-details": "Vehicle: {{vehicle}}",
"vehicles": "Vehicles"
},
@@ -2778,6 +2784,7 @@
"shop_vendors": "Vendors | $t(titles.app)",
"temporarydocs": "Temporary Documents | $t(titles.app)",
"timetickets": "Time Tickets | $t(titles.app)",
+ "ttapprovals": "Time Ticket Approvals | $t(titles.app)",
"vehicledetail": "Vehicle Details {{vehicle}} | $t(titles.app)",
"vehicles": "All Vehicles | $t(titles.app)"
},
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 1b4dda6b8..9184f2237 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -439,6 +439,10 @@
"list": "",
"shiftedit": ""
},
+ "ttapprovals": {
+ "approve": "",
+ "view": ""
+ },
"users": {
"editaccess": ""
}
@@ -1902,6 +1906,7 @@
"shop_vendors": "Vendedores",
"temporarydocs": "",
"timetickets": "",
+ "ttapprovals": "",
"vehicles": "VehĂculos"
},
"jobsactions": {
@@ -2733,6 +2738,7 @@
"shop-vendors": "",
"temporarydocs": "",
"timetickets": "",
+ "ttapprovals": "",
"vehicle-details": "",
"vehicles": ""
},
@@ -2778,6 +2784,7 @@
"shop_vendors": "Vendedores | $t(titles.app)",
"temporarydocs": "",
"timetickets": "",
+ "ttapprovals": "",
"vehicledetail": "Detalles del vehĂculo {{vehicle}} | $t(titles.app)",
"vehicles": "Todos los vehiculos | $t(titles.app)"
},
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 35bba987c..5481a9dc5 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -439,6 +439,10 @@
"list": "",
"shiftedit": ""
},
+ "ttapprovals": {
+ "approve": "",
+ "view": ""
+ },
"users": {
"editaccess": ""
}
@@ -1902,6 +1906,7 @@
"shop_vendors": "Vendeurs",
"temporarydocs": "",
"timetickets": "",
+ "ttapprovals": "",
"vehicles": "Véhicules"
},
"jobsactions": {
@@ -2733,6 +2738,7 @@
"shop-vendors": "",
"temporarydocs": "",
"timetickets": "",
+ "ttapprovals": "",
"vehicle-details": "",
"vehicles": ""
},
@@ -2778,6 +2784,7 @@
"shop_vendors": "Vendeurs | $t(titles.app)",
"temporarydocs": "",
"timetickets": "",
+ "ttapprovals": "",
"vehicledetail": "Détails du véhicule {{vehicle} | $t(titles.app)",
"vehicles": "Tous les véhicules | $t(titles.app)"
},