Merge branch 'master-beta' into master-AIO
This commit is contained in:
316
_reference/TeamsSlackInvestigationFinalVersion.md
Normal file
316
_reference/TeamsSlackInvestigationFinalVersion.md
Normal file
@@ -0,0 +1,316 @@
|
||||
## Microsoft Teams
|
||||
|
||||
Integrating Microsoft Teams into your Node.js backend and React frontend application can enhance communication and workflow efficiency,
|
||||
especially in a body shop management context for the automotive industry. Microsoft Teams provides several ways to integrate its functionalities into your application,
|
||||
including bots, tabs, messaging extensions, webhooks, and connectors. Here's an overview of the options and how to implement them:
|
||||
|
||||
### 0.5 - Share to Teams (TM)
|
||||
|
||||
- https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/share-to-teams-from-web-apps
|
||||
- https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/share-to-teams-from-personal-app-or-tab
|
||||
- https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/device-capabilities/people-picker-capability
|
||||
- (Example of some Teams code Integrations) https://github.com/microsoft/teams-powerapps-app-templates/
|
||||
|
||||
### 1. Microsoft Graph API
|
||||
The Microsoft Graph API is the gateway to data and intelligence in Microsoft 365, including Teams. It allows you to work with Teams data like channels, messages, and more. You can use it to post status changes back to Teams or read data from Teams to affect your site.
|
||||
|
||||
Backend (Node.js): Use the @microsoft/microsoft-graph-client package to make API calls to Teams. You will need to handle authentication with Azure AD, which can be done using the @azure/identity package to get tokens for Graph API requests.
|
||||
|
||||
*Implementation Steps*:
|
||||
|
||||
Register your application in Azure AD to get the client_id and client_secret.
|
||||
|
||||
(Is client_id and client_secret going to be something required for every customer or is it going to be a singleton)
|
||||
Implement OAuth 2.0 authorization flow to obtain access tokens.
|
||||
(This will need to be tracked by association to a bodyshop)
|
||||
|
||||
bodyshop->Channels | People ->Messages
|
||||
|
||||
Use the access token to make requests to the Microsoft Graph API to interact with Teams.
|
||||
|
||||
### 2. Webhooks and Connectors
|
||||
Webhooks allow your application to send notifications to a Teams channel, which is useful for posting status changes. Connectors are a set of predefined webhooks that offer a more integrated experience.
|
||||
|
||||
Setup:
|
||||
In Microsoft Teams, configure an incoming webhook for the channel you want to post messages to.
|
||||
Use the webhook URL to send JSON payloads from your Node.js backend, which can then be displayed in Teams.
|
||||
|
||||
|
||||
### 3. Bots
|
||||
Bots in Teams can interact with users to take commands or post information. You can use the Bot Framework along with the Teams activity handler to create bots that can communicate with your React frontend and Node.js backend.
|
||||
|
||||
Backend (Node.js): Use the botbuilder package to create and manage your bot's interactions with Teams.
|
||||
|
||||
*Implementation Steps*:
|
||||
|
||||
Create a bot registration in Azure Bot Services.
|
||||
Implement the bot logic in your Node.js application using the Bot Framework SDK.
|
||||
Use the Microsoft Bot Framework's TeamsActivityHandler to respond to Teams-specific activities.
|
||||
|
||||
### 4. Tabs
|
||||
Tabs in Teams allow you to integrate web-based content as part of Teams, which can be your React application or specific parts of it. This is particularly useful for creating a seamless experience within Teams.
|
||||
|
||||
*Implementation Steps*:
|
||||
- Create a Teams app manifest that defines your tab and its configuration.
|
||||
- Host your React application or the specific parts you want to embed as a tab.
|
||||
- Use the Teams SDK in your React application to interact with Teams context and APIs.
|
||||
|
||||
*Getting Started*:
|
||||
Microsoft Teams Toolkit for Visual Studio Code: This toolkit simplifies the process of setting up your Teams application, including authentication, configuration, and deployment.
|
||||
Documentation and Samples: Microsoft provides extensive documentation and sample code for developing Teams applications, which can be invaluable for getting started and solving specific challenges.
|
||||
Implementing these features requires a good understanding of both the Microsoft Teams platform and your application's architecture.
|
||||
Start small, perhaps by implementing notifications via webhooks, and gradually add more complex integrations like bots or tabs based on your needs and user feedback.
|
||||
|
||||
### Examples:
|
||||
|
||||
##### Posting Messages to a Teams Channel Using Incoming Webhooks
|
||||
|
||||
1 - Set up an Incoming Webhook in Microsoft Teams:
|
||||
- Go to the Teams channel where you want to post messages.
|
||||
- Click on the three dots (...) next to the channel name and select Connectors.
|
||||
- Search for Incoming Webhook, click Add, and then Configure.
|
||||
- Name your webhook, upload an image if desired, and click Create.
|
||||
- Copy the webhook URL provided.
|
||||
|
||||
(Patrick Note: The clients might have issues with setting up webhooks, it requires a lot of user configuration and our users are not technical).
|
||||
|
||||
2 - Node.js Code to Post a Message:
|
||||
|
||||
Ensure you have axios or any HTTP client library installed in your Node.js project. If not, you can install axios via npm: npm install axios.
|
||||
Use the following code snippet to post a message to the Teams channel:
|
||||
|
||||
```javascript
|
||||
const axios = require('axios');
|
||||
|
||||
// Replace 'YOUR_WEBHOOK_URL' with your actual webhook URL
|
||||
const webhookUrl = 'YOUR_WEBHOOK_URL';
|
||||
|
||||
const message = {
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
"summary": "Issue 176715375",
|
||||
"themeColor": "0078D7",
|
||||
"title": "Issue opened: \"Push notifications not working\"",
|
||||
"sections": [{
|
||||
"activityTitle": "A new issue was created",
|
||||
"activitySubtitle": "Today, 2:36 PM",
|
||||
"activityImage": "https://example.com/issues/image.png",
|
||||
"facts": [{
|
||||
"name": "Repository:",
|
||||
"value": "Repo name"
|
||||
},
|
||||
{
|
||||
"name": "Issue #:",
|
||||
"value": "176715375"
|
||||
}
|
||||
],
|
||||
"markdown": true
|
||||
}]
|
||||
};
|
||||
|
||||
axios.post(webhookUrl, message)
|
||||
.then(response => console.log('Successfully sent message to Teams channel'))
|
||||
.catch(error => console.error('Error sending message to Teams channel', error));
|
||||
```
|
||||
|
||||
#### Creating a Simple Bot for Teams
|
||||
1 Prerequisites:
|
||||
- Register your bot with the Microsoft Bot Framework and get your bot's App ID and App Password.
|
||||
- Use the Bot Framework SDK for JavaScript.
|
||||
2 Install Dependencies:
|
||||
- You need to install the botbuilder package. Run npm install botbuilder.
|
||||
|
||||
The following example demonstrates a simple bot that echoes back received messages.
|
||||
|
||||
```javascript
|
||||
const { BotFrameworkAdapter, MemoryStorage, ConversationState, TurnContext } = require('botbuilder');
|
||||
|
||||
// Create adapter.
|
||||
// See https://aka.ms/about-bot-adapter to learn more about adapters.
|
||||
const adapter = new BotFrameworkAdapter({
|
||||
appId: process.env.MicrosoftAppId,
|
||||
appPassword: process.env.MicrosoftAppPassword
|
||||
});
|
||||
|
||||
// Create conversation state with in-memory storage provider.
|
||||
const conversationState = new ConversationState(new MemoryStorage());
|
||||
adapter.use(conversationState);
|
||||
|
||||
// Listen for incoming requests.
|
||||
server.post('/api/messages', (req, res) => {
|
||||
adapter.processActivity(req, res, async (context) => {
|
||||
// Echo back what the user said
|
||||
if (context.activity.type === 'message') {
|
||||
await context.sendActivity(`You said '${context.activity.text}'`);
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Running Your Bot:
|
||||
- Make sure to expose your bot endpoint (/api/messages in this case) to the internet using a service like ngrok.
|
||||
- Update your bot's messaging endpoint in the Microsoft Bot Framework portal to point to the ngrok URL.
|
||||
|
||||
### Slash Commands (Bot)
|
||||
1 - Create a Microsoft Teams App: You need to develop a Teams app that can interact with users through commands. This involves using the Microsoft Teams Developer Platform and possibly the Bot Framework.
|
||||
2 - Use Bots for Custom Commands: The primary way to introduce custom slash commands in Teams is through bots. Bots can respond to specific commands (or messages) that are input by users. When you create a bot for Teams, you can define custom commands that the bot will recognize and respond to.
|
||||
3 - Developing the Bot: You can develop a bot using the Microsoft Bot Framework. This allows your bot to receive and send messages to a Teams channel or chat. Within your bot's code, you can define what actions to take when it receives specific commands.
|
||||
4 - Register Your Bot with Microsoft Bot Framework: Register your bot with the Microsoft Bot Framework and configure it to work with Microsoft Teams. This step involves getting a Microsoft App ID and password that are necessary for your bot to communicate with the Teams platform.
|
||||
5 - Add Your Bot to Microsoft Teams: Once your bot is developed and registered, you can package your Teams app (which includes the bot) and upload it to Microsoft Teams. This will make your bot available to users within Teams, where they can interact with it using the custom commands you've defined.
|
||||
6 - Handling Slash Commands: In the context of your bot's code, you will need to interpret messages that start with a slash (/) as commands. You can then parse the command text and perform the appropriate actions or respond accordingly.
|
||||
7 - Publish Your App: For broader distribution, you can publish your Teams app to the Teams app store or distribute it within your organization through the Teams admin center.
|
||||
/ <command> <arb> ....
|
||||
(Unrelated to teams but then used in teams for functionality)
|
||||
- A Job has a todo list
|
||||
- a todo item may have an employee
|
||||
- a todo item may have a due date
|
||||
- a todo item may have a priority
|
||||
|
||||
--- Call notes ----
|
||||
- Tasks (TODO List)
|
||||
- EMail reminders
|
||||
|
||||
## Slack
|
||||
|
||||
Integrating Slack into your Node.js backend and React frontend application can significantly streamline communication and operations for your automotive industry body shop management software. Slack offers various integration points including bots, apps, webhooks, and its rich API to facilitate interactions between your application and Slack workspace. Here's how you can leverage these integration points:
|
||||
|
||||
### 1. **Slack Web API**
|
||||
|
||||
The Slack Web API allows you to interact with Slack, enabling functionalities like sending messages, managing channels, and more directly from your application.
|
||||
|
||||
- **Backend (Node.js)**: Utilize the `@slack/web-api` package to make API calls from your Node.js backend. This will be the backbone for actions such as posting status updates to Slack channels or handling commands from Slack that can affect your site.
|
||||
|
||||
- **Implementation Steps**:
|
||||
1. Create a Slack app in your Slack workspace and obtain the API tokens.
|
||||
2. Use the `@slack/web-api` package to authenticate and interact with Slack API endpoints.
|
||||
3. Implement features such as sending messages or processing events from Slack.
|
||||
|
||||
### 2. **Incoming Webhooks**
|
||||
|
||||
For simpler integrations focused on sending notifications to Slack channels, incoming webhooks are straightforward and effective. They allow you to send messages to a specific channel without a full-blown app.
|
||||
|
||||
- **Setup**:
|
||||
1. Create an incoming webhook from the Slack app configuration page.
|
||||
2. Use the webhook URL to send messages from your Node.js backend by making simple HTTP POST requests with your message payload.
|
||||
|
||||
### 3. **Bots**
|
||||
|
||||
Slack bots can facilitate interactive experiences within your Slack workspace, responding to commands, posting notifications, or even pulling data from your site on demand.
|
||||
|
||||
- **Backend (Node.js)**: Leverage the `@slack/bolt` framework, which simplifies creating Slack bots with event handling, messaging, and built-in OAuth support.
|
||||
|
||||
- **Implementation Steps**:
|
||||
1. Create a Slack app and enable bot features.
|
||||
2. Use the `@slack/bolt` package to develop your bot, handling events like messages or commands.
|
||||
3. Deploy your bot and set it to listen to incoming events from Slack.
|
||||
|
||||
### 4. **Slack Block Kit**
|
||||
|
||||
For more engaging and interactive messages, Slack's Block Kit provides a UI framework that allows you to create richly formatted messages, modals, and more.
|
||||
|
||||
- **Frontend (React)** and **Backend (Node.js)**: Utilize Slack's Block Kit to design complex messages with buttons, sections, and interactive components. You can send these payloads through the Web API or from bots to enhance your app's interaction with users.
|
||||
|
||||
### Getting Started
|
||||
|
||||
- **Slack API Documentation and Tools**: Slack's API documentation is comprehensive and includes tutorials, tooling (like Block Kit Builder), and SDK documentation to help you get started.
|
||||
|
||||
- **Testing and Development**: Slack provides a sandbox environment for you to test your app's integrations and interactions without affecting your live workspace.
|
||||
|
||||
To integrate Slack into your application effectively, start by planning out the interactions you need (e.g., notifications, commands, information retrieval) and map these to the appropriate Slack features. Then, incrementally build and test each integration, utilizing Slack's development tools and your existing Node.js and React knowledge to create a seamless experience for your users.
|
||||
|
||||
#### Examples
|
||||
|
||||
1. Incoming Webhooks
|
||||
Incoming Webhooks are a simple way to post messages from your Node.js application into Slack channels. They're perfect for notifying team members about status changes or updates.
|
||||
|
||||
Setup:
|
||||
|
||||
1 - Create a new Slack app in your workspace and enable incoming webhooks.
|
||||
2 - Add a new webhook to your app and choose the channel it will post to.
|
||||
3 - Use the webhook URL to send messages from your backend.
|
||||
|
||||
Node.js Example:
|
||||
|
||||
```javascript
|
||||
|
||||
const axios = require('axios');
|
||||
const webhookUrl = 'YOUR_SLACK_WEBHOOK_URL';
|
||||
|
||||
async function postMessageToSlack(message) {
|
||||
await axios.post(webhookUrl, {
|
||||
text: message, // Your message here
|
||||
});
|
||||
}
|
||||
|
||||
postMessageToSlack('New status update on the automotive project!').catch(console.error);
|
||||
```
|
||||
2. Bots
|
||||
Slack bots can interact with users via messages, respond to commands, and even post updates. They're great for building interactive features.
|
||||
|
||||
Setup:
|
||||
|
||||
1 - Create a Slack app and add the Bot Token Scopes (like chat:write).
|
||||
2 - Install the app to your workspace to get your bot token.
|
||||
3 - Use the @slack/bolt framework for easy bot development in Node.js.
|
||||
|
||||
Node.js Example:
|
||||
|
||||
```javascript
|
||||
const { App } = require('@slack/bolt');
|
||||
|
||||
const app = new App({
|
||||
token: process.env.SLACK_BOT_TOKEN, // Set your bot's access token here
|
||||
signingSecret: process.env.SLACK_SIGNING_SECRET // Set your app's signing secret here
|
||||
});
|
||||
|
||||
app.message('hello', async ({ message, say }) => {
|
||||
await say(`Hey there <@${message.user}>!`);
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await app.start(process.env.PORT || 3000);
|
||||
console.log('Slack bot is running!');
|
||||
})();
|
||||
```
|
||||
|
||||
3. Slash Commands
|
||||
Slash Commands allow users to interact with your application directly from Slack by typing commands that start with /.
|
||||
|
||||
Setup:
|
||||
|
||||
1 - Create a Slack app and configure a new Slash Command (e.g., /statusupdate).
|
||||
2 - Point the command request URL to your Node.js backend endpoint.
|
||||
3 - Implement the endpoint to handle the command and respond accordingly.
|
||||
|
||||
Node.js Example:
|
||||
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
const app = express();
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
|
||||
app.post('/slack/commands/statusupdate', (req, res) => {
|
||||
const { text, user_name } = req.body; // Command input and user info
|
||||
// Logic to handle the command goes here. For example, update a status or fetch information.
|
||||
res.send(`Status update received: ${text} - from ${user_name}`);
|
||||
});
|
||||
|
||||
app.listen(process.env.PORT || 3000, () => console.log('Server is running'));
|
||||
```
|
||||
|
||||
4. Interactive Components
|
||||
Interactive components like buttons or menus can make your Slack messages more engaging and interactive.
|
||||
|
||||
Setup:
|
||||
1 - Enable Interactivity in your Slack app settings.
|
||||
2 - Implement an endpoint in your Node.js backend to handle interactions.
|
||||
3 - Send messages with interactive components from your backend.
|
||||
|
||||
Implementing these features into your Node.js and React application will enable a more dynamic and integrated experience for users of your body shop management software. Start with the simpler integrations like webhooks and progressively incorporate bots and interactive components as needed.
|
||||
|
||||
ACTION ITEMS:
|
||||
- Slot in tasks, this will be a dependency for things we want to do in the future.
|
||||
- This would involve GUI components
|
||||
- This would involve a backend component
|
||||
@@ -3,7 +3,13 @@
|
||||
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
|
||||
modify the graphQL query and provide the user more power over their reports.
|
||||
|
||||
# Special Notes
|
||||
For filters and sorters, valid types include (`type` key in the schema):
|
||||
- string (default)
|
||||
- number
|
||||
- bool or boolean
|
||||
- date
|
||||
|
||||
## Special Notes
|
||||
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
|
||||
|
||||
## High level Schema Overview
|
||||
@@ -40,9 +46,10 @@ Filters effect the where clause of the graphQL query. They are used to filter th
|
||||
A note on special notation used in the `name` field.
|
||||
|
||||
## Reflection
|
||||
|
||||
Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file.
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"name": "jobs.status",
|
||||
"translation": "jobs.fields.status",
|
||||
@@ -52,7 +59,7 @@ Filters can make use of reflection to pre-fill select boxes, the following is an
|
||||
"type": "internal",
|
||||
"name": "special.job_statuses"
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses`
|
||||
@@ -67,7 +74,13 @@ The following cases are available
|
||||
- `special.employees` - This will reflect the employees `bodyshop.employees`
|
||||
- `special.first_names` - This will reflect the first names `bodyshop.employees`
|
||||
- `special.last_names` - This will reflect the last names `bodyshop.employees`
|
||||
-
|
||||
- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources`
|
||||
- `special.class`- This will reflect the class `bodyshop.md_classes`
|
||||
- `special.lost_sale_reasons` - This will reflect the lost sale reasons `bodyshop.md_lost_sale_reasons`
|
||||
- `special.alt_transports` - This will reflect the alternative transports `bodyshop.appt_alt_transport`
|
||||
- `special.payment_types` - This will reflect the payment types `bodyshop.md_payment_types`
|
||||
- `special.payment_payers` - This is a special case with a key value set of [Customer, Insurance]
|
||||
|
||||
### Path without brackets, multi level
|
||||
|
||||
`"name": "jobs.joblines.mod_lb_hrs",`
|
||||
@@ -142,12 +155,12 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
|
||||
|
||||
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
|
||||
is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
|
||||
- The `dates` object is not yet implemented and will be added in a future release.
|
||||
- The type object must be 'string' or 'number' and is case-sensitive.
|
||||
- The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' and is case-sensitive.
|
||||
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
|
||||
- Do not add the ability to filter things that are already filtered as part of the original query, this would be
|
||||
redundant and could cause issues.
|
||||
- Do not add the ability to filter on things like FK constraints, must like the above example.
|
||||
- *INHERITANCE CAVEAT* If you have a filters file on a parent report that has a child that you do not want the filters inherited from, you must place a blank filters file (valid json so {}) on the child report level. This will than fetch the child filters, which are empty and move along, versus inheriting the parent filters.
|
||||
|
||||
## Sorters
|
||||
|
||||
@@ -158,6 +171,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
|
||||
using the sorters.
|
||||
|
||||
### Default Sorters
|
||||
|
||||
- A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves.
|
||||
- The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`.
|
||||
|
||||
@@ -172,4 +186,4 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
|
||||
"direction": "asc"
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
462
client/package-lock.json
generated
462
client/package-lock.json
generated
@@ -17,8 +17,8 @@
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.1",
|
||||
"@sentry/cli": "^2.28.6",
|
||||
"@sentry/react": "^7.102.1",
|
||||
"@sentry/tracing": "^7.102.1",
|
||||
"@sentry/react": "^7.104.0",
|
||||
"@sentry/tracing": "^7.104.0",
|
||||
"@splitsoftware/splitio-react": "^1.11.0",
|
||||
"@tanem/react-nprogress": "^5.0.51",
|
||||
"@vitejs/plugin-legacy": "^5.3.0",
|
||||
@@ -38,7 +38,7 @@
|
||||
"env-cmd": "^10.1.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"exifr": "^7.1.3",
|
||||
"firebase": "^10.8.0",
|
||||
"firebase": "^10.8.1",
|
||||
"graphql": "^16.6.0",
|
||||
"i18next": "^23.10.0",
|
||||
"i18next-browser-languagedetector": "^7.0.2",
|
||||
@@ -51,11 +51,11 @@
|
||||
"phone": "^3.1.42",
|
||||
"preval.macro": "^5.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^8.2.0",
|
||||
"query-string": "^9.0.0",
|
||||
"rc-queue-anim": "^2.0.0",
|
||||
"rc-scroll-anim": "^2.7.6",
|
||||
"react": "^18.2.0",
|
||||
"react-big-calendar": "^1.10.3",
|
||||
"react-big-calendar": "^1.11.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^7.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -68,15 +68,15 @@
|
||||
"react-intersection-observer": "^9.8.1",
|
||||
"react-joyride": "^2.7.4",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-number-format": "^5.1.4",
|
||||
"react-number-format": "^5.3.3",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.22.1",
|
||||
"react-router-dom": "^6.22.2",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-sublime-video": "^0.2.5",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"recharts": "^2.12.1",
|
||||
"recharts": "^2.12.2",
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.3.0",
|
||||
@@ -97,7 +97,7 @@
|
||||
"workbox-precaching": "^7.0.0",
|
||||
"workbox-routing": "^7.0.0",
|
||||
"workbox-strategies": "^7.0.0",
|
||||
"yauzl": "^3.1.0"
|
||||
"yauzl": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
@@ -2955,7 +2955,8 @@
|
||||
},
|
||||
"node_modules/@fastify/busboy": {
|
||||
"version": "2.1.0",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
|
||||
"integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
@@ -3001,7 +3002,8 @@
|
||||
},
|
||||
"node_modules/@firebase/app": {
|
||||
"version": "0.9.27",
|
||||
"license": "Apache-2.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.27.tgz",
|
||||
"integrity": "sha512-p2Dvl1ge4kRsyK5+wWcmdAIE9MSwZ0pDKAYB51LZgZuz6wciUZk4E1yAEdkfQlRxuHehn+Ol9WP5Qk2XQZiHGg==",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.5",
|
||||
"@firebase/logger": "0.4.0",
|
||||
@@ -3048,9 +3050,10 @@
|
||||
},
|
||||
"node_modules/@firebase/app-compat": {
|
||||
"version": "0.2.27",
|
||||
"license": "Apache-2.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.27.tgz",
|
||||
"integrity": "sha512-SYlqocfUDKPHR6MSFC8hree0BTiWFu5o8wbf6zFlYXyG41w7TcHp4wJi4H/EL5V6cM4kxwruXTJtqXX/fRAZtw==",
|
||||
"dependencies": {
|
||||
"@firebase/app": "0.9.27",
|
||||
"@firebase/app": "0.9.28",
|
||||
"@firebase/component": "0.6.5",
|
||||
"@firebase/logger": "0.4.0",
|
||||
"@firebase/util": "1.9.4",
|
||||
@@ -3063,13 +3066,14 @@
|
||||
},
|
||||
"node_modules/@firebase/auth": {
|
||||
"version": "1.6.0",
|
||||
"license": "Apache-2.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.6.0.tgz",
|
||||
"integrity": "sha512-Qhl35eJTV6BwvuueTPCY6x8kUlYyzALtjp/Ws0X3fw3AnjVVfuVb7oQ3Xh5VPVfMFhaIuUAd1KXwcAuIklkSDw==",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.5",
|
||||
"@firebase/logger": "0.4.0",
|
||||
"@firebase/util": "1.9.4",
|
||||
"tslib": "^2.1.0",
|
||||
"undici": "5.26.5"
|
||||
"undici": "5.28.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@firebase/app": "0.x",
|
||||
@@ -3083,14 +3087,15 @@
|
||||
},
|
||||
"node_modules/@firebase/auth-compat": {
|
||||
"version": "0.5.2",
|
||||
"license": "Apache-2.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.2.tgz",
|
||||
"integrity": "sha512-pRgje5BPCNR1vXyvGOVXwOHtv88A2WooXfklI8sV7/jWi03ExFqNfpJT26GUo/oD39NoKJ3Kt6rD5gVvdV7lMw==",
|
||||
"dependencies": {
|
||||
"@firebase/auth": "1.6.0",
|
||||
"@firebase/auth": "1.6.1",
|
||||
"@firebase/auth-types": "0.12.0",
|
||||
"@firebase/component": "0.6.5",
|
||||
"@firebase/util": "1.9.4",
|
||||
"tslib": "^2.1.0",
|
||||
"undici": "5.26.5"
|
||||
"undici": "5.28.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@firebase/app-compat": "0.x"
|
||||
@@ -3171,7 +3176,8 @@
|
||||
},
|
||||
"node_modules/@firebase/firestore": {
|
||||
"version": "4.4.2",
|
||||
"license": "Apache-2.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.4.2.tgz",
|
||||
"integrity": "sha512-YaX6ypa/RzU6OkxzUQlpSxwhOIWdTraCNz7sMsbaSEjjl/pj/QvX6TqjkdWGzuBYh2S6rz7ErhDO0g39oZZw/g==",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.5",
|
||||
"@firebase/logger": "0.4.0",
|
||||
@@ -3180,7 +3186,7 @@
|
||||
"@grpc/grpc-js": "~1.9.0",
|
||||
"@grpc/proto-loader": "^0.7.8",
|
||||
"tslib": "^2.1.0",
|
||||
"undici": "5.26.5"
|
||||
"undici": "5.28.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.10.0"
|
||||
@@ -3191,10 +3197,11 @@
|
||||
},
|
||||
"node_modules/@firebase/firestore-compat": {
|
||||
"version": "0.3.25",
|
||||
"license": "Apache-2.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.25.tgz",
|
||||
"integrity": "sha512-+xI7WmsgZCBhMn/+uhDKcg+lsOUJ9FJyt5PGTzkFPbCsozWfeQZ7eVnfPh0rMkUOf0yIQ924RIe04gwvEIbcoQ==",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.5",
|
||||
"@firebase/firestore": "4.4.2",
|
||||
"@firebase/firestore": "4.4.3",
|
||||
"@firebase/firestore-types": "3.0.0",
|
||||
"@firebase/util": "1.9.4",
|
||||
"tslib": "^2.1.0"
|
||||
@@ -3223,7 +3230,8 @@
|
||||
},
|
||||
"node_modules/@firebase/functions": {
|
||||
"version": "0.11.1",
|
||||
"license": "Apache-2.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.11.1.tgz",
|
||||
"integrity": "sha512-3uUa1hB79Gmy6E1gHTfzoHeZolBeHc/I/n3+lOCDe6BOos9AHmzRjKygcFE/7VA2FJjitCE0K+OHI6+OuoY8fQ==",
|
||||
"dependencies": {
|
||||
"@firebase/app-check-interop-types": "0.3.0",
|
||||
"@firebase/auth-interop-types": "0.2.1",
|
||||
@@ -3231,7 +3239,7 @@
|
||||
"@firebase/messaging-interop-types": "0.2.0",
|
||||
"@firebase/util": "1.9.4",
|
||||
"tslib": "^2.1.0",
|
||||
"undici": "5.26.5"
|
||||
"undici": "5.28.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@firebase/app": "0.x"
|
||||
@@ -3239,10 +3247,11 @@
|
||||
},
|
||||
"node_modules/@firebase/functions-compat": {
|
||||
"version": "0.3.7",
|
||||
"license": "Apache-2.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.7.tgz",
|
||||
"integrity": "sha512-uXe6Kmku5lNogp3OpPBcOJbSvnaCOn+YxS3zlXKNU6Q/NLwcvO3RY1zwYyctCos2RemEw3KEQ7YdzcECXjHWLw==",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.5",
|
||||
"@firebase/functions": "0.11.1",
|
||||
"@firebase/functions": "0.11.2",
|
||||
"@firebase/functions-types": "0.6.0",
|
||||
"@firebase/util": "1.9.4",
|
||||
"tslib": "^2.1.0"
|
||||
@@ -3406,12 +3415,13 @@
|
||||
},
|
||||
"node_modules/@firebase/storage": {
|
||||
"version": "0.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.12.1.tgz",
|
||||
"integrity": "sha512-KJ5NV7FUh54TeTlEjdkTTX60ciCKOp9EqlbLnpdcXUYRJg0Z4810TXbilPc1z7fTIG4iPjtdi95bGE9n4dBX8A==",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.5",
|
||||
"@firebase/util": "1.9.4",
|
||||
"tslib": "^2.1.0",
|
||||
"undici": "5.26.5"
|
||||
"undici": "5.28.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@firebase/app": "0.x"
|
||||
@@ -3419,10 +3429,11 @@
|
||||
},
|
||||
"node_modules/@firebase/storage-compat": {
|
||||
"version": "0.3.4",
|
||||
"license": "Apache-2.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.4.tgz",
|
||||
"integrity": "sha512-Y0m5e2gS/wB9Ioth2X/Sgz76vcxvqgQrCmfa9qwhss/N31kxY2Gks6Frv0nrE18AjVfcSmcfDitqUwxcMOTRSg==",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.5",
|
||||
"@firebase/storage": "0.12.1",
|
||||
"@firebase/storage": "0.12.2",
|
||||
"@firebase/storage-types": "0.8.0",
|
||||
"@firebase/util": "1.9.4",
|
||||
"tslib": "^2.1.0"
|
||||
@@ -5445,7 +5456,8 @@
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.15.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.1.tgz",
|
||||
"integrity": "sha512-zcU0gM3z+3iqj8UX45AmWY810l3oUmXM7uH4dt5xtzvMhRtYVhKGOmgOd1877dOPPepfCjUv57w+syamWIYe7w==",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@@ -5559,17 +5571,14 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs/node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.102.1.tgz",
|
||||
"integrity": "sha512-vY4hpLLMNLjICtWiizc7KeGbWOTUMGrF7C+9dPCztZww3CLgzWy9A7DvPj5hodRiYzpdRnAMl8yQnMFbYXh7bA==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
"@sentry/core": "7.102.1",
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -5578,137 +5587,77 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs/node_modules/magic-string": {
|
||||
"version": "0.25.9",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"node_modules/@sentry-internal/feedback/node_modules/@sentry/core": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz",
|
||||
"integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==",
|
||||
"dependencies": {
|
||||
"sourcemap-codec": "^1.4.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-commonjs/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-inject": {
|
||||
"version": "5.0.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"node_modules/@sentry-internal/feedback/node_modules/@sentry/types": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz",
|
||||
"integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback/node_modules/@sentry/utils": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz",
|
||||
"integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.3"
|
||||
"@sentry/types": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-inject/node_modules/@rollup/pluginutils": {
|
||||
"version": "5.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.102.1.tgz",
|
||||
"integrity": "sha512-GUX4RWI10uRjdjeyvCLtAAhWRVqnAnG6+yNxWfqUQ3qMA7B7XxG43KT2UhSnulmErNzODQ6hA68rGPwwYeRIww==",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-inject/node_modules/magic-string": {
|
||||
"version": "0.30.7",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
"@sentry/core": "7.102.1",
|
||||
"@sentry/replay": "7.102.1",
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "11.2.1",
|
||||
"license": "MIT",
|
||||
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/core": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz",
|
||||
"integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^3.1.0",
|
||||
"@types/resolve": "1.17.1",
|
||||
"builtin-modules": "^3.1.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"is-module": "^1.0.0",
|
||||
"resolve": "^1.19.0"
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0"
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve/node_modules/@rollup/pluginutils": {
|
||||
"version": "3.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "0.0.39",
|
||||
"estree-walker": "^1.0.1",
|
||||
"picomatch": "^2.2.2"
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/types": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz",
|
||||
"integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==",
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0"
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve/node_modules/@types/estree": {
|
||||
"version": "0.0.39",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve/node_modules/estree-walker": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/plugin-replace": {
|
||||
"version": "2.4.2",
|
||||
"license": "MIT",
|
||||
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/utils": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz",
|
||||
"integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^3.1.0",
|
||||
"magic-string": "^0.25.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0 || ^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-replace/node_modules/@rollup/pluginutils": {
|
||||
"version": "3.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "0.0.39",
|
||||
"estree-walker": "^1.0.1",
|
||||
"picomatch": "^2.2.2"
|
||||
"@sentry/types": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
@@ -5759,34 +5708,27 @@
|
||||
"version": "1.7.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "7.102.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.102.1",
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "7.102.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.102.1.tgz",
|
||||
"integrity": "sha512-7BOfPBiM7Kp6q/iy0JIbsBTxIASV+zWXByqqjuEMWGj3X2u4oRIfm3gv4erPU/l+CORQUVQZLSPGoIoM1gbB/A==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/feedback": "7.102.1",
|
||||
"@sentry-internal/replay-canvas": "7.102.1",
|
||||
"@sentry-internal/tracing": "7.102.1",
|
||||
"@sentry/core": "7.102.1",
|
||||
"@sentry/replay": "7.102.1",
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/tracing": {
|
||||
"node_modules/@sentry/browser/node_modules/@sentry-internal/tracing": {
|
||||
"version": "7.102.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.102.1.tgz",
|
||||
"integrity": "sha512-RkFlFyAC0fQOvBbBqnq0CLmFW5m3JJz9pKbZd5vXPraWAlniKSb1bC/4DF9SlNx0FN1LWG+IU3ISdpzwwTeAGg==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.102.1",
|
||||
"@sentry/types": "7.102.1",
|
||||
@@ -5796,23 +5738,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/babel-plugin-component-annotate": {
|
||||
"version": "2.14.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"node_modules/@sentry/browser/node_modules/@sentry/core": {
|
||||
"version": "7.102.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz",
|
||||
"integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/feedback": "7.102.1",
|
||||
"@sentry-internal/replay-canvas": "7.102.1",
|
||||
"@sentry-internal/tracing": "7.102.1",
|
||||
"@sentry/core": "7.102.1",
|
||||
"@sentry/replay": "7.102.1",
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1"
|
||||
},
|
||||
@@ -5820,6 +5750,25 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser/node_modules/@sentry/types": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz",
|
||||
"integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser/node_modules/@sentry/utils": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz",
|
||||
"integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core": {
|
||||
"version": "2.14.2",
|
||||
"dev": true,
|
||||
@@ -5892,29 +5841,56 @@
|
||||
},
|
||||
"node_modules/@sentry/react": {
|
||||
"version": "7.102.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.102.1.tgz",
|
||||
"integrity": "sha512-X4j2DgbktlEifnd21YJKCayAmff5hnaS+9MNz9OonEwD0ARi0ks7bo0wtWHMjPK20992MO+JwczVg/1BXJYDdQ==",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "7.102.1",
|
||||
"@sentry/core": "7.102.1",
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1",
|
||||
"hoist-non-react-statics": "^3.3.2"
|
||||
"@sentry/types": "7.104.0",
|
||||
"@sentry/utils": "7.104.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/react/node_modules/@sentry/core": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz",
|
||||
"integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "15.x || 16.x || 17.x || 18.x"
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/react/node_modules/@sentry/types": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz",
|
||||
"integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/react/node_modules/@sentry/utils": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz",
|
||||
"integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/replay": {
|
||||
"version": "7.102.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.102.1.tgz",
|
||||
"integrity": "sha512-HR/j9dGIvbrId8fh8mQlODx7JrhRmawEd9e9P3laPtogWCg/5TI+XPb2VGSaXOX9VWtb/6Z2UjHsaGjgg6YcuA==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/tracing": "7.102.1",
|
||||
"@sentry/core": "7.102.1",
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1"
|
||||
"@sentry-internal/tracing": "7.104.0",
|
||||
"@sentry/core": "7.104.0",
|
||||
"@sentry/types": "7.104.0",
|
||||
"@sentry/utils": "7.104.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -5922,24 +5898,52 @@
|
||||
},
|
||||
"node_modules/@sentry/tracing": {
|
||||
"version": "7.102.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.102.1.tgz",
|
||||
"integrity": "sha512-9VQEox0R7ouhhUVHtBwlGlXG5beDCM/Uo0BY+G0M1H03aFJsLAwnxPNeWnK3WvPejxf94EgdimKMjDjv9l2Sbg==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/tracing": "7.102.1"
|
||||
"@sentry-internal/tracing": "7.104.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"node_modules/@sentry/tracing/node_modules/@sentry-internal/tracing": {
|
||||
"version": "7.102.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.102.1.tgz",
|
||||
"integrity": "sha512-RkFlFyAC0fQOvBbBqnq0CLmFW5m3JJz9pKbZd5vXPraWAlniKSb1bC/4DF9SlNx0FN1LWG+IU3ISdpzwwTeAGg==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.102.1",
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/utils": {
|
||||
"node_modules/@sentry/tracing/node_modules/@sentry/core": {
|
||||
"version": "7.102.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz",
|
||||
"integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.102.1",
|
||||
"@sentry/utils": "7.102.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/tracing/node_modules/@sentry/types": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz",
|
||||
"integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/tracing/node_modules/@sentry/utils": {
|
||||
"version": "7.102.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz",
|
||||
"integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.102.1"
|
||||
},
|
||||
@@ -13340,23 +13344,24 @@
|
||||
},
|
||||
"node_modules/firebase": {
|
||||
"version": "10.8.0",
|
||||
"license": "Apache-2.0",
|
||||
"resolved": "https://registry.npmjs.org/firebase/-/firebase-10.8.0.tgz",
|
||||
"integrity": "sha512-UJpC24vw8JFuHEOQyArBGKTUd7+kohLISCzHyn0M/prP0KOTx2io1eyLliEid330QqnWI7FOlPxoU97qecCSfQ==",
|
||||
"dependencies": {
|
||||
"@firebase/analytics": "0.10.1",
|
||||
"@firebase/analytics-compat": "0.2.7",
|
||||
"@firebase/app": "0.9.27",
|
||||
"@firebase/app": "0.9.28",
|
||||
"@firebase/app-check": "0.8.2",
|
||||
"@firebase/app-check-compat": "0.3.9",
|
||||
"@firebase/app-compat": "0.2.27",
|
||||
"@firebase/app-compat": "0.2.28",
|
||||
"@firebase/app-types": "0.9.0",
|
||||
"@firebase/auth": "1.6.0",
|
||||
"@firebase/auth-compat": "0.5.2",
|
||||
"@firebase/auth": "1.6.1",
|
||||
"@firebase/auth-compat": "0.5.3",
|
||||
"@firebase/database": "1.0.3",
|
||||
"@firebase/database-compat": "1.0.3",
|
||||
"@firebase/firestore": "4.4.2",
|
||||
"@firebase/firestore-compat": "0.3.25",
|
||||
"@firebase/functions": "0.11.1",
|
||||
"@firebase/functions-compat": "0.3.7",
|
||||
"@firebase/firestore": "4.4.3",
|
||||
"@firebase/firestore-compat": "0.3.26",
|
||||
"@firebase/functions": "0.11.2",
|
||||
"@firebase/functions-compat": "0.3.8",
|
||||
"@firebase/installations": "0.6.5",
|
||||
"@firebase/installations-compat": "0.2.5",
|
||||
"@firebase/messaging": "0.12.6",
|
||||
@@ -13365,8 +13370,8 @@
|
||||
"@firebase/performance-compat": "0.2.5",
|
||||
"@firebase/remote-config": "0.4.5",
|
||||
"@firebase/remote-config-compat": "0.2.5",
|
||||
"@firebase/storage": "0.12.1",
|
||||
"@firebase/storage-compat": "0.3.4",
|
||||
"@firebase/storage": "0.12.2",
|
||||
"@firebase/storage-compat": "0.3.5",
|
||||
"@firebase/util": "1.9.4"
|
||||
}
|
||||
},
|
||||
@@ -21661,14 +21666,15 @@
|
||||
},
|
||||
"node_modules/query-string": {
|
||||
"version": "8.2.0",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-8.2.0.tgz",
|
||||
"integrity": "sha512-tUZIw8J0CawM5wyGBiDOAp7ObdRQh4uBor/fUR9ZjmbZVvw95OD9If4w3MQxr99rg0DJZ/9CIORcpEqU5hQG7g==",
|
||||
"dependencies": {
|
||||
"decode-uri-component": "^0.4.1",
|
||||
"filter-obj": "^5.1.0",
|
||||
"split-on-first": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
@@ -22493,8 +22499,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-big-calendar": {
|
||||
"version": "1.11.0",
|
||||
"license": "MIT",
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.10.3.tgz",
|
||||
"integrity": "sha512-LmIWlFfGUn8yt4RxcVkGNmjM3GcWynr1bfDwKrrz4KKj517+DH3OGmQzErURN6Zb0OB88HF4oH2dvDHpBQJgIw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.7",
|
||||
"clsx": "^1.2.1",
|
||||
@@ -22898,8 +22905,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-number-format": {
|
||||
"version": "5.3.3",
|
||||
"license": "MIT",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.3.1.tgz",
|
||||
"integrity": "sha512-qpYcQLauIeEhCZUZY9jXZnnroOtdy3jYaS1zQ3M1Sr6r/KMOBEIGNIb7eKT19g2N1wbYgFgvDzs19hw5TrB8XQ==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
@@ -22971,9 +22979,10 @@
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.22.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.1.tgz",
|
||||
"integrity": "sha512-0pdoRGwLtemnJqn1K0XHUbnKiX0S4X8CgvVVmHGOWmofESj31msHo/1YiqcJWK7Wxfq2a4uvvtS01KAQyWK/CQ==",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.15.1"
|
||||
"@remix-run/router": "1.15.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -22984,10 +22993,11 @@
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.22.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.1.tgz",
|
||||
"integrity": "sha512-iwMyyyrbL7zkKY7MRjOVRy+TMnS/OPusaFVxM2P11x9dzSzGmLsebkCvYirGq0DWB9K9hOspHYYtDz33gE5Duw==",
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.15.1",
|
||||
"react-router": "6.22.1"
|
||||
"@remix-run/router": "1.15.2",
|
||||
"react-router": "6.22.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -23241,7 +23251,8 @@
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.12.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.1.tgz",
|
||||
"integrity": "sha512-35vUCEBPf+pM+iVgSgVTn86faKya5pc4JO6cYJL63qOK2zDEyzDn20Tdj+CDI/3z+VcpKyQ8ZBQ9OiQ+vuAbjg==",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"eventemitter3": "^4.0.1",
|
||||
@@ -26326,9 +26337,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.28.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz",
|
||||
"integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==",
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "^2.0.0"
|
||||
},
|
||||
@@ -29054,7 +29065,8 @@
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "3.1.0",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.0.tgz",
|
||||
"integrity": "sha512-zbff6SaAPyewVextulqeBjJm+1ZhS69vSN7cRpqVD7jMNSE9oXEdQ1SGF+ydfB+gKE2a3GiWfXf/pnwVZ1/tOA==",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"pend": "~1.2.0"
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.1",
|
||||
"@sentry/cli": "^2.28.6",
|
||||
"@sentry/react": "^7.102.1",
|
||||
"@sentry/tracing": "^7.102.1",
|
||||
"@sentry/react": "^7.104.0",
|
||||
"@sentry/tracing": "^7.104.0",
|
||||
"@splitsoftware/splitio-react": "^1.11.0",
|
||||
"@tanem/react-nprogress": "^5.0.51",
|
||||
"@vitejs/plugin-legacy": "^5.3.0",
|
||||
@@ -38,7 +38,7 @@
|
||||
"env-cmd": "^10.1.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"exifr": "^7.1.3",
|
||||
"firebase": "^10.8.0",
|
||||
"firebase": "^10.8.1",
|
||||
"graphql": "^16.6.0",
|
||||
"i18next": "^23.10.0",
|
||||
"i18next-browser-languagedetector": "^7.0.2",
|
||||
@@ -51,11 +51,11 @@
|
||||
"phone": "^3.1.42",
|
||||
"preval.macro": "^5.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^8.2.0",
|
||||
"query-string": "^9.0.0",
|
||||
"rc-queue-anim": "^2.0.0",
|
||||
"rc-scroll-anim": "^2.7.6",
|
||||
"react": "^18.2.0",
|
||||
"react-big-calendar": "^1.10.3",
|
||||
"react-big-calendar": "^1.11.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^7.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -68,15 +68,15 @@
|
||||
"react-intersection-observer": "^9.8.1",
|
||||
"react-joyride": "^2.7.4",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-number-format": "^5.1.4",
|
||||
"react-number-format": "^5.3.3",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.22.1",
|
||||
"react-router-dom": "^6.22.2",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-sublime-video": "^0.2.5",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"recharts": "^2.12.1",
|
||||
"recharts": "^2.12.2",
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.3.0",
|
||||
@@ -97,7 +97,7 @@
|
||||
"workbox-precaching": "^7.0.0",
|
||||
"workbox-routing": "^7.0.0",
|
||||
"workbox-strategies": "^7.0.0",
|
||||
"yauzl": "^3.1.0"
|
||||
"yauzl": "^3.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import {Card, Checkbox, Input, Space, Table} from "antd";
|
||||
import {Card, Checkbox, Input, Space, Table} from "antd";import queryString from "query-string";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect } from "react-redux";
|
||||
import {Link} from "react-router-dom";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import {alphaSort, dateSort} from "../../utils/sorters";
|
||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||
import {DateFormatter} from "../../utils/DateFormatter";
|
||||
import queryString from "query-string";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import {DateFormatter} from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import {alphaSort, dateSort} from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -147,7 +146,7 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
|
||||
|
||||
render: (text, record) => (
|
||||
<PayableExportButton
|
||||
|
||||
@@ -8,14 +8,16 @@ import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import {DateFormatter, DateTimeFormatter} from "../../utils/DateFormatter";
|
||||
import {pageLimit } from "../../utils/config";
|
||||
import {alphaSort, dateSort} from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
|
||||
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -75,7 +77,11 @@ export function AccountingPayablesTableComponent({
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
OwnerNameDisplayFunction(a.job),
|
||||
OwnerNameDisplayFunction(b.job)
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
@@ -94,7 +100,9 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("payments.fields.amount"),
|
||||
dataIndex: "amount",
|
||||
key: "amount",
|
||||
render: (text, record) => (
|
||||
sorter: (a, b) => a.amount - b.amount,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,render: (text, record) => (
|
||||
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
@@ -111,19 +119,21 @@ export function AccountingPayablesTableComponent({
|
||||
{
|
||||
title: t("payments.fields.created_at"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
key: "created_at",sorter: (a, b) => dateSort(a.created_at, b.created_at),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.exportedat"),
|
||||
dataIndex: "exportedat",
|
||||
key: "exportedat",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
//{
|
||||
// title: t("payments.fields.exportedat"),
|
||||
// dataIndex: "exportedat",
|
||||
// key: "exportedat",
|
||||
// render: (text, record) => (
|
||||
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||
// ),
|
||||
//},
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
@@ -137,7 +147,7 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
|
||||
|
||||
render: (text, record) => (
|
||||
<PaymentExportButton
|
||||
|
||||
@@ -4,17 +4,19 @@ import {useTranslation} from "react-i18next";
|
||||
import {Link} from "react-router-dom";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import {alphaSort, dateSort} from "../../utils/sorters";
|
||||
import {alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
import {DateFormatter} from "../../utils/DateFormatter";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -63,7 +65,7 @@ export function AccountingReceivablesTableComponent({
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => a.status - b.status,
|
||||
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
},
|
||||
@@ -83,7 +85,8 @@ export function AccountingReceivablesTableComponent({
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
@@ -103,7 +106,15 @@ export function AccountingReceivablesTableComponent({
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
|
||||
@@ -5,10 +5,22 @@ import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {DELETE_BILL} from "../../graphql/bills.queries";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
|
||||
export default function BillDeleteButton({bill, callback}) {
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
|
||||
|
||||
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const [deleteBill] = useMutation(DELETE_BILL);
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -35,7 +47,12 @@ export default function BillDeleteButton({bill, callback}) {
|
||||
});
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({message: t("bills.successes.deleted")});
|
||||
notification["success"]({ message: t("bills.successes.deleted") });
|
||||
insertAuditTrail({
|
||||
jobid: jobid,
|
||||
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
|
||||
type: "billdeleted",
|
||||
});
|
||||
|
||||
if (callback && typeof callback === "function") callback(bill.id);
|
||||
} else {
|
||||
|
||||
@@ -30,8 +30,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "partsOrder"})),
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
@@ -145,7 +145,8 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
|
||||
jobid: bill.jobid,
|
||||
billid: search.billid,
|
||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||
});
|
||||
type: "billupdated",
|
||||
});
|
||||
|
||||
await refetch();
|
||||
form.setFieldsValue(transformData(data));
|
||||
|
||||
@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "partsOrder"})),
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -32,8 +32,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
||||
insertAuditTrail: ({jobid, billid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, billid, operation})),
|
||||
insertAuditTrail: ({jobid, billid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, billid, operation, type })),
|
||||
});
|
||||
|
||||
const Templates = TemplateList("job_special");
|
||||
@@ -212,7 +212,7 @@ function BillEnterModalContainer({
|
||||
mod_lbr_ty: key,
|
||||
hours: adjustmentsToInsert[key].toFixed(1),
|
||||
}),
|
||||
});
|
||||
type: "jobmodifylbradj",});
|
||||
});
|
||||
|
||||
const jobUpdate = client.mutate({
|
||||
@@ -369,7 +369,8 @@ function BillEnterModalContainer({
|
||||
operation: AuditTrailMapping.billposted(
|
||||
r1.data.insert_bills.returning[0].invoice_number
|
||||
),
|
||||
});
|
||||
type: "billposted",
|
||||
});
|
||||
|
||||
if (enterAgain) {
|
||||
// form.resetFields();
|
||||
|
||||
@@ -50,17 +50,17 @@ export function BillsListTableComponent({
|
||||
|
||||
const Templates = TemplateList("bill");
|
||||
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
||||
const {refetch} = billsQuery;
|
||||
const { refetch } = billsQuery;
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space wrap>
|
||||
{showView && (
|
||||
<Button onClick={() => handleOnRowClick(record)}>
|
||||
<EditFilled/>
|
||||
<EditFilled />
|
||||
</Button>
|
||||
)}
|
||||
<BillDeleteButton bill={record}/>
|
||||
<BillDeleteButton bill={record} jobid={job.id} />
|
||||
<BillDetailEditReturnComponent
|
||||
data={{bills_by_pk: {...record, jobid: job.id}}}
|
||||
data={{ bills_by_pk: { ...record, jobid: job.id } }}
|
||||
disabled={
|
||||
record.is_credit_memo ||
|
||||
record.vendorid === bodyshop.inhousevendorid ||
|
||||
|
||||
@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type})),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ const CardPaymentModalComponent = ({
|
||||
insertAuditTrail({
|
||||
jobid: payment.jobid,
|
||||
operation: AuditTrailMapping.failedpayment(),
|
||||
})
|
||||
type: "failedpayment",})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import {SyncOutlined, WarningFilled} from "@ant-design/icons";
|
||||
import {Button, Card, Dropdown, Input, Space, Table, Tooltip,} from "antd";
|
||||
import { SyncOutlined, WarningFilled } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Dropdown,
|
||||
Input,
|
||||
Space,
|
||||
Table,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Link} from "react-router-dom";
|
||||
import {DateTimeFormatter} from "../../utils/DateFormatter";
|
||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
||||
import {TemplateList} from "../../utils/TemplateConstants";
|
||||
import {alphaSort} from "../../utils/sorters";
|
||||
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function CourtesyCarsList({loading, courtesycars, refetch}) {
|
||||
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {text: ""},
|
||||
});
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const {t} = useTranslation();
|
||||
const [filter, setFilter] = useLocalStorage(
|
||||
"filter_courtesy_cars_list",
|
||||
null
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@@ -42,6 +54,7 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
filteredValue: filter?.status || null,
|
||||
filters: [
|
||||
{
|
||||
text: t("courtesycars.status.in"),
|
||||
@@ -64,7 +77,7 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
const {nextservicedate, nextservicekm, mileage, insuranceexpires} =
|
||||
const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
|
||||
record;
|
||||
|
||||
const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
|
||||
@@ -75,19 +88,23 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
|
||||
const insuranceOver =
|
||||
insuranceexpires &&
|
||||
dayjs(insuranceexpires).endOf("day").isBefore(dayjs());
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{t(record.status)}
|
||||
{(mileageOver || dueForService || insuranceOver) && (
|
||||
<Tooltip title={(mileageOver || dueForService) && insuranceOver
|
||||
? t("contracts.labels.insuranceexpired") +
|
||||
" / " +
|
||||
t("contracts.labels.cardueforservice")
|
||||
: insuranceOver
|
||||
? t("contracts.labels.insuranceexpired")
|
||||
: t("contracts.labels.cardueforservice")
|
||||
}>
|
||||
<WarningFilled style={{color: "tomato"}}/>
|
||||
<Tooltip
|
||||
title={
|
||||
(mileageOver || dueForService) && insuranceOver
|
||||
? t("contracts.labels.insuranceexpired") +
|
||||
" / " +
|
||||
t("contracts.labels.cardueforservice")
|
||||
: insuranceOver
|
||||
? t("contracts.labels.insuranceexpired")
|
||||
: t("contracts.labels.cardueforservice")
|
||||
}
|
||||
>
|
||||
<WarningFilled style={{ color: "tomato" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
@@ -99,6 +116,7 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
|
||||
dataIndex: "readiness",
|
||||
key: "readiness",
|
||||
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
|
||||
filteredValue: filter?.readiness || null,
|
||||
filters: [
|
||||
{
|
||||
text: t("courtesycars.readiness.ready"),
|
||||
@@ -214,7 +232,8 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({...state, filteredInfo: filters, sortedInfo: sorter});
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
setFilter(filters);
|
||||
};
|
||||
|
||||
const tableData = searchText
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
PauseCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||
import moment from "moment";
|
||||
import dayjs from "../../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -74,7 +74,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
}
|
||||
});
|
||||
appt.sort(function (a, b) {
|
||||
return new moment(a.start) - new moment(b.start);
|
||||
return dayjs(a.start) - dayjs(b.start);
|
||||
});
|
||||
|
||||
const tvFontSize = 16;
|
||||
@@ -419,7 +419,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.scheduledindate", {
|
||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
||||
date: dayjs().startOf("day").format("MM/DD/YYYY"),
|
||||
})}
|
||||
extra={
|
||||
<Space>
|
||||
@@ -449,9 +449,9 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
}
|
||||
|
||||
export const DashboardScheduledInTodayGql = `
|
||||
scheduled_in_today: appointments(where: {start: {_gte: "${moment()
|
||||
scheduled_in_today: appointments(where: {start: {_gte: "${dayjs()
|
||||
.startOf("day")
|
||||
.toISOString()}", _lte: "${moment()
|
||||
.toISOString()}", _lte: "${dayjs()
|
||||
.endOf("day")
|
||||
.toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) {
|
||||
canceled
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function ScheduleEventContainer({bodyshop, event, refetch}) {
|
||||
insertAuditTrail({
|
||||
jobid: event.job.id,
|
||||
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
||||
})
|
||||
type: "appointmentcancel",})
|
||||
);
|
||||
}
|
||||
if (!!jobUpdate.errors) {
|
||||
|
||||
@@ -22,8 +22,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobChecklistForm({
|
||||
@@ -177,7 +177,7 @@ export function JobChecklistForm({
|
||||
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
|
||||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
|
||||
),
|
||||
});
|
||||
type: "jobchecklist",});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("checklist.errors.complete", {
|
||||
|
||||
@@ -15,8 +15,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -47,7 +47,8 @@ export function JobEmployeeAssignmentsContainer({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobassignmentchange(operation, name),
|
||||
});
|
||||
type: "jobassignmentchange",
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
@@ -77,7 +78,7 @@ export function JobEmployeeAssignmentsContainer({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobassignmentremoved(operation),
|
||||
});
|
||||
type: "jobassignmentremoved",});
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -102,7 +102,7 @@ export function JobLineConvertToLabor({
|
||||
hours: calculateAdjustment({mod_lbr_ty, job, jobline}).toFixed(1),
|
||||
mod_lbr_ty,
|
||||
}),
|
||||
});
|
||||
type: "jobmodifylbradj",});
|
||||
setLoading(false);
|
||||
setVisibility(false);
|
||||
};
|
||||
|
||||
@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
|
||||
|
||||
@@ -32,7 +32,7 @@ export function JobsAdminStatus({insertAuditTrail, bodyshop, job}) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobstatuschange(status),
|
||||
});
|
||||
type: "admin_jobstatuschange",});
|
||||
// refetch();
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
@@ -57,7 +57,7 @@ export function JobsAdminDatesChange({insertAuditTrail, job}) {
|
||||
? DateTimeFormat(changedAuditFields[key])
|
||||
: changedAuditFields[key]
|
||||
),
|
||||
});
|
||||
type: "admin_jobfieldchange",});
|
||||
});
|
||||
|
||||
if (!!!result.errors) {
|
||||
|
||||
@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -59,7 +59,7 @@ export function JobAdminMarkReexport({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobmarkforreexport(),
|
||||
});
|
||||
type: "admin_jobmarkforreexport",});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.saving", {
|
||||
@@ -99,7 +99,7 @@ export function JobAdminMarkReexport({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobmarkexported(),
|
||||
});
|
||||
type: "admin_jobmarkexported",});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.saving", {
|
||||
@@ -124,7 +124,7 @@ export function JobAdminMarkReexport({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobuninvoice(),
|
||||
});
|
||||
type: "admin_jobuninvoice",});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.saving", {
|
||||
|
||||
@@ -10,8 +10,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR);
|
||||
@@ -34,7 +34,7 @@ export function JobsAdminRemoveAR({insertAuditTrail, job}) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_job_remove_from_ar(value),
|
||||
});
|
||||
type: "admin_job_remove_from_ar",});
|
||||
setSwitchValue(value);
|
||||
} else {
|
||||
notification["error"]({
|
||||
|
||||
@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
|
||||
|
||||
@@ -46,6 +46,7 @@ export function JobsAdminUnvoid({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobunvoid(),
|
||||
type: "admin_jobunvoid",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
|
||||
@@ -39,8 +39,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,}) {
|
||||
@@ -194,10 +194,11 @@ export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,
|
||||
});
|
||||
//Job has been inserted. Clean up the available jobs record.
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: r.data.insert_jobs.returning[0].id,
|
||||
operation: AuditTrailMapping.jobimported(),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: r.data.insert_jobs.returning[0].id,
|
||||
operation: AuditTrailMapping.jobimported(),
|
||||
type: "jobimported",
|
||||
});
|
||||
|
||||
await deleteJob({
|
||||
variables: {id: estData.id},
|
||||
@@ -324,24 +325,25 @@ export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,
|
||||
setInsertLoading(false);
|
||||
});
|
||||
|
||||
await insertNote({
|
||||
variables: {
|
||||
noteInput: [
|
||||
{
|
||||
jobid: selectedJob,
|
||||
created_by: currentUser.email,
|
||||
audit: true,
|
||||
text: t("jobs.labels.supplementnote"),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: selectedJob,
|
||||
operation: AuditTrailMapping.jobsupplement(),
|
||||
});
|
||||
}
|
||||
};
|
||||
await insertNote({
|
||||
variables: {
|
||||
noteInput: [
|
||||
{
|
||||
jobid: selectedJob,
|
||||
created_by: currentUser.email,
|
||||
audit: true,
|
||||
text: t("jobs.labels.supplementnote"),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: selectedJob,
|
||||
operation: AuditTrailMapping.jobsupplement(),
|
||||
type: "jobsupplement",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const owner =
|
||||
estDataRaw.data &&
|
||||
|
||||
@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsChangeStatus({job, bodyshop, jobRO, insertAuditTrail}) {
|
||||
@@ -35,7 +35,7 @@ export function JobsChangeStatus({job, bodyshop, jobRO, insertAuditTrail}) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(status),
|
||||
});
|
||||
type: "jobstatuschange",});
|
||||
// refetch();
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -9,7 +9,9 @@ import {createStructuredSelector} from "reselect";
|
||||
import {auth, logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
|
||||
import {UPDATE_JOB} from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -17,6 +19,11 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
function updateJobCache(items) {
|
||||
client.cache.modify({
|
||||
id: "ROOT_QUERY",
|
||||
@@ -37,9 +44,10 @@ export function JobsCloseExportButton({
|
||||
disabled,
|
||||
setSelectedJobs,
|
||||
refetch,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const history = useNavigate();
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -178,6 +186,10 @@ export function JobsCloseExportButton({
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",});
|
||||
updateJobCache(
|
||||
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
|
||||
);
|
||||
@@ -189,12 +201,20 @@ export function JobsCloseExportButton({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
|
||||
if (
|
||||
bodyshop.accountingconfig &&
|
||||
bodyshop.accountingconfig.qbo &&
|
||||
successfulTransactions.length > 0
|
||||
) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobexported(),type: "jobexported",
|
||||
});
|
||||
updateJobCache([
|
||||
...new Set(
|
||||
successfulTransactions.map(
|
||||
@@ -224,4 +244,7 @@ export function JobsCloseExportButton({
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(JobsCloseExportButton);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsCloseExportButton);
|
||||
|
||||
@@ -17,8 +17,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsConvertButton({
|
||||
@@ -70,7 +70,8 @@ export function JobsConvertButton({
|
||||
operation: AuditTrailMapping.jobconverted(
|
||||
res.data.update_jobs.returning[0].ro_number
|
||||
),
|
||||
});
|
||||
type: "jobconverted",
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function AddToProduction(
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobinproductionchange(!remove),
|
||||
type: "jobinproductionchange",
|
||||
})
|
||||
);
|
||||
if (completionCallback) completionCallback();
|
||||
|
||||
@@ -50,8 +50,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(setModalContext({context: context, modal: "timeTicket"})),
|
||||
setCardPaymentContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "cardPayment"})),
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
setTimeTicketTaskContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "timeTicketTask"})),
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
@@ -242,6 +242,11 @@ export function JobsDetailHeaderActions({
|
||||
message: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobvoid(),
|
||||
type: "jobvoid",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (e.key === "email")
|
||||
@@ -356,6 +361,11 @@ export function JobsDetailHeaderActions({
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.voided"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobvoid(),
|
||||
type: "jobvoid",
|
||||
});
|
||||
//go back to jobs list.
|
||||
history(`/manage/`);
|
||||
} else {
|
||||
@@ -472,9 +482,29 @@ export function JobsDetailHeaderActions({
|
||||
? !job.production_vars.alert
|
||||
: true
|
||||
),
|
||||
type: "alertToggle",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSuspend = (e) => {
|
||||
logImEXEvent("production_toggle_alert");
|
||||
//e.stopPropagation();
|
||||
updateJob({
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
job: {
|
||||
suspended: !job.suspended,
|
||||
},
|
||||
},
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobsuspend(
|
||||
!!job.suspended ? !job.suspended : true
|
||||
),
|
||||
type: "jobsuspend",
|
||||
});
|
||||
};
|
||||
|
||||
// Function to handle OK
|
||||
const handleCancelScheduleOK = async () => {
|
||||
@@ -504,24 +534,12 @@ export function JobsDetailHeaderActions({
|
||||
jobid: job.id,
|
||||
operation:
|
||||
AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
||||
type: "appointmentcancel",
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuspend = (e) => {
|
||||
logImEXEvent("production_toggle_alert");
|
||||
//e.stopPropagation();
|
||||
updateJob({
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
job: {
|
||||
suspended: !job.suspended,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const popOverContent = (
|
||||
<Card>
|
||||
<div>
|
||||
|
||||
@@ -9,7 +9,12 @@ import {createStructuredSelector} from "reselect";
|
||||
import {auth, logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
|
||||
import {UPDATE_JOBS} from "../../graphql/jobs.queries";
|
||||
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -17,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
function updateJobCache(items) {
|
||||
client.cache.modify({
|
||||
id: "ROOT_QUERY",
|
||||
@@ -38,8 +48,9 @@ export function JobsExportAllButton({
|
||||
loadingCallback,
|
||||
completedCallback,
|
||||
refetch,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const [updateJob] = useMutation(UPDATE_JOBS);
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
|
||||
@@ -168,47 +179,66 @@ export function JobsExportAllButton({
|
||||
},
|
||||
});
|
||||
|
||||
if (!!!jobUpdateResponse.errors) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
updateJobCache(
|
||||
jobUpdateResponse.data.update_jobs.returning.map(
|
||||
(job) => job.id
|
||||
)
|
||||
);
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting", {
|
||||
error: JSON.stringify(jobUpdateResponse.error),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
updateJobCache([
|
||||
...new Set(
|
||||
successfulTransactions.map(
|
||||
(st) =>
|
||||
st[
|
||||
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
|
||||
? "jobid"
|
||||
: "id"
|
||||
]
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
if (!!!jobUpdateResponse.errors) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",
|
||||
});
|
||||
});
|
||||
updateJobCache(
|
||||
jobUpdateResponse.data.update_jobs.returning.map(
|
||||
(job) => job.id
|
||||
)
|
||||
);
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting", {
|
||||
error: JSON.stringify(jobUpdateResponse.error),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (
|
||||
bodyshop.accountingconfig &&
|
||||
bodyshop.accountingconfig.qbo &&
|
||||
successfulTransactions.length > 0
|
||||
) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
const successfulTransactionsSet = [
|
||||
...new Set(
|
||||
successfulTransactions.map(
|
||||
(st) =>
|
||||
st[
|
||||
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
|
||||
? "jobid"
|
||||
: "id"
|
||||
]
|
||||
)
|
||||
),
|
||||
];
|
||||
if (successfulTransactionsSet.length > 0) {
|
||||
insertAuditTrail({
|
||||
jobid: successfulTransactionsSet[0],
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",
|
||||
});
|
||||
}
|
||||
updateJobCache(successfulTransactionsSet);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (!!completedCallback) completedCallback([]);
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
@@ -222,4 +252,7 @@ export function JobsExportAllButton({
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(JobsExportAllButton);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsExportAllButton);
|
||||
|
||||
@@ -17,6 +17,7 @@ import AlertComponent from "../alert/alert.component";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { setJoyRideSteps } from "../../redux/application/application.actions";
|
||||
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -148,7 +149,8 @@ export function JobsList({bodyshop,setJoyRideSteps}) {
|
||||
ellipsis: true,
|
||||
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
@@ -193,7 +195,8 @@ export function JobsList({bodyshop,setJoyRideSteps}) {
|
||||
key: "status",
|
||||
ellipsis: true,
|
||||
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sorter: (a, b) =>
|
||||
statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filteredValue: filter?.status || null,
|
||||
@@ -224,7 +227,15 @@ export function JobsList({bodyshop,setJoyRideSteps}) {
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
to={"/manage/vehicles/" + record.vehicleid}
|
||||
@@ -271,7 +282,9 @@ export function JobsList({bodyshop,setJoyRideSteps}) {
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
filteredValue: filter?.ins_co_nm || null,
|
||||
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,filteredValue: filter?.ins_co_nm || null,
|
||||
filters:
|
||||
(jobs &&
|
||||
jobs
|
||||
@@ -307,7 +320,13 @@ export function JobsList({bodyshop,setJoyRideSteps}) {
|
||||
dataIndex: "jobs.labels.estimator",
|
||||
key: "jobs.labels.estimator",
|
||||
ellipsis: true,
|
||||
responsive: ["xl"],
|
||||
responsive: ["xl"],sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(),
|
||||
`${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim()
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order,
|
||||
filterSearch: true,
|
||||
filteredValue: filter?.estimator || null,
|
||||
filters:
|
||||
|
||||
@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobNotesContainer);
|
||||
|
||||
@@ -46,7 +46,7 @@ export function JobNotesContainer({jobId, insertAuditTrail}) {
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobnotedeleted(),
|
||||
});
|
||||
type: "jobnotedeleted",});
|
||||
});
|
||||
setDeleteLoading(false);
|
||||
};
|
||||
|
||||
@@ -13,8 +13,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -69,7 +69,7 @@ export function LaborAllocationsAdjustmentEdit({
|
||||
values.hours -
|
||||
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
|
||||
}),
|
||||
});
|
||||
type: "jobmodifylbradj",});
|
||||
}
|
||||
setLoading(false);
|
||||
setOpen(false);
|
||||
|
||||
@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function NoteUpsertModalContainer({
|
||||
@@ -70,7 +70,7 @@ export function NoteUpsertModalContainer({
|
||||
insertAuditTrail({
|
||||
jobid: context.jobId,
|
||||
operation: AuditTrailMapping.jobnoteupdated(),
|
||||
});
|
||||
type: "jobnoteupdated",});
|
||||
});
|
||||
if (refetch) refetch();
|
||||
toggleModalVisible();
|
||||
@@ -102,7 +102,7 @@ export function NoteUpsertModalContainer({
|
||||
insertAuditTrail({
|
||||
jobid: newJobId,
|
||||
operation: AuditTrailMapping.jobnoteadded(),
|
||||
});
|
||||
type: "jobnoteadded",});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ export function NoteUpsertModalContainer({
|
||||
insertAuditTrail({
|
||||
jobid: context.jobId,
|
||||
operation: AuditTrailMapping.jobnoteadded(),
|
||||
});
|
||||
type: "jobnoteadded",});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import {useApolloClient, useMutation, useQuery} from "@apollo/client";
|
||||
import {Form, Modal, notification} from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {auth, logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {UPDATE_JOB_LINE_STATUS} from "../../graphql/jobs-lines.queries";
|
||||
import {INSERT_NEW_PARTS_ORDERS, QUERY_PARTS_ORDER_OEC,} from "../../graphql/parts-orders.queries";
|
||||
import {QUERY_ALL_VENDORS_FOR_ORDER} from "../../graphql/vendors.queries";
|
||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||
import {setEmailOptions} from "../../redux/email/email.actions";
|
||||
import {setModalContext, toggleModalVisible,} from "../../redux/modals/modals.actions";
|
||||
import {selectPartsOrder} from "../../redux/modals/modals.selectors";
|
||||
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||
import { useMutation, useQuery, useApolloClient } from "@apollo/client";
|
||||
import { Form, Modal, notification } from "antd";
|
||||
import dayjs from '../../utils/day';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent, auth } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
|
||||
import {
|
||||
INSERT_NEW_PARTS_ORDERS,
|
||||
QUERY_PARTS_ORDER_OEC,
|
||||
} from "../../graphql/parts-orders.queries";
|
||||
import { QUERY_ALL_VENDORS_FOR_ORDER } from "../../graphql/vendors.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import {
|
||||
setModalContext,
|
||||
toggleModalVisible,
|
||||
} from "../../redux/modals/modals.actions";
|
||||
import { selectPartsOrder } from "../../redux/modals/modals.selectors";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
||||
import {TemplateList} from "../../utils/TemplateConstants";
|
||||
@@ -36,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")),
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "billEnter"})),
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function PartsOrderModalContainer({
|
||||
@@ -135,7 +144,8 @@ export function PartsOrderModalContainer({
|
||||
: AuditTrailMapping.jobspartsorder(
|
||||
insertResult.data.insert_parts_orders.returning[0].order_number
|
||||
),
|
||||
});
|
||||
type: isReturn ? "jobspartsreturn" : "jobspartsorder",
|
||||
});
|
||||
|
||||
const jobLinesResult = await updateJobLines({
|
||||
variables: {
|
||||
|
||||
@@ -52,8 +52,8 @@ const PaymentExpandedRowComponent = ({record, bodyshop}) => {
|
||||
await insertPayment({
|
||||
variables: {
|
||||
paymentInput: {
|
||||
amount: -refund_response.data.amount,
|
||||
transactionid: payment_response.response.receiptelements.transid,
|
||||
amount: -refund_response?.data?.amount,
|
||||
transactionid: payment_response?.response?.receiptelements?.transid,
|
||||
payer: record.payer,
|
||||
type: "Refund",
|
||||
jobid: payment_response.jobid,
|
||||
|
||||
@@ -2,14 +2,17 @@ import {Button, Form, Input, Space} from "antd";
|
||||
import {PageHeader} from "@ant-design/pro-layout";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectAuthLevel,
|
||||
selectBodyshop,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
||||
import PhoneFormItem, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {selectAuthLevel, selectBodyshop,} from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
authLevel: selectAuthLevel,
|
||||
@@ -43,13 +46,16 @@ export function PhonebookFormComponent({
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title={`${form.getFieldValue("firstname") || ""} ${
|
||||
title={<Form.Item shouldUpdate>
|
||||
{() =>`${form.getFieldValue("firstname") || ""} ${
|
||||
form.getFieldValue("lastname") || ""
|
||||
}${
|
||||
form.getFieldValue("company")
|
||||
? ` - ${form.getFieldValue("company")}`
|
||||
: ""
|
||||
}`}
|
||||
: ""}`
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
|
||||
@@ -31,8 +31,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ProductionBoardKanbanComponent({
|
||||
@@ -134,7 +134,8 @@ export function ProductionBoardKanbanComponent({
|
||||
insertAuditTrail({
|
||||
jobid: card.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
|
||||
});
|
||||
type: "jobstatuschange",
|
||||
});
|
||||
|
||||
if (update.errors) {
|
||||
notification["error"]({
|
||||
|
||||
@@ -27,6 +27,7 @@ export function ProductionColumnsComponent({
|
||||
bodyshop,
|
||||
data,
|
||||
tableState,
|
||||
refetch,
|
||||
}) {
|
||||
const [columns, setColumns] = columnState;
|
||||
const {t} = useTranslation();
|
||||
@@ -50,6 +51,7 @@ export function ProductionColumnsComponent({
|
||||
data,
|
||||
state: tableState,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
refetch,
|
||||
});
|
||||
|
||||
const menu = {
|
||||
|
||||
@@ -13,8 +13,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ProductionListColumnAlert({record, insertAuditTrail}) {
|
||||
@@ -46,7 +46,7 @@ export function ProductionListColumnAlert({record, insertAuditTrail}) {
|
||||
? !record.production_vars.alert
|
||||
: true
|
||||
),
|
||||
}).then(() => {
|
||||
type: "alertToggle",}).then(() => {
|
||||
if (record.refetch) record.refetch();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {BranchesOutlined, PauseCircleOutlined} from "@ant-design/icons";
|
||||
import {Space, Tooltip} from "antd";
|
||||
import {Checkbox,Space, Tooltip} from "antd";
|
||||
import i18n from "i18next";
|
||||
import dayjs from "../../utils/day";
|
||||
import {Link} from "react-router-dom";
|
||||
@@ -10,7 +10,9 @@ import {onlyUnique} from "../../utils/arrayHelper";
|
||||
import {alphaSort, dateSort, statusSort} from "../../utils/sorters";
|
||||
import JobAltTransportChange from "../job-at-change/job-at-change.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||
import ProductionListColumnAlert from "./production-list-columns.alert.component";
|
||||
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
|
||||
@@ -28,7 +30,7 @@ import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.co
|
||||
import {store} from "../../redux/store";
|
||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
||||
|
||||
const r = ({technician, state, activeStatuses, data, bodyshop}) => {
|
||||
const r = ({technician, state, activeStatuses, data, bodyshop, refetch}) => {
|
||||
return [
|
||||
{
|
||||
title: i18n.t("jobs.actions.viewdetail"),
|
||||
@@ -109,7 +111,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
|
||||
<OwnerNameDisplay ownerObject={record}/>
|
||||
</Link>
|
||||
),
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
|
||||
},
|
||||
@@ -120,8 +122,10 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
a.v_make_desc + a.v_model_desc,
|
||||
b.v_make_desc + b.v_model_desc
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
@@ -316,7 +320,21 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
|
||||
dataIndex: "special_coverage_policy",
|
||||
key: "special_coverage_policy",
|
||||
ellipsis: true,
|
||||
},
|
||||
sorter: (a, b) =>
|
||||
Number(a.special_coverage_policy) - Number(b.special_coverage_policy),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "special_coverage_policy" &&
|
||||
state.sortedInfo.order,
|
||||
filters: [
|
||||
{ text: "True", value: true },
|
||||
{ text: "False", value: false },
|
||||
],
|
||||
onFilter: (value, record) =>
|
||||
value.includes(record.special_coverage_policy),
|
||||
render: (text, record) => (
|
||||
<Checkbox disabled checked={record.special_coverage_policy} />
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: i18n.t("jobs.fields.alt_transport"),
|
||||
@@ -327,7 +345,16 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alt_transport" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
filters:
|
||||
(bodyshop &&
|
||||
bodyshop.appt_alt_transport.map((s) => {
|
||||
return {
|
||||
text: s,
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),render: (text, record) => (
|
||||
<div>
|
||||
{record.alt_transport}
|
||||
<JobAltTransportChange job={record}/>
|
||||
@@ -407,7 +434,11 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
|
||||
title: i18n.t("production.labels.alert"),
|
||||
dataIndex: "alert",
|
||||
key: "alert",
|
||||
|
||||
sorter: (a, b) =>
|
||||
Number(a.production_vars?.alert || false) -
|
||||
Number(b.production_vars?.alert || false),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alert" && state.sortedInfo.order,
|
||||
render: (text, record) => <ProductionListColumnAlert record={record}/>,
|
||||
},
|
||||
{
|
||||
@@ -498,6 +529,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
|
||||
),
|
||||
render: (text, record) => (
|
||||
<ProductionListEmployeeAssignment
|
||||
refetch={refetch}
|
||||
record={record}
|
||||
type="employee_body"
|
||||
/>
|
||||
@@ -518,6 +550,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
|
||||
render: (text, record) => (
|
||||
<ProductionListEmployeeAssignment
|
||||
record={record}
|
||||
refetch={refetch}
|
||||
type="employee_prep"
|
||||
/>
|
||||
),
|
||||
@@ -534,7 +567,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
|
||||
bodyshop.employees?.find((e) => e.id === b.employee_csr)?.first_name
|
||||
),
|
||||
render: (text, record) => (
|
||||
<ProductionListEmployeeAssignment record={record} type="employee_csr"/>
|
||||
<ProductionListEmployeeAssignment refetch={refetch} record={record} type="employee_csr"/>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -554,6 +587,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
|
||||
render: (text, record) => (
|
||||
<ProductionListEmployeeAssignment
|
||||
record={record}
|
||||
refetch={refetch}
|
||||
type="employee_refinish"
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -16,14 +16,15 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ProductionListEmpAssignment({
|
||||
insertAuditTrail,
|
||||
bodyshop,
|
||||
record,
|
||||
refetch,
|
||||
type,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
@@ -46,7 +47,8 @@ export function ProductionListEmpAssignment({
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobassignmentchange(empAssignment, name),
|
||||
});
|
||||
type: "jobassignmentchange",
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
@@ -55,6 +57,9 @@ export function ProductionListEmpAssignment({
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
await refetch();
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
const handleRemove = async (operation) => {
|
||||
@@ -71,7 +76,8 @@ export function ProductionListEmpAssignment({
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobassignmentremoved(empAssignment),
|
||||
});
|
||||
type: "jobassignmentremoved",
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
@@ -80,6 +86,9 @@ export function ProductionListEmpAssignment({
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
await refetch();
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ProductionListColumnCategory({record, bodyshop}) {
|
||||
|
||||
@@ -5,16 +5,15 @@ import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {UPDATE_JOB} from "../../graphql/jobs.queries";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||
import {insertAuditTrail} from "../../redux/application/application.actions";import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ProductionListColumnStatus({
|
||||
@@ -41,7 +40,8 @@ export function ProductionListColumnStatus({
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(key),
|
||||
});
|
||||
type: "jobstatuschange",
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
export function ProductionListTable({
|
||||
refetch,
|
||||
bodyshop,
|
||||
technician,
|
||||
currentUser,
|
||||
@@ -38,6 +39,7 @@ export function ProductionListTable({
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
refetch,
|
||||
technician,
|
||||
state,
|
||||
data: data,
|
||||
@@ -95,6 +97,7 @@ export function ProductionListTable({
|
||||
...ProductionListColumns({
|
||||
technician,
|
||||
state,
|
||||
refetch,
|
||||
data: data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
}).find((e) => e.key === k.key),
|
||||
|
||||
@@ -64,6 +64,7 @@ export function ProductionListTable({loading, data, refetch, bodyshop, technicia
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
refetch,
|
||||
technician,
|
||||
state,
|
||||
data,
|
||||
@@ -84,6 +85,7 @@ export function ProductionListTable({loading, data, refetch, bodyshop, technicia
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
technician,
|
||||
refetch,
|
||||
state,
|
||||
data: data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
@@ -262,6 +264,7 @@ export function ProductionListTable({loading, data, refetch, bodyshop, technicia
|
||||
state={state}
|
||||
setState={setState}
|
||||
setColumns={setColumns}
|
||||
refetch={refetch}
|
||||
data={data}
|
||||
/>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {useTranslation} from "react-i18next";
|
||||
import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import {generateInternalReflections} from "./report-center-modal-utils";
|
||||
|
||||
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
|
||||
|
||||
export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
|
||||
return (
|
||||
@@ -31,10 +31,9 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')}
|
||||
style={{marginTop: '10px'}}>
|
||||
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
|
||||
<Form.List name={["filters"]}>
|
||||
{(fields, {add, remove, move}) => {
|
||||
{(fields, {add, remove}) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
@@ -72,7 +71,8 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
dependencies={[['filters', field.name, "field"]]}>
|
||||
dependencies={[['filters', field.name, "field"],['filters', field.name, "value"]]}
|
||||
>
|
||||
{
|
||||
() => {
|
||||
const name = form.getFieldValue(['filters', field.name, "field"]);
|
||||
@@ -82,7 +82,6 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
key={`${index}operator`}
|
||||
label={t('reportcenter.labels.advanced_filters_filter_operator')}
|
||||
name={[field.name, "operator"]}
|
||||
dependencies={[]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -92,20 +91,32 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
>
|
||||
<Select
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
options={getWhereOperatorsByType(type)}/>
|
||||
options={ getWhereOperatorsByType(type)}
|
||||
onChange={() => {
|
||||
// Clear related Fields
|
||||
|
||||
form.setFieldValue(['filters', field.name, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item
|
||||
dependencies={[['filters', field.name, "field"]]}>
|
||||
<Form.Item dependencies={[
|
||||
['filters', field.name, "field"],
|
||||
['filters', field.name, "operator"]
|
||||
]}
|
||||
>
|
||||
{
|
||||
() => {
|
||||
// Because it looks cleaner than inlining.
|
||||
const name = form.getFieldValue(['filters', field.name, "field"]);
|
||||
const type = filters.find(f => f.name === name)?.type;
|
||||
const reflector = filters.find(f => f.name === name)?.reflector;
|
||||
const operator = form.getFieldValue(['filters', field.name, "operator"]);
|
||||
const operatorType = operator ? getWhereOperatorsByType(type).find((o) => o.value === operator)?.type : null;
|
||||
|
||||
return <Form.Item
|
||||
key={`${index}value`}
|
||||
@@ -131,14 +142,29 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
return generateInternalReflections({
|
||||
bodyshop,
|
||||
upperPath,
|
||||
finalPath
|
||||
finalPath,
|
||||
t
|
||||
});
|
||||
};
|
||||
|
||||
const reflections = reflector ? generateReflections(reflector) : [];
|
||||
const fieldPath = [[field.name, "value"]];
|
||||
|
||||
// We have reflections so we will use a select box
|
||||
if (reflections.length > 0) {
|
||||
// We have reflections and the operator type is array, so we will use a select box with multiple options
|
||||
if (operatorType === "array") {
|
||||
return (
|
||||
<Select
|
||||
disabled={!operator}
|
||||
mode="multiple"
|
||||
options={reflections}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
onChange={(value) => {
|
||||
form.setFieldValue(fieldPath, value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
options={reflections}
|
||||
@@ -150,16 +176,50 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
);
|
||||
}
|
||||
|
||||
// We have a type of number, so we will use a number input
|
||||
if (type === "number") {
|
||||
return (
|
||||
<InputNumber
|
||||
disabled={!operator}
|
||||
onChange={(value) => form.setFieldValue(fieldPath, value)}/>
|
||||
);
|
||||
}
|
||||
|
||||
// We have a type of date, so we will use a date picker
|
||||
if (type === "date") {
|
||||
return (
|
||||
<FormDatePicker
|
||||
disabled={!operator}
|
||||
onChange={(date) => form.setFieldValue(fieldPath, date)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// we have a type of boolean, so we will use a select box with a true or false option.
|
||||
if (type === "boolean" || type === "bool") {
|
||||
return (
|
||||
<Select
|
||||
disabled={!operator}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
options={[
|
||||
{
|
||||
label: t('reportcenter.labels.advanced_filters_true'),
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: t('reportcenter.labels.advanced_filters_false'),
|
||||
value: false
|
||||
}
|
||||
]}
|
||||
onChange={(value) => form.setFieldValue(fieldPath, value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}/>
|
||||
disabled={!operator}
|
||||
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}
|
||||
/>
|
||||
);
|
||||
})()
|
||||
}
|
||||
@@ -206,13 +266,12 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function SortersSection({sorters, form}) {
|
||||
function SortersSection({sorters}) {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')}
|
||||
style={{marginTop: '10px'}}>
|
||||
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
|
||||
<Form.List name={["sorters"]}>
|
||||
{(fields, {add, remove, move}) => {
|
||||
{(fields, {add, remove}) => {
|
||||
return (
|
||||
<div>
|
||||
Sorters
|
||||
|
||||
@@ -8,6 +8,20 @@ import {uniqBy} from "lodash";
|
||||
*/
|
||||
const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj);
|
||||
|
||||
/**
|
||||
* Generate options from array
|
||||
* @param bodyshop
|
||||
* @param path
|
||||
* @returns {unknown[]}
|
||||
*/
|
||||
const generateOptionsFromArray = (bodyshop, path) => {
|
||||
const options = getValueFromPath(bodyshop, path);
|
||||
return uniqBy(options.map((value) => ({
|
||||
label: value,
|
||||
value: value,
|
||||
})), 'value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid internal reflections
|
||||
* Note: This is intended for future functionality
|
||||
@@ -42,19 +56,44 @@ const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
|
||||
* Generate special reflections
|
||||
* @param bodyshop
|
||||
* @param finalPath
|
||||
* @param t - i18n
|
||||
* @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]}
|
||||
*/
|
||||
const generateSpecialReflections = (bodyshop, finalPath) => {
|
||||
const generateSpecialReflections = (bodyshop, finalPath, t) => {
|
||||
switch (finalPath) {
|
||||
case 'payment_payers':
|
||||
return [
|
||||
{
|
||||
label: t("payments.labels.customer"),
|
||||
value: t("payments.labels.customer"),
|
||||
},
|
||||
{
|
||||
label: t("payments.labels.insurance"),
|
||||
value: t("payments.labels.insurance"),
|
||||
},
|
||||
// This is a weird one supposedly only used by one shop and could potentially be
|
||||
// placed behind a SplitSDK
|
||||
{
|
||||
label: t("payments.labels.external"),
|
||||
value: t("payments.labels.external"),
|
||||
}
|
||||
];
|
||||
case 'payment_types':
|
||||
return generateOptionsFromArray(bodyshop, 'md_payment_types');
|
||||
case 'alt_transports':
|
||||
return generateOptionsFromArray(bodyshop, 'appt_alt_transport');
|
||||
case 'lost_sale_reasons':
|
||||
return generateOptionsFromArray(bodyshop, 'md_lost_sale_reasons');
|
||||
// Special case because Referral Sources is an Array, not an Object.
|
||||
case 'referral_source':
|
||||
return generateOptionsFromArray(bodyshop, 'md_referral_sources');
|
||||
case 'class':
|
||||
return generateOptionsFromArray(bodyshop, 'md_classes');
|
||||
case 'cost_centers':
|
||||
return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name');
|
||||
// Special case because Categories is an Array, not an Object.
|
||||
case 'categories':
|
||||
const catOptions = getValueFromPath(bodyshop, 'md_categories');
|
||||
return uniqBy(catOptions.map((value) => ({
|
||||
label: value,
|
||||
value: value,
|
||||
})), 'value');
|
||||
return generateOptionsFromArray(bodyshop, 'md_categories');
|
||||
case 'insurance_companies':
|
||||
return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name');
|
||||
case 'employee_teams':
|
||||
@@ -105,12 +144,13 @@ const generateBodyshopReflections = (bodyshop, finalPath) => {
|
||||
* @param bodyshop
|
||||
* @param upperPath
|
||||
* @param finalPath
|
||||
* @param t - i18n
|
||||
* @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]}
|
||||
*/
|
||||
const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
|
||||
const generateInternalReflections = ({bodyshop, upperPath, finalPath, t}) => {
|
||||
switch (upperPath) {
|
||||
case 'special':
|
||||
return generateSpecialReflections(bodyshop, finalPath);
|
||||
return generateSpecialReflections(bodyshop, finalPath, t);
|
||||
case 'bodyshop':
|
||||
return generateBodyshopReflections(bodyshop, finalPath);
|
||||
default:
|
||||
@@ -118,4 +158,4 @@ const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
|
||||
}
|
||||
};
|
||||
|
||||
export {generateInternalReflections,}
|
||||
export {generateInternalReflections}
|
||||
@@ -30,8 +30,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ScheduleJobModalContainer({
|
||||
@@ -142,7 +142,7 @@ export function ScheduleJobModalContainer({
|
||||
operation: AuditTrailMapping.appointmentinsert(
|
||||
DateTimeFormat(values.start)
|
||||
),
|
||||
});
|
||||
type: "appointmentinsert",});
|
||||
}
|
||||
|
||||
if (!!appt.errors) {
|
||||
|
||||
@@ -26,6 +26,8 @@ export function ScoreboardDayStats({bodyshop, date, entries}) {
|
||||
return acc + value.bodyhrs;
|
||||
}, 0);
|
||||
|
||||
const numJobs = entries.length;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={dayjs(date).format("D - ddd")}
|
||||
@@ -34,17 +36,18 @@ export function ScoreboardDayStats({bodyshop, date, entries}) {
|
||||
>
|
||||
<Statistic
|
||||
valueStyle={{color: dailyBodyTarget > bodyHrs ? "red" : "green"}}
|
||||
label="B"
|
||||
label="Body"
|
||||
value={bodyHrs.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
valueStyle={{color: dailyPaintTarget > paintHrs ? "red" : "green"}}
|
||||
label="P"
|
||||
label="Refinish"
|
||||
value={paintHrs.toFixed(1)}
|
||||
/>
|
||||
<Divider style={{margin: 0}}/>
|
||||
|
||||
<Statistic value={(bodyHrs + paintHrs).toFixed(1)}/>
|
||||
<Statistic label="Total" value={(bodyHrs + paintHrs).toFixed(1)}/>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Statistic label="Jobs" value={numJobs} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,227 +26,260 @@ export function ScoreboardTargetsTable({bodyshop, scoreBoardlist}) {
|
||||
const values = useMemo(() => {
|
||||
const dateHash = _.groupBy(scoreBoardlist, "date");
|
||||
|
||||
let ret = {
|
||||
todayBody: 0,
|
||||
todayPaint: 0,
|
||||
weeklyPaint: 0,
|
||||
weeklyBody: 0,
|
||||
toDateBody: 0,
|
||||
toDatePaint: 0,
|
||||
};
|
||||
let ret = {
|
||||
todayBody: 0,
|
||||
todayPaint: 0,
|
||||
todayJobs: 0,
|
||||
weeklyPaint: 0,
|
||||
weeklyJobs: 0,
|
||||
weeklyBody: 0,
|
||||
toDateBody: 0,
|
||||
toDatePaint: 0,
|
||||
toDateJobs: 0,
|
||||
};
|
||||
|
||||
const today = dayjs();
|
||||
if (dateHash[today.format("YYYY-MM-DD")]) {
|
||||
dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.todayBody = ret.todayBody + d.bodyhrs;
|
||||
ret.todayPaint = ret.todayPaint + d.painthrs;
|
||||
});
|
||||
}
|
||||
const today = dayjs();
|
||||
if (dateHash[today.format("YYYY-MM-DD")]) {
|
||||
dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.todayBody = ret.todayBody + d.bodyhrs;
|
||||
ret.todayPaint = ret.todayPaint + d.painthrs;
|
||||
ret.todayJobs++;
|
||||
});
|
||||
}
|
||||
|
||||
let StartOfWeek = dayjs().startOf("week");
|
||||
while (StartOfWeek.isSameOrBefore(today)) {
|
||||
if (dateHash[StartOfWeek.format("YYYY-MM-DD")]) {
|
||||
dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
|
||||
ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
|
||||
});
|
||||
}
|
||||
StartOfWeek = StartOfWeek.add(1, "day");
|
||||
}
|
||||
let StartOfWeek = dayjs().startOf("week");
|
||||
while (StartOfWeek.isSameOrBefore(today)) {
|
||||
if (dateHash[StartOfWeek.format("YYYY-MM-DD")]) {
|
||||
dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
|
||||
ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
|
||||
ret.weeklyJobs++;
|
||||
});
|
||||
}
|
||||
StartOfWeek = StartOfWeek.add(1, "day");
|
||||
}
|
||||
|
||||
let startOfMonth = dayjs().startOf("month");
|
||||
while (startOfMonth.isSameOrBefore(today)) {
|
||||
if (dateHash[startOfMonth.format("YYYY-MM-DD")]) {
|
||||
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.toDateBody = ret.toDateBody + d.bodyhrs;
|
||||
ret.toDatePaint = ret.toDatePaint + d.painthrs;
|
||||
});
|
||||
}
|
||||
startOfMonth = startOfMonth.add(1, "day");
|
||||
}
|
||||
let startOfMonth = dayjs().startOf("month");
|
||||
while (startOfMonth.isSameOrBefore(today)) {
|
||||
if (dateHash[startOfMonth.format("YYYY-MM-DD")]) {
|
||||
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.toDateBody = ret.toDateBody + d.bodyhrs;
|
||||
ret.toDatePaint = ret.toDatePaint + d.painthrs;
|
||||
ret.toDateJobs++;
|
||||
});
|
||||
}
|
||||
startOfMonth = startOfMonth.add(1, "day");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}, [scoreBoardlist]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("scoreboard.labels.targets")}
|
||||
extra={<ScoreboardJobsList scoreBoardlist={scoreBoardlist}/>}
|
||||
>
|
||||
<Row gutter={rowGutter}>
|
||||
<Col xs={24} sm={{offset: 0, span: 4}} lg={{span: 4}}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.workingdays")}
|
||||
value={Util.CalculateWorkingDaysThisMonth()}
|
||||
prefix={<CalendarOutlined/>}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={{offset: 0, span: 20}} lg={{offset: 0, span: 20}}>
|
||||
<Row>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.dailytarget")}
|
||||
value={bodyshop.scoreboard_target.dailyBodyTarget}
|
||||
prefix="B"
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.dailyactual")}
|
||||
value={values.todayBody.toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.weeklytarget")}
|
||||
value={Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.weeklyactual")}
|
||||
value={values.weeklyBody.toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.monthlytarget")}
|
||||
value={Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.asoftodaytarget")}
|
||||
value={Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.todateactual")}
|
||||
value={values.toDateBody.toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={bodyshop.scoreboard_target.dailyPaintTarget}
|
||||
prefix="P"
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.todayPaint.toFixed(1)}/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.weeklyPaint.toFixed(1)}/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.toDatePaint.toFixed(1)}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Divider style={{margin: 5}}/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}></Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.todayPaint + values.todayBody).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(
|
||||
Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
) +
|
||||
Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)
|
||||
).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(
|
||||
Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
) +
|
||||
Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)
|
||||
).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(
|
||||
Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
) +
|
||||
Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)
|
||||
).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card
|
||||
title={t("scoreboard.labels.targets")}
|
||||
extra={<ScoreboardJobsList scoreBoardlist={scoreBoardlist} />}
|
||||
>
|
||||
<Row gutter={rowGutter}>
|
||||
<Col xs={24} sm={{ offset: 0, span: 4 }} lg={{ span: 4 }}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.workingdays")}
|
||||
value={Util.CalculateWorkingDaysThisMonth()}
|
||||
prefix={<CalendarOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={{ offset: 0, span: 20 }} lg={{ offset: 0, span: 20 }}>
|
||||
<Row>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.dailytarget")}
|
||||
value={bodyshop.scoreboard_target.dailyBodyTarget}
|
||||
prefix={t("scoreboard.labels.bodyabbrev")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.dailyactual")}
|
||||
value={values.todayBody.toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.weeklytarget")}
|
||||
value={Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.weeklyactual")}
|
||||
value={values.weeklyBody.toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.monthlytarget")}
|
||||
value={Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.asoftodaytarget")}
|
||||
value={Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.todateactual")}
|
||||
value={values.toDateBody.toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={bodyshop.scoreboard_target.dailyPaintTarget}
|
||||
prefix={t("scoreboard.labels.refinishabbrev")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.todayPaint.toFixed(1)} />
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.weeklyPaint.toFixed(1)} />
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.toDatePaint.toFixed(1)} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Divider style={{ margin: 5 }} />
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={"\u00A0"}
|
||||
prefix={t("scoreboard.labels.total")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.todayPaint + values.todayBody).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(
|
||||
Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
) +
|
||||
Util.WeeklyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)
|
||||
).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(
|
||||
Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
) +
|
||||
Util.MonthlyTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)
|
||||
).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(
|
||||
Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget,
|
||||
bodyshop
|
||||
) +
|
||||
Util.AsOfTodayTargetHrs(
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
bodyshop
|
||||
)
|
||||
).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Divider style={{ margin: 5 }} />
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={"\u00A0"}
|
||||
prefix={t("scoreboard.labels.jobs")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.todayJobs} />
|
||||
</Col>
|
||||
<Col {...statSpans} />
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.weeklyJobs} />
|
||||
</Col>
|
||||
<Col {...statSpans} />
|
||||
<Col {...statSpans} />
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.toDateJobs} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ScoreboardTargetsTable);
|
||||
|
||||
@@ -40,6 +40,7 @@ export const INSERT_AUDIT_TRAIL = gql`
|
||||
bodyshopid
|
||||
created
|
||||
operation
|
||||
type
|
||||
useremail
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,14 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
|
||||
import {OwnerNameDisplayFunction} from "../../components/owner-name-display/owner-name-display.component";
|
||||
import {auth} from "../../firebase/firebase.utils";
|
||||
import {QUERY_JOB_EXPORT_DMS} from "../../graphql/jobs.queries";
|
||||
import {setBreadcrumbs, setSelectedHeader,} from "../../redux/application/application.actions";
|
||||
import {
|
||||
insertAuditTrail,
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import InstanceRenderManager from '../../utils/instanceRenderMgr';
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -27,6 +32,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
||||
@@ -46,7 +53,12 @@ export const socket = SocketIO(
|
||||
}
|
||||
);
|
||||
|
||||
export function DmsContainer({bodyshop, setBreadcrumbs, setSelectedHeader}) {
|
||||
export function DmsContainer({
|
||||
bodyshop,
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const {t} = useTranslation();
|
||||
const [logLevel, setLogLevel] = useState("DEBUG");
|
||||
const history = useNavigate();
|
||||
@@ -104,6 +116,10 @@ export function DmsContainer({bodyshop, setBreadcrumbs, setSelectedHeader}) {
|
||||
notification.success({
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: payload,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",});
|
||||
history("/manage/accounting/receivables");
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import AlertComponent from "../../components/alert/alert.component";
|
||||
import {QUERY_EXPORT_LOG_PAGINATED} from "../../graphql/accounting.queries";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import {DateTimeFormatter} from "../../utils/DateFormatter";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "./../../utils/sorters";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -34,11 +35,42 @@ export function ExportLogsPageComponent({bodyshop}) {
|
||||
limit: pageLimit,
|
||||
order: [
|
||||
{
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
...(sortcolumn === "ro_number"
|
||||
? {
|
||||
job: {
|
||||
[sortcolumn|| "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
: "desc",},
|
||||
}
|
||||
: sortcolumn === "invoice_number"
|
||||
? {
|
||||
bill: {
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
}
|
||||
: sortcolumn === "paymentnum"
|
||||
? {
|
||||
payment: {
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
}
|
||||
: {
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -68,7 +100,8 @@ export function ExportLogsPageComponent({bodyshop}) {
|
||||
title: t("general.labels.created_at"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
render: (text, record) => (
|
||||
sorter: (a, b) => dateSort(a.created_at, b.created_at),
|
||||
sortOrder: sortcolumn === "created_at" && sortorder,render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
@@ -81,7 +114,8 @@ export function ExportLogsPageComponent({bodyshop}) {
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||
render: (text, record) =>
|
||||
record.job && (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||
@@ -93,7 +127,8 @@ export function ExportLogsPageComponent({bodyshop}) {
|
||||
title: t("bills.fields.invoice_number"),
|
||||
dataIndex: "invoice_number",
|
||||
key: "invoice_number",
|
||||
render: (text, record) =>
|
||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||
sortOrder: sortcolumn === "invoice_number" && sortorder,render: (text, record) =>
|
||||
record.bill && (
|
||||
<Link to={"/manage/bills?billid=" + (record.bill && record.bill.id)}>
|
||||
{record.bill && record.bill.invoice_number}
|
||||
@@ -103,7 +138,8 @@ export function ExportLogsPageComponent({bodyshop}) {
|
||||
{
|
||||
title: t("payments.fields.paymentnum"),
|
||||
dataIndex: "paymentnum",
|
||||
key: "paymentnum",
|
||||
key: "paymentnum",sorter: (a, b) => alphaSort(a.paymentnum, b.paymentnum),
|
||||
sortOrder: sortcolumn === "paymentnum" && sortorder,
|
||||
render: (text, record) =>
|
||||
record.payment && (
|
||||
<Link
|
||||
@@ -119,7 +155,13 @@ export function ExportLogsPageComponent({bodyshop}) {
|
||||
{
|
||||
title: t("general.labels.successful"),
|
||||
dataIndex: "successful",
|
||||
key: "successful",
|
||||
key: "successful",sorter: (a, b) => Number(a.successful) - Number(b.successful),
|
||||
sortOrder: sortcolumn === "successful" && sortorder,
|
||||
filters: [
|
||||
{ text: "True", value: true },
|
||||
{ text: "False", value: false },
|
||||
],
|
||||
onFilter: (value, record) => record.successful === value,
|
||||
render: (text, record) => (
|
||||
<Checkbox disabled checked={record.successful}/>
|
||||
),
|
||||
|
||||
@@ -48,11 +48,11 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsCloseComponent({job, bodyshop, jobRO, insertAuditTrail,}) {
|
||||
export function JobsCloseComponent({job, bodyshop, jobRO, insertAuditTrail}) {
|
||||
const {t} = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const client = useApolloClient();
|
||||
@@ -118,7 +118,7 @@ export function JobsCloseComponent({job, bodyshop, jobRO, insertAuditTrail,}) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobinvoiced(),
|
||||
});
|
||||
type: "jobinvoiced",});
|
||||
// history(`/manage/jobs/${job.id}`);
|
||||
} else {
|
||||
setLoading(false);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {createStructuredSelector} from "reselect";
|
||||
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
|
||||
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
|
||||
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
|
||||
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
|
||||
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
|
||||
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component";
|
||||
@@ -51,7 +52,6 @@ import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
import _ from "lodash";
|
||||
import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component";
|
||||
import {DateTimeFormat} from "../../utils/DateFormatter";
|
||||
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||
|
||||
@@ -62,8 +62,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPrintCenterContext: (context) =>
|
||||
dispatch(setModalContext({context: context, modal: "printCenter"})),
|
||||
insertAuditTrail: ({jobid, operation}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation})),
|
||||
insertAuditTrail: ({jobid, operation, type}) =>
|
||||
dispatch(insertAuditTrail({jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsDetailPage({
|
||||
@@ -205,7 +205,7 @@ export function JobsDetailPage({
|
||||
? DateTimeFormat(changedAuditFields[key])
|
||||
: changedAuditFields[key]
|
||||
),
|
||||
});
|
||||
type: "jobfieldchange",});
|
||||
});
|
||||
|
||||
await refetch();
|
||||
@@ -344,7 +344,7 @@ export function JobsDetailPage({
|
||||
{
|
||||
key: 'lifecycle',
|
||||
icon: <BarsOutlined/>,
|
||||
label: t('menus.jobsdetail.lifecycle'),
|
||||
label: t("menus.jobsdetail.lifecycle"),
|
||||
children: <JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses}/>
|
||||
},
|
||||
{
|
||||
|
||||
@@ -54,9 +54,9 @@ export const setOnline = (isOnline) => ({
|
||||
payload: isOnline,
|
||||
});
|
||||
|
||||
export const insertAuditTrail = ({jobid, billid, operation}) => ({
|
||||
export const insertAuditTrail = ({jobid, billid, operation, type}) => ({
|
||||
type: ApplicationActionTypes.INSERT_AUDIT_TRAIL,
|
||||
payload: {jobid, billid, operation},
|
||||
payload: {jobid, billid, operation, type },
|
||||
});
|
||||
export const setProblemJobs = (problemJobs) => ({
|
||||
type: ApplicationActionTypes.SET_PROBLEM_JOBS,
|
||||
|
||||
@@ -263,7 +263,7 @@ export function* onInsertAuditTrail() {
|
||||
}
|
||||
|
||||
export function* insertAuditTrailSaga({
|
||||
payload: {jobid, billid, operation},
|
||||
payload: {jobid, billid, operation, type},
|
||||
}) {
|
||||
const state = yield select();
|
||||
const bodyshop = state.user.bodyshop;
|
||||
@@ -275,18 +275,18 @@ export function* insertAuditTrailSaga({
|
||||
jobid,
|
||||
billid,
|
||||
operation,
|
||||
useremail: currentUser.email,
|
||||
type,useremail: currentUser.email,
|
||||
},
|
||||
};
|
||||
yield client.mutate({
|
||||
mutation: INSERT_AUDIT_TRAIL,
|
||||
variables,
|
||||
update(cache, {data}) {
|
||||
update(cache, { data }) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
audit_trail(existingAuditTrail, {readField}) {
|
||||
audit_trail(existingAuditTrail, { readField }) {
|
||||
const newAuditTrail = cache.writeQuery({
|
||||
data: data.insert_audit_trail_one,
|
||||
data: data,
|
||||
query: INSERT_AUDIT_TRAIL,
|
||||
variables,
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,8 @@ const AuditTrailMapping = {
|
||||
i18n.t("audit_trail.messages.appointmentcancel", {lost_sale_reason}),
|
||||
appointmentinsert: (start) =>
|
||||
i18n.t("audit_trail.messages.appointmentinsert", {start}),
|
||||
billdeleted: (invoice_number) =>
|
||||
i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
|
||||
billposted: (invoice_number) =>
|
||||
i18n.t("audit_trail.messages.billposted", {invoice_number}),
|
||||
billupdated: (invoice_number) =>
|
||||
@@ -30,6 +32,7 @@ const AuditTrailMapping = {
|
||||
i18n.t("audit_trail.messages.jobintake", {status, email,scheduled_completion}),
|
||||
jobdelivery: (status, email, actual_completion) =>
|
||||
i18n.t("audit_trail.messages.jobdelivery", {status, email,actual_completion}),
|
||||
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
|
||||
jobfieldchange: (field, value) =>
|
||||
i18n.t("audit_trail.messages.jobfieldchanged", {field, value}),
|
||||
jobimported: () => i18n.t("audit_trail.messages.jobimported"),
|
||||
@@ -57,6 +60,8 @@ const AuditTrailMapping = {
|
||||
jobstatuschange: (status) =>
|
||||
i18n.t("audit_trail.messages.jobstatuschange", {status}),
|
||||
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
|
||||
jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }),
|
||||
jobvoid: () => i18n.t("audit_trail.messages.jobvoid"),
|
||||
};
|
||||
|
||||
export default AuditTrailMapping;
|
||||
|
||||
@@ -2,22 +2,66 @@ import {Kind, parse, print, visit} from "graphql";
|
||||
import client from "./GraphQLClient";
|
||||
import {gql} from "@apollo/client";
|
||||
|
||||
/* eslint-disable no-loop-func */
|
||||
|
||||
/**
|
||||
* The available operators for filtering (string)
|
||||
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
|
||||
*/
|
||||
const STRING_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
{value: "_like", label: "contains"},
|
||||
{value: "_nlike", label: "does not contain"},
|
||||
{value: "_ilike", label: "contains case-insensitive"},
|
||||
{value: "_nilike", label: "does not contain case-insensitive"}
|
||||
{value: "_nilike", label: "does not contain case-insensitive"},
|
||||
{value: "_in", label: "in", type: "array"},
|
||||
{value: "_nin", label: "not in", type: "array"}
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for filtering (dates)
|
||||
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
|
||||
*/
|
||||
const DATE_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
{value: "_gt", label: "greater than"},
|
||||
{value: "_lt", label: "less than"},
|
||||
{value: "_gte", label: "greater than or equal"},
|
||||
{value: "_lte", label: "less than or equal"},
|
||||
{value: "_in", label: "in", type: "array"},
|
||||
{value: "_nin", label: "not in", type: "array"}
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for filtering (booleans)
|
||||
* @type {[{label: string, value: string},{label: string, value: string}]}
|
||||
*/
|
||||
const BOOLEAN_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for filtering (numbers)
|
||||
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
|
||||
*/
|
||||
const NUMBER_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
{value: "_gt", label: "greater than"},
|
||||
{value: "_lt", label: "less than"},
|
||||
{value: "_gte", label: "greater than or equal"},
|
||||
{value: "_lte", label: "less than or equal"}
|
||||
{value: "_lte", label: "less than or equal"},
|
||||
{value: "_in", label: "in", type: "array"},
|
||||
{value: "_nin", label: "not in", type: "array"}
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for sorting
|
||||
* @type {[{label: string, value: string},{label: string, value: string}]}
|
||||
*/
|
||||
const ORDER_BY_OPERATORS = [
|
||||
{value: "asc", label: "ascending"},
|
||||
{value: "desc", label: "descending"}
|
||||
@@ -31,7 +75,6 @@ export function getOrderOperatorsByType() {
|
||||
return ORDER_BY_OPERATORS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the available operators for filtering
|
||||
* @param type
|
||||
@@ -40,13 +83,14 @@ export function getOrderOperatorsByType() {
|
||||
export function getWhereOperatorsByType(type = 'string') {
|
||||
const operators = {
|
||||
string: STRING_OPERATORS,
|
||||
number: NUMBER_OPERATORS
|
||||
number: NUMBER_OPERATORS,
|
||||
boolean: BOOLEAN_OPERATORS,
|
||||
bool: BOOLEAN_OPERATORS,
|
||||
date: DATE_OPERATORS
|
||||
};
|
||||
return operators[type];
|
||||
}
|
||||
|
||||
/* eslint-disable no-loop-func */
|
||||
|
||||
/**
|
||||
* Parse a GraphQL query into an AST
|
||||
* @param query
|
||||
@@ -78,11 +122,9 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
|
||||
// Parse the query and apply the filters and sorters
|
||||
const ast = parseQuery(templateQueryToExecute);
|
||||
|
||||
let filterFields = [];
|
||||
|
||||
if (templateObject?.filters && templateObject?.filters?.length) {
|
||||
applyFilters(ast, templateObject.filters, filterFields);
|
||||
wrapFiltersInAnd(ast, filterFields);
|
||||
applyFilters(ast, templateObject.filters);
|
||||
}
|
||||
|
||||
if (templateObject?.sorters && templateObject?.sorters?.length) {
|
||||
@@ -109,7 +151,6 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
|
||||
return {contextData, useShopSpecificTemplate};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply sorters to the AST
|
||||
* @param ast
|
||||
@@ -170,10 +211,59 @@ export function applySorters(ast, sorters) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Top Level Sub to the AST
|
||||
* @param node
|
||||
* @param fieldPath
|
||||
* @param filterField
|
||||
*/
|
||||
function applyTopLevelSub(node, fieldPath, filterField) {
|
||||
// Find or create the where argument for the top-level subfield
|
||||
let whereArg = node.selectionSet.selections
|
||||
.find(selection => selection.name.value === fieldPath[0])
|
||||
?.arguments.find(arg => arg.name.value === 'where');
|
||||
|
||||
if (!whereArg) {
|
||||
whereArg = {
|
||||
kind: Kind.ARGUMENT,
|
||||
name: {kind: Kind.NAME, value: 'where'},
|
||||
value: {kind: Kind.OBJECT, fields: []},
|
||||
};
|
||||
const topLevelSubSelection = node.selectionSet.selections.find(selection =>
|
||||
selection.name.value === fieldPath[0]
|
||||
);
|
||||
if (topLevelSubSelection) {
|
||||
topLevelSubSelection.arguments = topLevelSubSelection.arguments || [];
|
||||
topLevelSubSelection.arguments.push(whereArg);
|
||||
}
|
||||
}
|
||||
|
||||
// Correctly position the nested filter without an extra 'where'
|
||||
if (fieldPath.length > 2) { // More than one level deep
|
||||
let currentField = whereArg.value;
|
||||
fieldPath.slice(1, -1).forEach((path, index) => {
|
||||
let existingField = currentField.fields.find(f => f.name.value === path);
|
||||
if (!existingField) {
|
||||
existingField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: path},
|
||||
value: {kind: Kind.OBJECT, fields: []}
|
||||
};
|
||||
currentField.fields.push(existingField);
|
||||
}
|
||||
currentField = existingField.value;
|
||||
});
|
||||
currentField.fields.push(filterField);
|
||||
} else { // Directly under the top level
|
||||
whereArg.value.fields.push(filterField);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters to the AST
|
||||
* @param ast
|
||||
* @param filters
|
||||
* @returns {ASTNode}
|
||||
*/
|
||||
export function applyFilters(ast, filters) {
|
||||
return visit(ast, {
|
||||
@@ -182,192 +272,197 @@ export function applyFilters(ast, filters) {
|
||||
filters.forEach(filter => {
|
||||
const fieldPath = filter.field.split('.');
|
||||
let topLevel = false;
|
||||
let topLevelSub = false;
|
||||
|
||||
// Determine if the filter should be applied at the top level
|
||||
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
|
||||
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
|
||||
if (fieldPath.length === 2) {
|
||||
topLevel = true;
|
||||
}
|
||||
|
||||
if (topLevel) {
|
||||
// Construct the filter for a top-level application
|
||||
const targetFieldName = fieldPath[fieldPath.length - 1];
|
||||
const filterValue = {
|
||||
kind: getGraphQLKind(filter.value),
|
||||
value: filter.value,
|
||||
};
|
||||
|
||||
const nestedFilter = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: targetFieldName},
|
||||
value: {
|
||||
kind: Kind.OBJECT,
|
||||
fields: [{
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: filter.operator},
|
||||
value: filterValue,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
// Find or create the where argument for the top-level field
|
||||
let whereArg = node.selectionSet.selections
|
||||
.find(selection => selection.name.value === fieldPath[0])
|
||||
?.arguments.find(arg => arg.name.value === 'where');
|
||||
|
||||
if (!whereArg) {
|
||||
whereArg = {
|
||||
kind: Kind.ARGUMENT,
|
||||
name: {kind: Kind.NAME, value: 'where'},
|
||||
value: {kind: Kind.OBJECT, fields: []},
|
||||
};
|
||||
const topLevelSelection = node.selectionSet.selections.find(selection =>
|
||||
selection.name.value === fieldPath[0]
|
||||
);
|
||||
if (topLevelSelection) {
|
||||
topLevelSelection.arguments = topLevelSelection.arguments || [];
|
||||
topLevelSelection.arguments.push(whereArg);
|
||||
}
|
||||
}
|
||||
|
||||
// Correctly position the nested filter without an extra 'where'
|
||||
if (fieldPath.length > 2) { // More than one level deep
|
||||
let currentField = whereArg.value;
|
||||
fieldPath.slice(1, -1).forEach((path, index) => {
|
||||
let existingField = currentField.fields.find(f => f.name.value === path);
|
||||
if (!existingField) {
|
||||
existingField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: path},
|
||||
value: {kind: Kind.OBJECT, fields: []}
|
||||
};
|
||||
currentField.fields.push(existingField);
|
||||
}
|
||||
currentField = existingField.value;
|
||||
});
|
||||
currentField.fields.push(nestedFilter);
|
||||
} else { // Directly under the top level
|
||||
whereArg.value.fields.push(nestedFilter);
|
||||
}
|
||||
} else {
|
||||
// Initialize a reference to the current selection to traverse down the AST
|
||||
let currentSelection = node;
|
||||
let whereArgFound = false;
|
||||
|
||||
// Iterate over the fieldPath, except for the last entry, to navigate the structure
|
||||
for (let i = 0; i < fieldPath.length - 1; i++) {
|
||||
const fieldName = fieldPath[i];
|
||||
let fieldFound = false;
|
||||
|
||||
// Check if the current selection has a selectionSet and selections
|
||||
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
|
||||
// Look for the field in the current selection's selections
|
||||
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
|
||||
if (selection) {
|
||||
// Move down the AST to the found selection
|
||||
currentSelection = selection;
|
||||
fieldFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the field was not found in the current path, it's an issue
|
||||
if (!fieldFound) {
|
||||
console.error(`Field ${fieldName} not found in the current selection.`);
|
||||
return; // Exit the loop and function due to error
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, currentSelection should be the parent field where the filter needs to be applied
|
||||
// Check if the 'where' argument already exists in the current selection
|
||||
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
|
||||
if (whereArg) {
|
||||
whereArgFound = true;
|
||||
} else {
|
||||
// If not found, create a new 'where' argument for the current selection
|
||||
currentSelection.arguments.push({
|
||||
kind: Kind.ARGUMENT,
|
||||
name: {kind: Kind.NAME, value: 'where'},
|
||||
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter
|
||||
});
|
||||
}
|
||||
|
||||
// Assuming the last entry in fieldPath is the field to apply the filter on
|
||||
const targetField = fieldPath[fieldPath.length - 1];
|
||||
const filterValue = {
|
||||
kind: getGraphQLKind(filter.value),
|
||||
value: filter.value,
|
||||
};
|
||||
|
||||
// Construct the filter field object
|
||||
const filterField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: targetField},
|
||||
value: {
|
||||
kind: Kind.OBJECT,
|
||||
fields: [{
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: filter.operator},
|
||||
value: filterValue,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
// Add the filter field to the 'where' clause of the current selection
|
||||
if (whereArgFound) {
|
||||
whereArg.value.fields.push(filterField);
|
||||
} else {
|
||||
// If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter
|
||||
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
|
||||
}
|
||||
if (fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
|
||||
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
|
||||
topLevelSub = true;
|
||||
}
|
||||
|
||||
// Construct the filter for a top-level application
|
||||
const targetFieldName = fieldPath[fieldPath.length - 1];
|
||||
|
||||
let filterValue = createFilterValue(filter);
|
||||
let filterField = createFilterField(targetFieldName, filter, filterValue);
|
||||
|
||||
if (topLevel) {
|
||||
applyTopLevelFilter(node, fieldPath, filterField);
|
||||
} else if (topLevelSub) {
|
||||
applyTopLevelSub(node, fieldPath, filterField);
|
||||
} else {
|
||||
applyNestedFilter(node, fieldPath, filterField);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter value based on the filter
|
||||
* @param filter
|
||||
* @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}}
|
||||
*/
|
||||
function createFilterValue(filter) {
|
||||
if (Array.isArray(filter.value)) {
|
||||
// If it's an array, create a list value with the array items
|
||||
return {
|
||||
kind: Kind.LIST,
|
||||
values: filter.value.map(item => ({
|
||||
kind: getGraphQLKind(item),
|
||||
value: item,
|
||||
})),
|
||||
};
|
||||
} else {
|
||||
// If it's not an array, use the existing logic
|
||||
return {
|
||||
kind: getGraphQLKind(filter.value),
|
||||
value: filter.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter field based on the target field and filter
|
||||
* @param targetFieldName
|
||||
* @param filter
|
||||
* @param filterValue
|
||||
* @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}}
|
||||
*/
|
||||
function createFilterField(targetFieldName, filter, filterValue) {
|
||||
return {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: targetFieldName},
|
||||
value: {
|
||||
kind: Kind.OBJECT,
|
||||
fields: [{
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: filter.operator},
|
||||
value: filterValue,
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a top-level filter to the AST
|
||||
* @param node
|
||||
* @param fieldPath
|
||||
* @param filterField
|
||||
*/
|
||||
function applyTopLevelFilter(node, fieldPath, filterField) {
|
||||
// Find or create the where argument for the top-level field
|
||||
let whereArg = node.selectionSet.selections
|
||||
.find(selection => selection.name.value === fieldPath[0])
|
||||
?.arguments.find(arg => arg.name.value === 'where');
|
||||
|
||||
if (!whereArg) {
|
||||
whereArg = {
|
||||
kind: Kind.ARGUMENT,
|
||||
name: {kind: Kind.NAME, value: 'where'},
|
||||
value: {kind: Kind.OBJECT, fields: []},
|
||||
};
|
||||
const topLevelSelection = node.selectionSet.selections.find(selection =>
|
||||
selection.name.value === fieldPath[0]
|
||||
);
|
||||
if (topLevelSelection) {
|
||||
topLevelSelection.arguments = topLevelSelection.arguments || [];
|
||||
topLevelSelection.arguments.push(whereArg);
|
||||
}
|
||||
}
|
||||
|
||||
// Correctly position the nested filter without an extra 'where'
|
||||
if (fieldPath.length > 2) { // More than one level deep
|
||||
let currentField = whereArg.value;
|
||||
fieldPath.slice(1, -1).forEach((path, index) => {
|
||||
let existingField = currentField.fields.find(f => f.name.value === path);
|
||||
if (!existingField) {
|
||||
existingField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: path},
|
||||
value: {kind: Kind.OBJECT, fields: []}
|
||||
};
|
||||
currentField.fields.push(existingField);
|
||||
}
|
||||
currentField = existingField.value;
|
||||
});
|
||||
currentField.fields.push(filterField);
|
||||
} else { // Directly under the top level
|
||||
whereArg.value.fields.push(filterField);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a nested filter to the AST
|
||||
* @param node
|
||||
* @param fieldPath
|
||||
* @param filterField
|
||||
*/
|
||||
function applyNestedFilter(node, fieldPath, filterField) {
|
||||
// Initialize a reference to the current selection to traverse down the AST
|
||||
let currentSelection = node;
|
||||
|
||||
// Iterate over the fieldPath, except for the last entry, to navigate the structure
|
||||
for (let i = 0; i < fieldPath.length - 1; i++) {
|
||||
const fieldName = fieldPath[i];
|
||||
let fieldFound = false;
|
||||
|
||||
// Check if the current selection has a selectionSet and selections
|
||||
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
|
||||
// Look for the field in the current selection's selections
|
||||
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
|
||||
if (selection) {
|
||||
// Move down the AST to the found selection
|
||||
currentSelection = selection;
|
||||
fieldFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the field was not found in the current path, it's an issue
|
||||
if (!fieldFound) {
|
||||
console.error(`Field ${fieldName} not found in the current selection.`);
|
||||
return; // Exit the loop and function due to error
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, currentSelection should be the parent field where the filter needs to be applied
|
||||
// Check if the 'where' argument already exists in the current selection
|
||||
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
|
||||
if (!whereArg) {
|
||||
// If not found, create a new 'where' argument for the current selection
|
||||
currentSelection.arguments.push({
|
||||
kind: Kind.ARGUMENT,
|
||||
name: {kind: Kind.NAME, value: 'where'},
|
||||
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter
|
||||
});
|
||||
}
|
||||
|
||||
// Add the filter field to the 'where' clause of the current selection
|
||||
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GraphQL kind for a value
|
||||
* @param value
|
||||
* @returns {Kind|Kind.INT}
|
||||
*/
|
||||
function getGraphQLKind(value) {
|
||||
if (typeof value === 'number') {
|
||||
if (Array.isArray(value)) {
|
||||
return Kind.LIST;
|
||||
} else if (typeof value === 'number') {
|
||||
return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return Kind.BOOLEAN;
|
||||
} else if (typeof value === 'string') {
|
||||
return Kind.STRING;
|
||||
} else if (value instanceof Date) {
|
||||
return Kind.STRING; // GraphQL does not have a Date type, so we return it as a string
|
||||
}
|
||||
// Extend with more types as needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap filters in an 'and' object
|
||||
* @param ast
|
||||
* @param filterFields
|
||||
*/
|
||||
export function wrapFiltersInAnd(ast, filterFields) {
|
||||
visit(ast, {
|
||||
OperationDefinition: {
|
||||
enter(node) {
|
||||
node.selectionSet.selections.forEach((selection) => {
|
||||
let whereArg = selection.arguments.find(arg => arg.name.value === 'where');
|
||||
if (filterFields.length > 1) {
|
||||
const andFilter = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: '_and'},
|
||||
value: {kind: Kind.LIST, values: filterFields}
|
||||
};
|
||||
whereArg.value.fields.push(andFilter);
|
||||
} else if (filterFields.length === 1) {
|
||||
whereArg.value.fields.push(filterFields[0].fields[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable no-loop-func */
|
||||
/* eslint-enable no-loop-func */
|
||||
|
||||
@@ -259,28 +259,30 @@
|
||||
- active:
|
||||
_eq: true
|
||||
columns:
|
||||
- id
|
||||
- billid
|
||||
- bodyshopid
|
||||
- created
|
||||
- operation
|
||||
- id
|
||||
- jobid
|
||||
- new_val
|
||||
- old_val
|
||||
- operation
|
||||
- type
|
||||
- useremail
|
||||
- bodyshopid
|
||||
- jobid
|
||||
- billid
|
||||
select_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- billid
|
||||
- bodyshopid
|
||||
- created
|
||||
- id
|
||||
- jobid
|
||||
- new_val
|
||||
- old_val
|
||||
- operation
|
||||
- type
|
||||
- useremail
|
||||
- created
|
||||
- billid
|
||||
- bodyshopid
|
||||
- jobid
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."audit_trail" add column "type" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."audit_trail" add column "type" text
|
||||
null;
|
||||
1563
package-lock.json
generated
1563
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -18,45 +18,46 @@
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.515.0",
|
||||
"@aws-sdk/client-ses": "^3.515.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.515.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.525.0",
|
||||
"@aws-sdk/client-ses": "^3.525.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.525.0",
|
||||
"@azure/storage-blob": "^12.17.0",
|
||||
"@opensearch-project/opensearch": "^2.5.0",
|
||||
"aws4": "^1.12.0",
|
||||
"axios": "^1.6.5",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.20.2",
|
||||
"cloudinary": "^2.0.1",
|
||||
"cloudinary": "^2.0.2",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "2.8.5",
|
||||
"csrf": "^3.1.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"express": "^4.18.3",
|
||||
"firebase-admin": "^12.0.0",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-request": "^6.1.0",
|
||||
"graylog2": "^0.2.1",
|
||||
"inline-css": "^4.0.2",
|
||||
"intuit-oauth": "^4.0.0",
|
||||
"json-2-csv": "^5.2.0",
|
||||
"json-2-csv": "^5.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-mailjet": "^6.0.5",
|
||||
"node-persist": "^4.0.1",
|
||||
"node-quickbooks": "^2.0.43",
|
||||
"nodemailer": "^6.9.10",
|
||||
"node-quickbooks": "^2.0.44",
|
||||
"nodemailer": "^6.9.11",
|
||||
"phone": "^3.1.42",
|
||||
"recursive-diff": "^1.0.9",
|
||||
"rimraf": "^5.0.5",
|
||||
"soap": "^1.0.0",
|
||||
"socket.io": "^4.7.4",
|
||||
"ssh2-sftp-client": "^10.0.3",
|
||||
"stripe": "^14.18.0",
|
||||
"twilio": "^4.22.0",
|
||||
"stripe": "^14.19.0",
|
||||
"twilio": "^4.23.0",
|
||||
"uuid": "^9.0.1",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
|
||||
Reference in New Issue
Block a user