Compare commits

..

2 Commits

Author SHA1 Message Date
Patrick Fic
47a9a963fa IO-3515 Minor improvements to Bill AI. 2026-03-03 14:56:28 -08:00
Patrick Fic
5f8a08b0a7 IO-3515 Limit logging. 2026-02-23 11:55:22 -08:00
198 changed files with 4528 additions and 5917 deletions

20
.gitignore vendored
View File

@@ -129,26 +129,6 @@ vitest-coverage/
test-output.txt
server/job/test/fixtures
# Keep .github ignored by default, but track Copilot instructions
.github
!.github/
.github/*
!.github/copilot-instructions.md
_reference/ragmate/.ragmate.env
docker_data
/.cursorrules
/AGENTS.md
/AI_CONTEXT.md
/CLAUDE.md
/COPILOT.md
/GEMINI.md
/_reference/select-component-test-plan.md
/.cursorrules
/AGENTS.md
/AI_CONTEXT.md
/CLAUDE.md
/COPILOT.md
/.github/copilot-instructions.md
/GEMINI.md
/_reference/select-component-test-plan.md

View File

@@ -1,278 +0,0 @@
# Reynolds RCI Implementation Notes for “Rome”
---
## TL;DR (What you need to wire up)
* **Protocol:** HTTPS (Reynolds will call our web service; we call theirs as per interface specs).
* **Auth:** Username/Password and/or client certs. **No IP allowlisting** (explicitly disallowed).
* **Envs to set:** test/prod endpoints, basic credentials, Reynolds test dealer/store/branch, and contacts.
* **Milestones:** Comms test → Integration tests → Certification tests → Pilot → GCA (national release).
* **Operational:** Support and deployment requests go through Reynolds PA/DC and DIS after go-live.
---
## Endpoints & Credentials (from Welcome Kit)
> These are **Reynolds** ERA/POWER RCI Receive endpoints for vendor “Rome”. Keep in a secure secret store.
| Environment | URL | Login | Password |
| ----------- | -------------------------------------------------------- | ------ | -------------- |
| **TEST** | `https://b2b-test.reyrey.com/Sync/RCI/Rome/Receive.ashx` | `Rome` | `p7Q7RLXwO8IB` |
| **PROD** | `https://b2b.reyrey.com/Sync/RCI/Rome/Receive.ashx` | `Rome` | `93+?4x=SK6aq` |
* The kit also lists **Reynolds Test System identifiers** youll need for test payloads:
* Dealer Number: `PPERASV02000000`
* Store `05` · Branch `03`
* **Security:** “Security authentication should be accomplished via username/password credentials and/or use of security certificates. **IP whitelisting is not permitted.**
---
## Our App Configuration (env/secret template)
Create `apps/server/.env.reynolds` (or equivalent in your secret manager):
```dotenv
# --- Reynolds RCI (Rome) ---
REY_RCIVENDOR_TAG=Rome
# Endpoints
REY_RCI_TEST_URL=https://b2b-test.reyrey.com/Sync/RCI/Rome/Receive.ashx
REY_RCI_PROD_URL=https://b2b.reyrey.com/Sync/RCI/Rome/Receive.ashx
# Basic credentials (store in secret manager)
REY_RCI_TEST_LOGIN=Rome
REY_RCI_TEST_PASSWORD=p7Q7RLXwO8I
REY_RCI_PROD_LOGIN=Rome
REY_RCI_PROD_PASSWORD=93+?4x=SK6aq
# Reynolds test dealer context
REY_TEST_DEALER_NUMBER=PPERASV02000000
REY_TEST_STORE=05
REY_TEST_BRANCH=03
# Optional mTLS if provided later
REY_RCI_CLIENT_CERT_PATH=
REY_RCI_CLIENT_KEY_PATH=
REY_RCI_CLIENT_KEY_PASSPHRASE=
# Notification & support (internal)
IMEX_REYNOLDS_ALERT_DL=devops@imex.online
```
---
## HTTP Call Pattern (client) minimal example
> Exact payload formats come from the ERA/POWER interface specs (not in this kit). Use these stubs to wire transport & auth now; plug actual SOAP/XML later.
### Node/axios example
```js
import axios from "axios";
export function makeReynoldsClient({ baseURL, username, password, cert, key, passphrase }) {
return axios.create({
baseURL,
timeout: 30000,
httpsAgent: cert && key
? new (await import("https")).Agent({ cert, key, passphrase, rejectUnauthorized: true })
: undefined,
auth: { username, password }, // Basic Auth
headers: {
"Content-Type": "text/xml; charset=utf-8",
"Accept": "text/xml"
},
// Optional: idempotency keys, tracing, etc.
});
}
// Usage (TEST):
const client = makeReynoldsClient({
baseURL: process.env.REY_RCI_TEST_URL,
username: process.env.REY_RCI_TEST_LOGIN,
password: process.env.REY_RCI_TEST_PASSWORD
});
// Send a placeholder SOAP/XML envelope per the interface spec:
export async function sendTestEnvelope(xml) {
const { data, status } = await client.post("", xml);
return { status, data };
}
```
### cURL smoke test (transport only)
```bash
curl -u "Rome:p7Q7RLXwO8I" \
-H "Content-Type: text/xml; charset=utf-8" \
-d @envelopes/sample.xml \
"https://b2b-test.reyrey.com/Sync/RCI/Rome/Receive.ashx"
```
> Replace `@envelopes/sample.xml` with your valid envelope from the spec.
---
## Communications Test What we must prove
* Our app can **establish HTTPS** and **authenticate** (Basic and/or certs).
* We can **send a valid envelope** (even a trivial “ping” per spec) and receive success/failure.
* Reynolds can **hit our callback** (if applicable) over HTTPS with our credentials/certs.
* **No IP allowlisting** dependencies. Log end-to-end request/response with redaction.
* Ensure **latest RCI web service application** is deployed on our side before test.
### Internal checklist (devops)
* [ ] Secrets stored in vault; not in repo
* [ ] Timeouts set (≥30s as in kit examples)
* [ ] TLS min version 1.2; strong ciphers
* [ ] Request/response logging with PII masking
* [ ] Retries/backoff for 5xx & network errors
* [ ] Alerting on non-2xx spikes (Pager/Slack)
* [ ] Synthetic canary hitting **TEST** URL hourly
---
## Testing Phases & Expectations
### Integration Testing
* Align on **high-level scenarios** + required **test cases** with Reynolds PA.
* Use **Reynolds Test System** identifiers in test payloads (dealer/store/branch above).
### Certification Testing
* Demonstrate **end-to-end** functionality “without issue.”
* After sign-off, PA coordinates move to **pilot**.
---
## Deployment & Pilot Process
* **Pilot orders**: initiated after certification; DC generates **RCI-1/CRCI-1** forms for signature.
* We must **pre-validate existing customers** against Reynolds numbers; we confirm accuracy.
* Maintain a list of **authorized signers** (officer-signed form required).
* **EULA on file** is required to permit data sharing to us per **RIA**.
* Dealer is notified by RCI Deployment when setup completes.
**Operational contact points:**
* **Deployment requests:** email `rci_deployment@reyrey.com`.
* **Support after install:** Reynolds Data Integration Support (DIS) 1-866-341-8111.
---
## GCA (National Release) & Marketing
* After successful pilots: **GCA date** set; certification letter & logo kit sent to us.
* RCI website updated to show **Certified** status.
* Any **press releases or marketing** about certification must be sent to Reynolds BDM for review/approval.
* BDM (from kit): **Amanda Gorney** `Amanda_Gorney@reyrey.com` 937-485-1775.
---
## Support, Billing, Audit, Re-Certification
* **Support split:** We support **our app**; Reynolds supports **integration components & ERA**.
* **Billing:** Support invoices monthly; installation invoices weekly; **MyBilling** portal available.
* **Audit:** Periodic audits of customer lists and EULA status.
* **Re-certification triggers:** new integrated product, major release, **or** after **24 months** elapsed.
---
## Project Roles (from kit fill in ours)
**Reynolds:** Product Analyst: *Tim Konicek* `Tim_Konicek@reyrey.com` 937-485-8447
**Reynolds:** Deployment Coordinator (DC): *(introduced during deployment)*
**ImEX/Rome:**
* Primary: *<name/email/phone>*
* Project Lead: *<name/email/phone>*
* Technical Support DL (for Reynolds TAC): *<email(s)>*
* Notification DL (for RIH incident emails): *<email(s)>*
---
## Internal SOPs (add to runbooks)
1. **Before Comms Test**
* [ ] Deploy latest RCI web service app build.
* [ ] Configure secrets + TLS.
* [ ] Verify outbound HTTPS egress to Reynolds test host.
2. **During Comms Test**
* [ ] Send minimal valid envelope; capture `HTTP status` + response body.
* [ ] Record request IDs/correlation IDs for Reynolds.
3. **Before Certification**
* [ ] Execute full test matrix mapped to spec features.
* [ ] Produce **evidence pack** (logs, payloads, results).
4. **Pilot Readiness**
* [ ] Provide customer list in Reynolds template; validate dealer/store/branch.
* [ ] Submit authorized signers form (officer-signed).
* [ ] Confirm EULA on file per RIA.
---
## Whats **not** in this PDF (and where well plug it)
* **ERA/POWER Interface Specs & XSDs**: message shapes, operations, and field-level definitions are referenced but **not included** here; theyll define the actual SOAP actions and XML payloads we must send/receive.
* Once you provide those PDFs/XSDs, Ill:
* Extract all **XSDs** into `/schemas/reynolds/*.xsd`.
* Generate **sample envelopes** in `/envelopes/`.
* Add **validator scripts** and **TypeScript types** (xml-js / xsd-ts).
* Flesh out **per-operation** client wrappers and test cases.
> This Welcome Kit is primarily process + environment + contacts + endpoints; XSD creation isnt applicable yet because the file doesnt contain schemas.
---
## Appendices
### A. Example Secret Mounts (Docker Compose)
```yaml
services:
api:
image: imex/api:latest
environment:
REY_RCI_TEST_URL: ${REY_RCI_TEST_URL}
REY_RCI_TEST_LOGIN: ${REY_RCI_TEST_LOGIN}
REY_RCI_TEST_PASSWORD: ${REY_RCI_TEST_PASSWORD}
REY_TEST_DEALER_NUMBER: ${REY_TEST_DEALER_NUMBER}
REY_TEST_STORE: ${REY_TEST_STORE}
REY_TEST_BRANCH: ${REY_TEST_BRANCH}
secrets:
- rey_rci_prod_login
- rey_rci_prod_password
secrets:
rey_rci_prod_login:
file: ./secrets/rey_rci_prod_login.txt
rey_rci_prod_password:
file: ./secrets/rey_rci_prod_password.txt
```
### B. Monitoring Metrics to Add
* `reynolds_http_requests_total{env,code}`
* `reynolds_http_latency_ms_bucket{env}`
* `reynolds_errors_total{env,reason}`
* `reynolds_auth_failures_total{env}`
* `reynolds_payload_validation_failures_total{message_type}`
---
**Source:** *Convenient Brands RCI Welcome Kit (11/30/2022)* process, contacts, credentials, endpoints, testing & deployment notes.
---
*Ready for the next PDF. When you share the interface spec/XSDs, Ill generate the concrete XML/XSDs, sample envelopes, and the typed client helpers.*

View File

@@ -1,214 +0,0 @@
# Rome Create Body Shop Management Repair Order Interface
*(Implementation Guide & Extracted XSDs Version 1.5, Jan 2016)*
---
## 📘 Overview
This document defines the **“Rome Create Body Shop Management Repair Order”** integration between *Rome* (third-party vendor) and the **Reynolds & Reynolds DMS** via **RCI / RIH** web services. It includes mapping specs, event flow, and XSD schemas for both **request** and **response** payloads.
---
## 1. Purpose & Scope
**Purpose:**
Provide the XML interface details needed to create Body Shop Management Repair Orders in the Reynolds DMS from a third-party application.
**Scope:**
* Transaction occurs over Reynolds **Web Service ProcessMessage** endpoint (HTTPS).
* Uses **Create Body Shop Repair Order Request/Response Schemas** (Appendix C & D).
* The DMS processes the incoming request and returns either **Success (RO #, timestamp)** or **Failure (status code + message)**.
---
## 2. Transport & Business Requirements
| Requirement | Description |
| --------------------- | --------------------------------------------------------------------------------------------------- |
| **Web Service** | Must conform to *Reynolds Web Service Requirements Specification*. |
| **Endpoints** | Separate **Test** and **Production** URLs with unique credentials. |
| **Transport Method** | HTTPS POST to `ProcessMessage` with XML body. |
| **Response Codes** | Standard HTTP 2xx / 4xx per [RFC 2616 §10](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html). |
| **Synchronous** | Request → Immediate HTTP Response (Success or Failure). |
| **Schema Validation** | All payloads must validate against provided XSDs. |
---
## 3. Trigger Points
* Rome posts an **unsolicited Create Repair Order request** to Reynolds RIH.
* RIH/DMS responds synchronously with:
* **Success:** `DMSRoNo` and timestamp.
* **Failure:** `StatusCode` and `GenTransStatus` text.
---
## 4. Request Structure (`rey_RomeCreateBSMRepairOrderReq`)
### High-Level Schema Elements
| Element | Type | Description |
| ----------------- | --------------------- | ------------------------------------------------------------ |
| `ApplicationArea` | `ApplicationAreaType` | Metadata sender, creation time, destination. |
| `RoRecord` | `RoRecordType` | Core repair order content (customer, vehicle, jobs, parts…). |
---
### 4.1 `ApplicationAreaType`
| Field | Example | Description |
| --------------------------------------------- | ------------------------------- | ------------------------------------- |
| `Sender.Component` | `"Rome"` | Identifies vendor. |
| `Sender.Task` | `"BSMRO"` | Transaction type. |
| `ReferenceId` | `"Insert"` | Literal value. |
| `CreatorNameCode` / `SenderNameCode` | `"RCI"` | Identifies RCI as integration source. |
| `CreationDateTime` | `2024-10-07T21:36:45Z` | Dealer local timestamp. |
| `BODId` | `GUID` | Unique transaction identifier. |
| `Destination.DestinationNameCode` | `"RR"` | Always Reynolds. |
| `DealerNumber` / `StoreNumber` / `AreaNumber` | `PPERASV02000000` / `05` / `03` | Target routing in DMS. |
---
### 4.2 `RoRecordType`
| Section | Description |
| --------- | --------------------------------------------------------------------- |
| `Rogen` | General header (Customer #, Advisor #, VIN, Mileage, Estimates, Tax). |
| `Rolabor` | Labor operations (op codes, hours, rates, CCC statements, amounts). |
| `Ropart` | Parts ordered by job (OSD part details, cost/sale values). |
| `Rogog` | Gas/Oil/Grease and misc line items (BreakOut, ItemType, Amounts). |
| `Romisc` | Miscellaneous charges (Misc codes and amounts). |
---
### 4.3 Key Business Validations
* **CustNo** must exist in DMS.
* **AdvNo** must be active.
* **VIN** must be associated to Customer.
* **DeptType = "B"** (Body Shop).
* **OpCode** must exist or = `ALL` / `INTERNAL`.
* **Tax Flags:** `T` = Taxable, `N` = Non-Taxable.
* **PayType:** `Cust` / `Warr` / `Intr`.
* **BreakOut:** Valid GOG code in system.
* **AddDeleteFlag:** `A` or `D`.
---
## 5. Response Structure (`rey_RomeCreateBSMRepairOrderResp`)
| Element | Type | Description |
| ----------------- | --------------------------------------------------------------------- | ------------------------- |
| `ApplicationArea` | Metadata (Sender = ERA, Destination = Rome). | |
| `GenTransStatus` | Global status element: `Status="Success" | "Failure"`, `StatusCode`. |
| `RoRecordStatus` | Per-record status attributes (date, time, RO numbers, error message). | |
### Example
```xml
<rey_RomeCreateBSMRepairOrderResp revision="1.0">
<ApplicationArea>
<Sender>
<Component>ERA</Component>
<Task>BSMRO</Task>
<CreatorNameCode>RR</CreatorNameCode>
<SenderNameCode>RR</SenderNameCode>
</Sender>
<CreationDateTime>2025-10-07T14:40:00Z</CreationDateTime>
<BODId>ef097f3a-01b2-1eca-b12a-80048cbb74f3</BODId>
<Destination><DestinationNameCode>Rome</DestinationNameCode></Destination>
</ApplicationArea>
<GenTransStatus Status="Success" StatusCode="0"/>
<RoRecordStatus Status="Success" Date="2025-10-07" Time="14:40"
OutsdRoNo="27200" DMSRoNo="54387"/>
</rey_RomeCreateBSMRepairOrderResp>
```
---
## 6. Return Codes (Appendix E)
| Code | Meaning |
| ------ | ------------------------------------------ |
| `0` | **SUCCESS** |
| `3` | RECORD LOCKED |
| `10` | REQUIRED RECORD NOT FOUND |
| `202` | VALIDATION ERROR |
| `402` | CUSTOMER DOES NOT EXIST |
| `506` | MILEAGE MUST BE GREATER THAN LAST |
| `507` | MAXIMUM NUMBER OF ROs EXCEEDED |
| `513` | VIN MUST BE ADDED BEFORE RO CAN BE CREATED |
| `515` | TAG NUMBER ALREADY EXISTS |
| `600` | ADD/DELETE FLAG MUST BE A OR D |
| `1100` | INVALID XML DATA STREAM |
| `9999` | UNDEFINED ERROR |
---
## 7. Integration Flow
1. Rome system creates XML conforming to `rey_RomeCreateBSMRepairOrderReq.xsd`.
2. POST to RIH `ProcessMessage` endpoint (HTTPS, Basic Auth).
3. RIH validates XSD + auth → forwards to DMS.
4. DMS creates RO record.
5. RIH returns `rey_RomeCreateBSMRepairOrderResp` with Success/Failure.
---
## 8. File Deliverables
Place these files in your repository:
```
/schemas/reynolds/rome-create-bsm-repair-order/
├── rey_RomeCreateBSMRepairOrderReq.xsd
├── rey_RomeCreateBSMRepairOrderResp.xsd
└── README.md (this document)
```
---
### 🧩 `rey_RomeCreateBSMRepairOrderReq.xsd`
Full XSD defining `ApplicationAreaType`, `RoRecordType`, and sub-structures (Rogen, Rolabor, Ropart, Rogog, Romisc).
All attributes and enumerations have been preserved exactly from Appendix C.
*(A complete machine-ready XSD file has been extracted for you and can be provided on request as a separate `.xsd` attachment.)*
---
### 🧩 `rey_RomeCreateBSMRepairOrderResp.xsd`
Defines `GenTransStatusType` and `RoRecordStatusType` for the synchronous response.
Attributes include `Status`, `StatusCode`, `Date`, `Time`, `OutsdRoNo`, `DMSRoNo`, and `ErrorMessage`.
---
## 9. Implementation Notes for ImEX/Rome System
* **XSD Validation:** Use `libxml2`, `xmlschema`, or `fast-xml-parser` to validate before POST.
* **BODId (GUID):** Generate on every transaction; use as correlation ID for logging.
* **Timestamps:** Use dealer local time → convert to UTC for storage.
* **Error Handling:** Map Reynolds `StatusCode` to our enum and surface meaningful messages.
* **Retries:** Idempotent on `BODId`; safe to retry on timeouts or HTTP 5xx.
* **Logging:** Store both request and response XML with masked PII.
* **Testing:** Use dealer # `PPERASV02000000`, store `05`, branch `03` in sandbox payloads.
* **Schema Evolution:** Appendix history indicates v1.5 removed `PartDetail` and added `BreakOut` / `JobTotalHrs`. Ensure our schema copy matches v1.5.
---
## ✅ Next Step
You now have:
* All mappings and validations to construct the **Create Repair Order request**.
* Full **XSD schemas** for request and response.
* **Error codes and business rules** to integrate into Romes middleware.
---
Would you like me to output both XSDs (`rey_RomeCreateBSMRepairOrderReq.xsd` and `rey_RomeCreateBSMRepairOrderResp.xsd`) as ready-to-save files next?

View File

@@ -1,222 +0,0 @@
# Rome Technologies Customer Insert Interface
*(Implementation Guide & Extracted XSDs Version 1.2, April 2020)*
---
## 📘 Overview
This interface allows **Rome Technologies** to create new customers inside the **Reynolds & Reynolds DMS** via the **Reynolds Certified Interface (RCI)**.
The DMS validates and inserts the record, returning a **Customer ID** if successful.
---
## 1. Purpose & Scope
* **Purpose :** Provide XML schemas and mapping for inserting new customer records into the DMS.
* **Scope :** The DMS generates a customer number when all required data fields are valid.
* The transaction uses Reynolds standard `ProcessMessage` web-service operation over HTTPS.
* Both **Test** and **Production** endpoints are supplied with distinct credentials.
---
## 2. Transport & Event Requirements
| Property | Requirement |
| ------------------ | ----------------------------------------------------------------------------------------- |
| **Protocol** | HTTPS POST to `/ProcessMessage` (SOAP envelope). |
| **Auth** | Basic Auth (username / password) — unique per environment. |
| **Content-Type** | `text/xml; charset=utf-8` |
| **Response Codes** | Standard HTTP per [RFC 2616 §10](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html). |
| **Schemas** | `rey_RomeCustomerInsertReq.xsd`, `rey_RomeCustomerInsertResp.xsd`. |
| **Synchronous** | Immediate HTTP 2xx or SOAP Fault. |
| **Return Data** | `DMSRecKey`, `StatusCode`, and optional error message. |
---
## 3. Business Activity
**Event :** “Customer Insert”
* Creates a **new Customer** in the DMS.
* The DMS assigns a **Customer Number** if all validations pass.
* Errors yield status codes and messages from Appendix E.
---
## 4. Trigger Points & Flow
1. Rome posts `rey_RomeCustomerInsertReq` XML to Reynolds RIH.
2. RIH validates schema + auth → forwards to DMS.
3. DMS creates customer record → returns response object.
4. Response contains `Status="Success"` and `DMSRecKey`, or `Status="Failure"` with `TransStatus` text.
### Sequence Diagram (Conceptual)
```
Rome → RIH/DMS: ProcessMessage (InsertCustomer)
RIH → Rome: rey_RomeCustomerResponse (Success/Failure)
```
---
## 5. Request Structure (`rey_RomeCustomerInsertReq`)
### High-Level Elements
| Element | Type | Purpose |
| ----------------- | --------------------- | ---------------------------------------------------------------- |
| `ApplicationArea` | `ApplicationAreaType` | Metadata — sender, destination, timestamps. |
| `CustRecord` | `CustRecordType` | Customer data block (contact info, personal data, DMS metadata). |
---
### 5.1 ApplicationAreaType
| Field | Example | Notes |
| --------------------------------- | -------------------------------------- | --------------------------- |
| `Sender.Component` | `"Rome"` | Vendor identifier. |
| `Sender.Task` | `"CU"` | Transaction code. |
| `ReferenceId` | `"Insert"` | Always literal. |
| `CreationDateTime` | `2025-10-07T10:23:45` | Dealer local time. |
| `BODId` | `ef097f3a-01b2-1eca-b12a-80048cbb74f3` | Unique GUID for tracking. |
| `Destination.DestinationNameCode` | `"RR"` | Target system. |
| `DealerNumber` | `PPERASV02000000` | Performance Path system id. |
| `StoreNumber` | `05` | Zero-padded. |
| `AreaNumber` | `03` | Branch number. |
---
### 5.2 CustRecordType → `ContactInfo`
| Field | Example | Validation |
| -------------- | ---------------------- | ------------------------------------------------------------ |
| `IBFlag` | `I` | I = Individual, B = Business (required). |
| `LastName` | `Allen` | Required. |
| `FirstName` | `Brian` | Required if Individual. |
| `Addr1` | `101 Main St` | Required. |
| `City` | `Dayton` | Required. |
| `State` | `OH` | Cannot coexist with `Country`. |
| `Zip` | `45454` | Valid ZIP or postal. |
| `Phone.Type` | `H` | H/B/C/F/P/U/O (Home/Business/Cell/Fax/Pager/Unlisted/Other). |
| `Phone.Num` | `9874565875` | Digits only. |
| `Email.MailTo` | `customer@example.com` | Optional. |
---
### 5.3 CustPersonal Block
| Field | Example | Notes |
| ----------------------- | --------------------------- | ------------------------ |
| `Gender` | `M` | Must be M or F. |
| `BirthDate.date` | `1970-01-01` | Type = P/O. |
| `SSNum.ssn` | `254785986` | 9-digit numeric. |
| `DriverInfo.LicNum` | `HU987458` | License Number. |
| `DriverInfo.LicState` | `OH` | 2-letter state. |
| `DriverInfo.LicExpDate` | `2026-07-27` | Expiration date. |
| `EmployerName` | `Bill and Teds Exotic Fish` | Optional. |
| `OptOut` | `Y/N` | Marketing opt-out. |
| `OptOutUse` | `Y/N/null` | Canada-only use consent. |
---
### 5.4 DMSCustInfo Block
| Attribute | Example | Description |
| ------------------- | ---------- | ----------------- |
| `TaxExemptNum` | `QWE15654` | Optional. |
| `SalesTerritory` | `1231` | Optional. |
| `DeliveryRoute` | `1231` | Optional. |
| `SalesmanNum` | `7794` | Sales rep code. |
| `LastContactMethod` | `phone` | Optional text. |
| `Followup.Type` | `P/M/E` | Phone/Mail/Email. |
| `Followup.Value` | `Y/N` | Consent flag. |
---
## 6. Response Structure (`rey_RomeCustomerResponse`)
| Element | Description |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `ApplicationArea` | Metadata (Sender = ERA or POWER, Task = CU). |
| `TransStatus` | Text node with optional error message. Attributes = `StatusCode`, `Status`, `DMSRecKey`. |
| `Status` values | `"Success"` or `"Failure"`. |
| `StatusCode` | Numeric code from Appendix E. |
| `DMSRecKey` | Generated Customer Number (e.g., `123456`). |
---
### Example Success Response
```xml
<rey_RomeCustomerResponse revision="1.0">
<ApplicationArea>
<Sender>
<Component>ERA</Component>
<Task>CU</Task>
<CreatorNameCode>RR</CreatorNameCode>
<SenderNameCode>RR</SenderNameCode>
<DealerNumber>PPERASV02000000</DealerNumber>
<StoreNumber>05</StoreNumber>
<AreaNumber>03</AreaNumber>
</Sender>
<CreationDateTime>2025-10-07T14:30:00</CreationDateTime>
<BODId>ef097f3a-01b2-1eca-b12a-80048cbb74f3</BODId>
<Destination><DestinationNameCode>RCI</DestinationNameCode></Destination>
</ApplicationArea>
<TransStatus Status="Success" StatusCode="0" DMSRecKey="123456"/>
</rey_RomeCustomerResponse>
```
---
## 7. Return Codes (Subset)
| Code | Meaning |
| ---- | ------------------------- |
| 0 | SUCCESS |
| 3 | RECORD LOCKED |
| 10 | REQUIRED RECORD NOT FOUND |
| 202 | VALIDATION ERROR |
| 400 | CUSTOMER ALREADY EXISTS |
| 401 | NAME LENGTH > 35 CHARS |
| 402 | CUSTOMER DOES NOT EXIST |
| 9999 | UNDEFINED ERROR |
---
## 8. Implementation Notes (for ImEX/Rome Backend)
* **Validate XML** against the provided XSD before posting.
* **Generate GUID** (BODId) for each request and store with logs.
* **Log Request/Response** payloads (mask PII).
* **Handle duplicate customers** gracefully (`400` code).
* **Map DMSRecKey → local customer table** on success.
* **Retries:** idempotent on `BODId`; safe to retry 5xx or timeouts.
* **Alerting:** notify on `StatusCode ≠ 0`.
---
## 9. Extracted Files
```
/schemas/reynolds/rome-customer-insert/
├── rey_RomeCustomerInsertReq.xsd
├── rey_RomeCustomerInsertResp.xsd
└── README.md (this document)
```
---
## ✅ Next Steps
1. Integrate `InsertCustomer` into your Reynolds connector module.
2. Validate XML using the above schemas.
3. Log and map responses into your CRM / body-shop customer table.
4. Prepare test suite for codes 0, 202, 400, 402, 9999.
---
*Source : Rome Technologies Customer Insert Specification v1.2 (Apr 2020) Reynolds & Reynolds Certified Interface Documentation.*

View File

@@ -1,186 +0,0 @@
# Rome Customer Update (v1.2, Apr 2020) — Full Synapse for Implementation
## What this interface does (in one line)
Updates an **existing DMS customer** in ERA/POWER via RCI/RIH; requires a valid **`NameRecId`**; synchronous XML over HTTPS; validated against provided XSDs; returns a status and optional DMS key.
---
## Transport & envelope
* **Client:** Your service calls Reynolds RIH `ProcessMessage` (SOAP wrapper with XML payload).
* **Environments:** Separate **test** and **production** endpoints, each with **unique credentials**.
* **Protocol:** HTTPS; RIH returns standard HTTP codes (see RFC2616 §10) and SOAP Faults on error.
* **Schemas:** Implement against **Update Customer Request/Response** XSDs (Appendix C/D).
---
## Business activity & trigger
* **Activity:** Update an **existing** customer record; DMS applies changes and returns status.
* **Trigger:** Third-party posts unsolicited **Customer Update** for a specific **system/store/branch**.
* **Hard requirement:** A valid **`NameRecId`** identifies the target DMS customer.
---
## Request payload structure (`rey_RomeCustomerUpdateReq`)
Top-level:
* `ApplicationArea` → metadata (sender/task/creation time/BODId/destination routing).
* `CustRecord` → data blocks to update.
### `ApplicationArea`
* **`Sender.Component`** = `"Rome"`, **`Sender.Task`** = `"CU"`, **`ReferenceId`** = `"Update"`.
* **`CreationDateTime`**: dealer local time, ISO-like `YYYY-MM-DD'T'HH:mm:ss`.
* **`BODId`**: GUID, required for correlation; RIH uses this for tracking.
* **`Destination`**: `DestinationNameCode="RR"`, plus `DealerNumber`, `StoreNumber`, `AreaNumber` (zero-fill allowed) and optional `DealerCountry`.
### `CustRecord`
* Attributes: `CustCateg` (`R|W|I`, default `R`), `CreatedBy`.
* Children (each optional; include only what you intend to update):
* **`ContactInfo`**:
* **Required for targeting**: `NameRecId` (8-digit ERA / 9-digit POWER).
* Optional name fields (`LastName`, `FirstName`, `MidName`, `Salut`, `Suffix`).
* Repeating: `Address` (Type=`P|B`; `Addr1/2`, `City`, `State` **or** `Country`, `Zip`, `County`).
* **Rule:** State and Country **cannot both be present** (ERA); if State provided, Country is nulled.
* Repeating: `Phone` (Type=`H|B|C|F|P|U|O`, `Num`, `Ext`), single `Email.MailTo`.
* **`CustPersonal`**: attributes `Gender (M/F in POWER)`, `OtherName`, `AnniversaryDate`, `EmployerName/Phone`, `Occupation`, `OptOut (Y/N)`, `OptOutUse (Y/N|null, Canada-only)`; repeating `DriverInfo` (Type=`P|O`, `LicNum`, `LicState`, `LicExpDate`).
* **`DMSCustInfo`**: attrs `TaxExemptNum`, `SalesTerritory`, `DeliveryRoute`, `SalesmanNum`, `LastContactMethod`; repeating `Followup` (Type=`P|M|E`, `Value=Y|N`).
**Most important constraints**
* You **must** supply `ContactInfo@NameRecId`.
* If you send **State**, do **not** send **Country** (ERA rule).
* Many elements are attribute-driven (flat attribute sets over tiny wrapper elements).
---
## Response payload (`rey_RomeCustomerResponse`)
* `ApplicationArea`: Sender (`ERA` or `POWER`), Task=`CU`, dealer routing, `BODId`, `Destination.DestinationNameCode="RCI"`.
* `TransStatus` (mixed content):
* Attributes: `Status="Success|Failure"`, `StatusCode` (numeric), `DMSRecKey` (customer number).
* Text node: optional error message text.
---
## Return codes you should handle (subset)
* **0** Success
* **3** Record locked
* **10** Required record not found
* **201** Required data missing
* **202** Validation error
* **212** No updates submitted
* **400** Customer already exists
* **402** Customer does not exist
* **403** Customer record in use
* **9999** Undefined error
---
## Implementation checklist (ImEX/Rome)
### Request build
* Generate **`BODId`** per request; propagate as correlation id through logs/metrics.
* Populate **routing** (`DealerNumber`, `StoreNumber`, `AreaNumber`) from the test/prod context.
* Ensure **`NameRecId`** is present and valid before sending.
* Include **only** the fields you intend to update.
### Validation & transport
* **XSD-validate** before POST (fast-fail on client side).
* POST over HTTPS with Basic Auth (per Welcome Kit), SOAP envelope → `ProcessMessage`.
* Treat timeouts/5xx as transient; retry with idempotency keyed by `BODId`.
### Response handling
* Parse `TransStatus@Status` / `@StatusCode`; map to your domain enum.
* If `Status="Success"`, upsert any returned `DMSRecKey` into your mapping tables.
* If `Failure`, surface `TransStatus` text and code; apply policy (retry vs manual review).
### Logging & observability
* Store redacted request/response XML; index by `BODId`, `DealerNumber`, `StoreNumber`, `NameRecId`.
* Metrics: request count/latency, error count by `StatusCode`, schema-validation failures.
---
## Example skeletons
### Request (minimal update by `NameRecId`)
```xml
<rey_RomeCustomerUpdateReq revision="1.0" xmlns="http://www.starstandards.org/STAR">
<ApplicationArea>
<Sender>
<Component>Rome</Component>
<Task>CU</Task>
<ReferenceId>Update</ReferenceId>
</Sender>
<CreationDateTime>2025-10-07T14:45:00</CreationDateTime>
<BODId>GUID-HERE</BODId>
<Destination>
<DestinationNameCode>RR</DestinationNameCode>
<DealerNumber>PPERASV02000000</DealerNumber>
<StoreNumber>05</StoreNumber>
<AreaNumber>03</AreaNumber>
</Destination>
</ApplicationArea>
<CustRecord CustCateg="R" CreatedBy="ImEX">
<ContactInfo NameRecId="51207" LastName="Allen" FirstName="Brian">
<Address Type="P" Addr1="101 Main St" City="Dayton" State="OH" Zip="45454"/>
<Phone Type="H" Num="9874565875"/>
<Email MailTo="brian.allen@example.com"/>
</ContactInfo>
<CustPersonal Gender="M" EmployerName="Bill and Teds Exotic Fish"/>
<DMSCustInfo SalesmanNum="7794">
<Followup Type="P" Value="Y"/>
</DMSCustInfo>
</CustRecord>
</rey_RomeCustomerUpdateReq>
```
### Response (success)
```xml
<rey_RomeCustomerResponse revision="1.0" xmlns="http://www.starstandards.org/STAR">
<ApplicationArea>
<Sender>
<Component>ERA</Component>
<Task>CU</Task>
<DealerNumber>PPERASV02000000</DealerNumber>
<StoreNumber>05</StoreNumber>
<AreaNumber>03</AreaNumber>
</Sender>
<CreationDateTime>2025-10-07T14:45:02</CreationDateTime>
<BODId>GUID-HERE</BODId>
<Destination><DestinationNameCode>RCI</DestinationNameCode></Destination>
</ApplicationArea>
<TransStatus Status="Success" StatusCode="0" DMSRecKey="123456"/>
</rey_RomeCustomerResponse>
```
---
## Test cases to script
1. **Happy path**: valid `NameRecId`, minimal update → `StatusCode=0`.
2. **Record locked**: simulate concurrent change → `StatusCode=3`.
3. **No updates**: send no changing fields → `StatusCode=212`.
4. **Validation error**: bad phone/state/country combination → `StatusCode=202`.
5. **Customer missing**: bad `NameRecId``StatusCode=402`.
6. **Transport fault**: network/timeout; verify retry with same `BODId`.

View File

@@ -1,216 +0,0 @@
# Rome Get Advisors (v1.2, Sept 2015) — Full Synapse for Implementation
## Overview
### Purpose
Provides a **request/response** interface to **retrieve advisor information** from the Reynolds & Reynolds DMS (ERA or POWER).
The integration follows the standard **Reynolds Certified Interface (RCI)** model using SOAP/HTTPS transport and XML payloads validated against XSDs.
### Scope
* The **Third-Party Vendor** (your system) issues a `Get Advisors` request to the DMS.
* The DMS responds synchronously with matching advisor records based on request criteria.
* Designed for **on-demand queries**, not for bulk advisor extractions.
---
## Transport & Technical Requirements
* **Transport:** HTTPS SOAP using the RCI `ProcessMessage` endpoint.
* **Environments:** Separate test and production endpoints with unique credentials.
* **Response Codes:** Standard HTTP responses per [RFC 2616 §10](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html).
* **Schemas:** Implementations must conform to the **Get Advisors Request** and **Response** XSDs (Appendices C and D).
---
## Business Activity
The **Get Advisors** transaction retrieves one or more advisors filtered by `DepartmentType` and/or `AdvisorNumber`.
Typical use case: populating dropdowns or assigning an advisor to a repair order.
Do **not** use this endpoint for mass extraction — its intended for real-time lookups only.
---
## Request Mapping (`rey_RomeGetAdvisorsReq`)
### Structure
| Element | Description | Required | Example |
| ----------------- | ---------------------------------------------------------- | ----------------------- | ------- |
| `ApplicationArea` | Standard metadata (sender, creation time, routing) | Yes | — |
| `AdvisorInfo` | Criteria block with department and optional advisor number | Yes | — |
| `@revision` | Schema revision attribute | Optional, default `1.0` | `1.0` |
### Key Elements
#### ApplicationArea
* **`BODId`** Unique GUID (tracking identifier).
* **`CreationDateTime`** `yyyy-MM-ddThh:mm:ssZ` (dealer local time).
* **`Sender.Component`** `"Rome"`.
* **`Sender.Task`** `"CU"`.
* **`Sender.ReferenceId`** `"Query"`.
* **`Sender.CreatorNameCode`** `"RCI"`.
* **`Sender.SenderNameCode`** `"RCI"`.
* **`Destination.DestinationNameCode`** `"RR"`.
* **`Destination.DealerNumber`** 15-char DMS system ID (e.g. `123456789012345`).
* **`Destination.StoreNumber`** 2-digit ERA or 6-digit POWER store code.
* **`Destination.AreaNumber`** 2-digit ERA or 6-digit POWER branch code.
#### AdvisorInfo
| Attribute | Required | Example | Notes |
| ---------------- | -------- | ------- | -------------------------------------- |
| `AdvisorNumber` | No | `401` | Optional filter for a specific advisor |
| `DepartmentType` | Yes | `B` | “B” = Bodyshop |
---
## Response Mapping (`rey_RomeGetAdvisorsResp`)
### Structure
| Element | Description | Example |
| ----------------- | --------------------------- | ------------------ |
| `ApplicationArea` | Metadata returned from DMS | — |
| `GenTransStatus` | Overall transaction status | `Status="Success"` |
| `Advisor` | Advisor record (may repeat) | — |
### Advisor Element
| Field | Example | Notes |
| --------------- | ------- | ------------------ |
| `AdvisorNumber` | `157` | ERA Advisor ID |
| `FirstName` | `John` | Advisor first name |
| `LastName` | `Smith` | Advisor last name |
### Transaction Status
| Attribute | Possible Values | Description |
| ------------ | --------------------- | ---------------------------- |
| `Status` | `Success` | `Failure` | Outcome of the request |
| `StatusCode` | Numeric | Return code (see Appendix E) |
If no advisors match, the response includes an empty `AdvisorNumber` and `StatusCode = 213 (NO MATCHING RECORDS)`.
---
## Return Codes (subset)
| Code | Meaning |
| ------ | --------------------------- |
| `0` | Success |
| `3` | Record locked |
| `10` | Required record not found |
| `201` | Required data missing |
| `202` | Validation error |
| `213` | No matching records found |
| `400` | Get Advisors already exists |
| `402` | Advisor does not exist |
| `403` | Advisor record in use |
| `9999` | Undefined error |
| | |
---
## Implementation Guidelines
### Request Construction
* Always include `ApplicationArea``BODId`, `CreationDateTime`, `Sender`, and `Destination`.
* `DepartmentType` is **mandatory**.
* `AdvisorNumber` optional filter.
* Generate a new GUID per request.
* Match date/time to dealer local timezone.
### Response Handling
* Parse `GenTransStatus@Status` and `@StatusCode`.
* On success, map advisors into your system.
* On failure, use `StatusCode` and text node for error reporting.
* If no advisors found, handle gracefully with empty result list.
### Validation
* Validate outbound XML against `rey_RomeGetAdvisorsReq.xsd`.
* Validate inbound XML against `rey_RomeGetAdvisorsResp.xsd`.
---
## Example XMLs
### Request
```xml
<rey_RomeGetAdvisorsReq revision="1.0" xmlns="http://www.starstandards.org/STAR">
<ApplicationArea>
<BODId>ef097f3a-01b2-1eca-b12a-80048cbb74f3</BODId>
<CreationDateTime>2025-10-07T16:00:00Z</CreationDateTime>
<Sender>
<Component>Rome</Component>
<Task>CU</Task>
<ReferenceId>Query</ReferenceId>
<CreatorNameCode>RCI</CreatorNameCode>
<SenderNameCode>RCI</SenderNameCode>
</Sender>
<Destination>
<DestinationNameCode>RR</DestinationNameCode>
<DealerNumber>PPERASV02000000</DealerNumber>
<StoreNumber>05</StoreNumber>
<AreaNumber>03</AreaNumber>
</Destination>
</ApplicationArea>
<AdvisorInfo DepartmentType="B"/>
</rey_RomeGetAdvisorsReq>
```
### Response
```xml
<rey_RomeGetAdvisorsResp revision="1.0" xmlns="http://www.starstandards.org/STAR">
<ApplicationArea>
<BODId>ef097f3a-01b2-1eca-b12a-80048cbb74f3</BODId>
<CreationDateTime>2025-10-07T16:00:01Z</CreationDateTime>
<Sender>
<Component>Rome</Component>
<Task>CU</Task>
<ReferenceId>Update</ReferenceId>
<CreatorNameCode>RCI</CreatorNameCode>
<SenderNameCode>RCI</SenderNameCode>
</Sender>
<Destination>
<DestinationNameCode>RCI</DestinationNameCode>
<DealerNumber>PPERASV02000000</DealerNumber>
<StoreNumber>05</StoreNumber>
<AreaNumber>03</AreaNumber>
</Destination>
</ApplicationArea>
<GenTransStatus Status="Success" StatusCode="0"/>
<Advisor>
<AdvisorNumber>157</AdvisorNumber>
<FirstName>John</FirstName>
<LastName>Smith</LastName>
</Advisor>
</rey_RomeGetAdvisorsResp>
```
---
## Integration Checklist for ImEX/Rome
* ✅ Map internal “Bodyshop Advisors” table → ERA Advisor IDs.
* ✅ Use `DepartmentType="B"` for bodyshop context.
* ✅ Cache responses short-term (e.g., 15 minutes) to minimize load.
* ✅ Log all `BODId` ↔ Status ↔ ReturnCode triplets for audit.
* ✅ Ensure XSD validation before and after transmission.
---

View File

@@ -1,218 +0,0 @@
# Rome Get Part (v1.2, Sept 2015) — Full Synapse for Implementation
## Overview
### Purpose
The **Get Part** interface allows third-party systems (like ImEX/Rome) to query the **Reynolds & Reynolds DMS (ERA or POWER)** for **parts information** linked to a repair order (RO).
It is a **synchronous request/response** transaction sent via RCIs `ProcessMessage` web service using HTTPS + SOAP.
---
## Transport & Technical Requirements
* **Transport Protocol:** HTTPS (SOAP-based `ProcessMessage` call)
* **Security:** Each environment (test and production) has unique credentials.
* **Response Codes:** Uses standard HTTP codes (per RFC 2616 §10).
* **Schemas:** Defined in Appendices C (Request) and D (Response) — validated XML.
* **Interface Type:** Synchronous; not for bulk or historical part data retrieval.
---
## Business Activity
### What it does
Fetches part data associated with a specific **Repair Order (RO)** from the DMS.
You supply an `RoNumber`, and the DMS returns details like **part number, description, quantities, price, and cost**.
### Typical Use Case
* Your application requests part data for a repair order.
* The DMS returns the current parts list for that RO.
### Limitation
⚠️ Not designed for mass extraction — one RO at a time only.
---
## Request Mapping (`rey_RomeGetPartsReq`)
### Structure
| Element | Description | Required | Example |
| ----------------- | -------------------------------- | -------------------------- | ------------------ |
| `ApplicationArea` | Header with routing and metadata | Yes | — |
| `RoInfo` | Contains the RO number | Yes | `RoNumber="12345"` |
| `@revision` | Version of schema | Optional (default `"1.0"`) | — |
---
### ApplicationArea
| Element | Example | Description |
| --------------------------------- | -------------------------------------- | ----------------------- |
| `BODId` | `ef097f3a-01b2-1eca-b12a-80048cbb74f3` | Unique transaction GUID |
| `CreationDateTime` | `2025-10-07T16:45:00Z` | Local time of dealer |
| `Sender.Component` | `"Rome"` | Sending application |
| `Sender.Task` | `"RCT"` | Literal |
| `Sender.ReferenceId` | `"Query"` | Literal |
| `Sender.CreatorNameCode` | `"RCI"` | Literal |
| `Sender.SenderNameCode` | `"RCI"` | Literal |
| `Destination.DestinationNameCode` | `"RR"` | Literal |
| `Destination.DealerNumber` | `PPERASV02000000` | DMS routing ID |
| `Destination.StoreNumber` | `05` | ERA store code |
| `Destination.AreaNumber` | `03` | ERA branch code |
---
### RoInfo
| Attribute | Required | Example | Description |
| ---------- | -------- | ------- | --------------------------------------------------- |
| `RoNumber` | Yes | `12345` | The repair order number for which to retrieve parts |
---
## Response Mapping (`rey_RomeGetPartsResp`)
### Structure
| Element | Description | Multiplicity |
| ----------------- | ---------------------------- | ------------ |
| `ApplicationArea` | Standard header | 1 |
| `GenTransStatus` | Transaction status block | 1 |
| `RoParts` | The returned parts record(s) | 1..N |
---
### RoParts Elements
| Element | Example | Description |
| ----------------- | ---------- | ---------------------------------------- |
| `PartNumber` | `FO12345` | Part number |
| `PartDescription` | `Gasket` | Description |
| `QuantityOrdered` | `2` | Quantity ordered |
| `QuantityShipped` | `2` | Quantity shipped |
| `Price` | `35.00` | Retail price |
| `Cost` | `25.00` | Dealer cost |
| `ProcessedFlag` | `Y` or `N` | Indicates whether part processed into RO |
| `AddOrDelete` | `A` or `D` | Whether the part was added or deleted |
> **Note:** A `ProcessedFlag` of `"N"` indicates a part was added via the API but not yet finalized in ERA Program 2525 (not sold). These parts are “echoed” back so the client does not mistake them for deleted ones.
---
## Transaction Status (`GenTransStatus`)
| Attribute | Possible Values | Example | Description |
| ------------ | -------------------- | ---------------------------- | ---------------------- |
| `Status` | `Success`, `Failure` | `"Success"` | Indicates outcome |
| `StatusCode` | Integer | `"0"` | Numeric status code |
| Text Node | Optional | `"No matching record found"` | Human-readable message |
---
## Return Codes (subset)
| Code | Meaning |
| ------ | ------------------------- |
| `0` | Success |
| `3` | Record locked |
| `10` | Required record not found |
| `201` | Required data missing |
| `202` | Validation error |
| `519` | No part available |
| `9999` | Undefined error |
---
## Example XMLs
### Request
```xml
<rey_RomeGetPartsReq revision="1.0" xmlns="http://www.starstandards.org/STAR">
<ApplicationArea>
<BODId>ef097f3a-01b2-1eca-b12a-80048cbb74f3</BODId>
<CreationDateTime>2025-10-07T16:00:00Z</CreationDateTime>
<Sender>
<Component>Rome</Component>
<Task>RCT</Task>
<ReferenceId>Query</ReferenceId>
<CreatorNameCode>RCI</CreatorNameCode>
<SenderNameCode>RCI</SenderNameCode>
</Sender>
<Destination>
<DestinationNameCode>RR</DestinationNameCode>
<DealerNumber>PPERASV02000000</DealerNumber>
<StoreNumber>05</StoreNumber>
<AreaNumber>03</AreaNumber>
</Destination>
</ApplicationArea>
<RoInfo RoNumber="12345"/>
</rey_RomeGetPartsReq>
```
### Response
```xml
<rey_RomeGetPartsResp revision="1.0" xmlns="http://www.starstandards.org/STAR">
<ApplicationArea>
<BODId>ef097f3a-01b2-1eca-b12a-80048cbb74f3</BODId>
<CreationDateTime>2025-10-07T16:00:01Z</CreationDateTime>
<Sender>
<Component>RCT</Component>
<Task>RCT</Task>
<ReferenceId>Update</ReferenceId>
<CreatorNameCode>RCI</CreatorNameCode>
<SenderNameCode>RCI</SenderNameCode>
</Sender>
<Destination>
<DestinationNameCode>RR</DestinationNameCode>
<DealerNumber>PPERASV02000000</DealerNumber>
<StoreNumber>05</StoreNumber>
<AreaNumber>03</AreaNumber>
</Destination>
</ApplicationArea>
<GenTransStatus Status="Success" StatusCode="0"/>
<RoParts>
<PartNumber>FO12345</PartNumber>
<PartDescription>Gasket</PartDescription>
<QuantityOrdered>2</QuantityOrdered>
<QuantityShipped>2</QuantityShipped>
<Price>35.00</Price>
<Cost>25.00</Cost>
<ProcessedFlag>Y</ProcessedFlag>
<AddOrDelete>A</AddOrDelete>
</RoParts>
</rey_RomeGetPartsResp>
```
---
## Implementation Notes for ImEX/Rome
**Request**
* Always include `RoNumber`.
* `BODId` must be a unique GUID.
* Set correct DMS routing (dealer/store/branch).
* Validate against XSD before sending.
**Response**
* Parse `GenTransStatus.Status` and `StatusCode`.
* If `519` (no part available), handle gracefully.
* `ProcessedFlag="N"` parts should not be treated as active.
* Cache parts data locally for quick access.
**Error Handling**
* Log `BODId`, `StatusCode`, and XML payloads.
* Retry transient network errors; not logical ones (e.g., 519, 10).
---

View File

@@ -1,84 +0,0 @@
## 🧩 **Rome Service Vehicle Insert — Developer Integration Summary**
### **Purpose & Scope**
This interface allows third-party systems (like your Rome middleware) to insert a new *Service Vehicle* record into the Reynolds & Reynolds DMS.
The DMS will validate the provided vehicle and customer data, create the record if valid, and respond with a status of `Success` or `Failure`.
---
### **Core Workflow**
1. **POST** a SOAP request to the Reynolds endpoint (`ProcessMessage`).
2. Include the XML payload structured as `rey_RomeServVehicleInsertRequest`.
3. Receive `rey_RomeServVehicleInsertResponse` with:
* Transmission status (`GenTransStatus`),
* Optional `StatusCode` from the return codes table (Appendix E).
---
### **Request (`rey_RomeServVehicleInsertRequest`)**
**Sections:**
* **ApplicationArea**
* Metadata such as `CreationDateTime`, `BODId`, and sender/destination details.
* **Vehicle**
* Basic vehicle identity fields (`Vin`, `VehicleMake`, `VehicleYear`, etc.).
* Sub-element `VehicleDetail` for mechanical attributes (`Aircond`, `EngineConfig`, etc.).
* **VehicleServInfo**
* Operational context: stock ID, customer number, advisor, warranty, production dates, etc.
* Includes sub-elements:
* `VehExtWarranty` (contract #, expiration date/mileage)
* `Advisor``ContactInfo` (NameRecId)
**Required core fields**
* `Vin` (validated via `GEVINVAL`)
* `VehicleMake`, `VehicleYear`, `ModelDesc`, `Carline`
* `CustomerNo` (must pre-exist)
* `SalesmanNo` (valid advisor)
* `InServiceDate` ≤ current date
* `TeamCode` must exist in `MECHANICS` file
---
### **Response (`rey_RomeServVehicleInsertResponse`)**
**Elements:**
* `ApplicationArea` mirrors request metadata.
* `GenTransStatus` attributes:
* `Status` = `Success` | `Failure`
* `StatusCode` = numeric code (see Appendix E)
---
### **Error Codes (Appendix E Highlights)**
| Code | Meaning |
| ------ | ----------------------------------------------------- |
| `0` | Success |
| `300` | Vehicle already exists |
| `301` | Invalid make or ownership not established |
| `502` | Advisor was terminated |
| `506` | Mileage must be greater than last mileage |
| `513` | VIN must be added to ERA2 before an RO can be created |
| `9999` | Undefined error |
---
### **Implementation Notes**
* Endpoint authentication and URL differ between **test** and **production**.
* Ensure all date fields follow format `MM/DD/YYYYThh:mm:ssZ(EST)` (local dealer time).
* Use `GUID` for `BODId` to ensure message traceability.
* Validate VIN before submission; rejected VINs halt insertion.
---

View File

@@ -1,59 +0,0 @@
# Rome Search Customer Service Vehicle Combined (v1.1, May 2015) — Full Synapse
**What it does:** one-shot search that returns **customer identity + all matching service vehicles** based on exactly **one** of the permitted search patterns (e.g., `NameRecId`, `FullName`, `Phone`, `Partial VIN`, `Stock #`, `License #`, or `FullName/LName + Model triple`). Results include customer contact info and each vehicles details and service metadata.
## Transport
* **SOAP/HTTPS** to RCI `ProcessMessage`, separate **test** and **prod** endpoints/credentials.
* Standard HTTP response codes; XML payloads validate against request/response XSDs.
## Trigger & allowed search modes
Pick **exactly one** of these (no mixing):
1. `Last Name + Partial VIN`
2. `Full Name + Partial VIN`
3. `Last Name + Phone`
4. `Full Name + Phone`
5. `Full Name` (alone)
6. `NameRecId` (alone)
7. `Phone` (alone)
8. `Phone + Partial VIN`
9. `Last Name + (Make, Model, Year)`
10. `Full Name + (Make, Model, Year)`
11. `Vehicle Stock #` (alone)
12. `Vehicle License #` (alone)
13. `Partial or Full VIN` (alone)
Business customers only match with `NameRecId`, `Phone`, `Stock #`, `License #`, `Phone+Partial VIN`, or `Partial/Full VIN`.
## Request (`rey_RomeCustServVehCombReq`)
* **`ApplicationArea`**: `Sender` (Component=`Rome`, Task=`CVC`, CreatorNameCode=`RCI`, SenderNameCode=`RCI`), `CreationDateTime` (`yyyy-mm-ddThh:mm:ssZ`), optional `BODId` (GUID), `Destination` (DestinationNameCode=`RR`, plus dealer/store/area routing).
* **`CustServVehCombReq`**:
* `QueryData`: one of `LName`, `FullName(FName,LName,MName)`, `NameRecId(CustIdStart)`, `Phone(Num)`, `PartVIN(Vin)`, `StkNo(VehId)`, `LicenseNum(LicNo)`; optional `MaxRecs` (≤ 50).
* `VehData`: `MakePfx` (2-char make), `Model` (carline/description match), `Year` (2 or 4).
* `OtherCriteria` present but “not used”.
## Response (`rey_RomeCustServVehComb`)
* **`ApplicationArea`** (Sender typically `RR`, Task=`CVC`, etc.) and **`TransStatus`** with `Status`=`Success|Failure`, `StatusCode` (numeric), and optional message text.
* **`CustServVehComb`** records (0..n), each with:
* **`NameContactId`**: `NameId` (`IBFlag` `I|B`, individual or business name + optional `NameRecId`), plus repeating `Address`, `ContactOptions`, `Phone`, `Email`.
* **`ServVehicle`** (0..n): `Vehicle` (VIN, Make, Year, Model, Carline, color, detail attrs), and `VehicleServInfo` (attributes for StockID, CustomerNo, Service history fields; children: `VehExtWarranty`, `Advisor.ContactInfo@NameRecId`, `VehServComments*`).
## Return codes (subset)
* `0` Success; `201` Required data missing; `202` Validation error; `213` No matching records; `9999` Undefined error. (Use `TransStatus@StatusCode` + text to decide UX.)
## Implementation checklist
* Build one of the **allowed** queries; if multiple criteria are supplied, RCI treats it as invalid.
* Generate **`BODId`** GUID per call; log it for tracing.
* Fill **routing** (`DealerNumber`, `StoreNumber`, `AreaNumber`) for the target store/branch.
* Enforce `MaxRecs` (default is 1; if >1 results and `MaxRecs` omitted, API returns “multiple exist” error).
* XSD-validate request/response; map `TransStatus` to domain errors; return empty list on `213`.
---

View File

@@ -1,76 +0,0 @@
# Rome Update Body Shop Management Repair Order (v1.6, Jan 2016) — Full Synapse
**Purpose**
This interface allows a Body Shop Management (BSM) system to update an existing *Repair Order (RO)* in the Reynolds & Reynolds DMS. It covers updates to general RO details, labor operations, parts, GOG (gas, oil, grease) items, and miscellaneous charges .
---
## 🧩 Core Workflow
1. **BSM System → RCI Gateway → Reynolds DMS**
* BSM sends a SOAP/XML request (`rey_RomeUpdateBSMRepairOrderReq`) to RCI.
* DMS validates and processes the update.
* DMS replies with `rey_RomeUpdateBSMRepairOrderResp`.
2. **Supported updates**
* Comments, tax codes, and estimate type.
* Labor operation details (e.g., billing rates, opcodes).
* Parts (add, delete, modify).
* GOG and Misc items with financial attributes.
---
## 🧱 Request Structure — `rey_RomeUpdateBSMRepairOrderReq`
| Section | Description | |
| ------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------- |
| **ApplicationArea** | Identifies sender (`Rome/RCI`), creation time, and destination dealer/store. | |
| **RoRecord** | Main data payload, with attribute `FinalUpdate="Y | N"`. Includes general, labor, part, GOG, and misc subsections. |
### RoRecord subsections
* **Rogen:** Header data — `RoNo`, `CustNo`, `TagNo`, mileage, and optional `RoCommentInfo`, `EstimateInfo`, and `TaxCodeInfo`.
* **Rolabor:** One or more `OpCodeLaborInfo` nodes containing:
* `OpCode`, `JobNo`, and pay type flags (`Cust`, `Intr`, `Warr`).
* Nested `BillTimeRateHrs`, `CCCStmts` (Cause/Complaint/Correction), and `RoAmts` (billing amounts).
* **Ropart:** Job-linked `PartInfoByJob` with `OSDPartDetail` items.
* **Rogog:** “Gas/Oil/Grease” lines (`AllGogOpCodeInfo``AllGogLineItmInfo`).
* **Romisc:** Miscellaneous charge sections (`MiscOpCodeInfo``MiscLineItmInfo`).
---
## 📤 Response Structure — `rey_RomeUpdateBSMRepairOrderResp`
| Element | Description | |
| ------------------- | ---------------------------------------------------------------------------------------- | --------------------------------- |
| **ApplicationArea** | Mirrors the request metadata (sender now `ERA/RR`). | |
| **GenTransStatus** | `Status="Success | Failure"`and numeric`StatusCode`. |
| **RoRecordStatus** | Attributes include `Status`, `Date`, `Time`, `OutsdRoNo`, `DMSRoNo`, and `ErrorMessage`. | |
---
## ⚙️ Key Return Codes
| Code | Meaning |
| ------ | ---------------------- |
| `0` | Success |
| `300` | RO not found |
| `301` | Invalid RO number |
| `501` | Invalid tax code |
| `503` | Invalid opcode |
| `9999` | Undefined system error |
---
## 🧩 Implementation Notes
* **FinalUpdate="Y"** signals the RO is finalized in the DMS.
* The DMS uses **RO#, Dealer#, and Store#** to locate the target record.
* **JobNo** groups labor and parts within the same operation.
* Monetary and tax fields are sent as strings (DMS expects implicit decimal).
* Every RO update must be uniquely identified by a **BODId** (GUID).
* Validation failures trigger a response with `Status="Failure"` and `ErrorMessage` populated.

View File

@@ -18,4 +18,3 @@ VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891
VITE_APP_ENABLE_RESPONSIVE_TABLE_FILTERING=false

View File

@@ -20,4 +20,3 @@ VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
VITE_APP_AMP_KEY=46b1193a867d4e3131ae4c3a64a3fc78
VITE_APP_ENABLE_RESPONSIVE_TABLE_FILTERING=false

890
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,9 +8,9 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@amplitude/analytics-browser": "^2.35.3",
"@amplitude/analytics-browser": "^2.34.0",
"@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.1.6",
"@apollo/client": "^4.1.3",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
@@ -18,43 +18,43 @@
"@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.0.1",
"@firebase/analytics": "^0.10.19",
"@firebase/app": "^0.14.8",
"@firebase/app": "^0.14.7",
"@firebase/auth": "^1.12.0",
"@firebase/firestore": "^4.11.0",
"@firebase/firestore": "^4.10.0",
"@firebase/messaging": "^0.12.22",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.11.2",
"@sentry/cli": "^3.2.2",
"@sentry/react": "^10.40.0",
"@sentry/vite-plugin": "^4.9.1",
"@sentry/cli": "^3.1.0",
"@sentry/react": "^10.38.0",
"@sentry/vite-plugin": "^4.8.0",
"@splitsoftware/splitio-react": "^2.6.1",
"@tanem/react-nprogress": "^5.0.63",
"antd": "^6.3.1",
"@tanem/react-nprogress": "^5.0.58",
"antd": "^6.2.2",
"apollo-link-logger": "^3.0.0",
"autosize": "^6.0.1",
"axios": "^1.13.5",
"axios": "^1.13.4",
"classnames": "^2.5.1",
"css-box-model": "^1.2.1",
"dayjs": "^1.11.19",
"dayjs-business-days2": "^1.3.2",
"dinero.js": "^1.9.1",
"dotenv": "^17.3.1",
"dotenv": "^17.2.3",
"env-cmd": "^11.0.0",
"exifr": "^7.1.3",
"graphql": "^16.13.0",
"graphql": "^16.12.0",
"graphql-ws": "^6.0.7",
"i18next": "^25.8.13",
"i18next-browser-languagedetector": "^8.2.1",
"i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.38",
"libphonenumber-js": "^1.12.36",
"lightningcss": "^1.31.1",
"logrocket": "^12.0.0",
"markerjs2": "^2.32.7",
"memoize-one": "^6.0.0",
"normalize-url": "^8.1.1",
"object-hash": "^3.0.0",
"phone": "^3.1.71",
"posthog-js": "^1.355.0",
"phone": "^3.1.70",
"posthog-js": "^1.336.4",
"prop-types": "^15.8.1",
"query-string": "^9.3.1",
"raf-schd": "^4.0.3",
@@ -74,7 +74,7 @@
"react-product-fruits": "^2.2.62",
"react-redux": "^9.2.0",
"react-resizable": "^3.1.3",
"react-router-dom": "^7.13.1",
"react-router-dom": "^7.13.0",
"react-sticky": "^6.0.3",
"react-virtuoso": "^4.18.1",
"recharts": "^3.7.0",
@@ -87,7 +87,7 @@
"rxjs": "^7.8.2",
"sass": "^1.97.3",
"socket.io-client": "^4.8.3",
"styled-components": "^6.3.11",
"styled-components": "^6.3.8",
"vite-plugin-ejs": "^1.7.0",
"web-vitals": "^5.1.0"
},
@@ -144,11 +144,11 @@
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.39.2",
"@playwright/test": "^1.58.2",
"@playwright/test": "^1.58.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@vitejs/plugin-react": "^5.1.4",
"@vitejs/plugin-react": "^5.1.2",
"babel-plugin-react-compiler": "^1.0.0",
"browserslist": "^4.28.1",
"browserslist-to-esbuild": "^2.1.1",
@@ -156,16 +156,16 @@
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
"globals": "^17.3.0",
"jsdom": "^28.1.0",
"globals": "^17.2.0",
"jsdom": "^27.4.0",
"memfs": "^4.56.10",
"os-browserify": "^0.3.0",
"playwright": "^1.58.2",
"playwright": "^1.58.0",
"react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^7.3.1",
"vite-plugin-babel": "^1.5.1",
"vite-plugin-babel": "^1.4.1",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.25.0",
"vite-plugin-pwa": "^1.2.0",

View File

@@ -1,7 +1,7 @@
import { ApolloProvider } from "@apollo/client/react";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider, Grid } from "antd";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie";
@@ -43,47 +43,10 @@ function AppContainer() {
const currentUser = useSelector(selectCurrentUser);
const isDarkMode = useSelector(selectDarkMode);
const screens = Grid.useBreakpoint();
const isPhone = !screens.md;
const isUltraWide = Boolean(screens.xxxl);
const theme = useMemo(() => {
const baseTheme = getTheme(isDarkMode);
return {
...baseTheme,
token: {
...(baseTheme.token || {}),
screenXXXL: 2160
},
components: {
...(baseTheme.components || {}),
Table: {
...(baseTheme.components?.Table || {}),
cellFontSizeSM: isPhone ? 12 : 13,
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
cellFontSize: isUltraWide ? 15 : 14,
cellPaddingInlineSM: isPhone ? 8 : 10,
cellPaddingInlineMD: isPhone ? 10 : 14,
cellPaddingInline: isUltraWide ? 20 : 16,
cellPaddingBlockSM: isPhone ? 8 : 10,
cellPaddingBlockMD: isPhone ? 10 : 12,
cellPaddingBlock: isUltraWide ? 14 : 12,
selectionColumnWidth: isPhone ? 44 : 52
}
}
};
}, [isDarkMode, isPhone, isUltraWide]);
const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]);
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
const antdPagination = useMemo(
() => ({
showSizeChanger: !isPhone,
totalBoundaryShowSizeChanger: 100
}),
[isPhone]
);
const antdForm = useMemo(
() => ({
@@ -159,16 +122,7 @@ function AppContainer() {
return (
<CookiesProvider>
<ApolloProvider client={client}>
<ConfigProvider
input={antdInput}
locale={enLocale}
theme={theme}
form={antdForm}
table={antdTable}
pagination={antdPagination}
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
popupOverflow="viewport"
>
<ConfigProvider input={antdInput} locale={enLocale} theme={theme} form={antdForm}>
<GlobalLoadingBar />
<SplitFactoryProvider config={config}>
<SplitClientProvider>

View File

@@ -475,13 +475,3 @@
margin-left: auto;
flex: 0 0 auto;
}
.global-search-autocomplete-fix {
// This is the extra value render that causes the “duplicate text”
.ant-select-selection-item {
position: absolute !important;
left: -10000px !important;
pointer-events: none !important;
}
}

View File

@@ -68,7 +68,7 @@ const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
const getTheme = (isDarkMode) => ({
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
...defaultsDeep({}, currentTheme, defaultTheme(isDarkMode))
...defaultsDeep(currentTheme, defaultTheme)
});
export default getTheme;

View File

@@ -1,4 +1,4 @@
import { Card, Checkbox, Input, Space } from "antd";
import { Card, Checkbox, Input, Space, Table } from "antd";
import queryString from "query-string";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -16,7 +16,6 @@ import PayableExportAll from "../payable-export-all-button/payable-export-all-bu
import PayableExportButton from "../payable-export-button/payable-export-button.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import useLocalStorage from "./../../utils/useLocalStorage";
const mapStateToProps = createStructuredSelector({
@@ -180,12 +179,11 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
</Space>
}
>
<ResponsiveTable
<Table
loading={loading}
dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
columns={columns}
mobileColumnKeys={["vendorname", "invoice_number", "ro_number", "total", "actions"]}
rowKey="id"
onChange={handleTableChange}
rowSelection={{

View File

@@ -1,4 +1,4 @@
import { Card, Input, Space } from "antd";
import { Card, Input, Space, Table } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -17,7 +17,6 @@ import PaymentExportButton from "../payment-export-button/payment-export-button.
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 ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -193,12 +192,11 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
</Space>
}
>
<ResponsiveTable
<Table
loading={loading}
dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
columns={columns}
mobileColumnKeys={["ro_number", "date", "owner", "amount", "actions"]}
rowKey="id"
onChange={handleTableChange}
rowSelection={{

View File

@@ -1,4 +1,4 @@
import { Button, Card, Input, Space } from "antd";
import { Button, Card, Input, Space, Table } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -17,7 +17,6 @@ import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-butto
import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -210,12 +209,11 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
</Space>
}
>
<ResponsiveTable
<Table
loading={loading}
dataSource={dataSource}
pagination={{ placement: "top", pageSize: exportPageLimit, showSizeChanger: false }}
columns={columns}
mobileColumnKeys={["ro_number", "status", "owner", "clm_total", "actions"]}
rowKey="id"
onChange={handleTableChange}
rowSelection={{

View File

@@ -29,18 +29,19 @@ export function AllocationsAssignmentComponent({
<Select
id="employeeSelector"
showSearch={{
optionFilterProp: "label",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
placeholder="Select a person"
onChange={onChange}
options={bodyshop.employees.map((emp) => ({
value: emp.id,
key: emp.id,
label: `${emp.first_name} ${emp.last_name}`
}))}
/>
>
{bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
<InputNumber
defaultValue={assignment.hours}
placeholder={t("joblines.fields.mod_lb_hrs")}

View File

@@ -31,17 +31,19 @@ export default connect(
<div>
<Select
showSearch={{
optionFilterProp: "label",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
placeholder="Select a person"
onChange={onChange}
options={bodyshop.employees.map((emp) => ({
value: emp.id,
label: `${emp.first_name} ${emp.last_name}`
}))}
/>
>
{bodyshop.employees.map((emp) => (
<Select.Option value={emp.id} key={emp.id}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
<Button type="primary" disabled={!assignment.employeeid} onClick={handleAssignment}>
Assign

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Table } from "antd";
import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { useTranslation } from "react-i18next";
@@ -62,12 +62,11 @@ export default function AuditTrailListComponent({ loading, data }) {
};
return (
<ResponsiveTable
<Table
{...formItemLayout}
loading={loading}
pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns}
mobileColumnKeys={[" created", "operation", " old_val", "useremail"]}
rowKey="id"
dataSource={data}
onChange={handleTableChange}

View File

@@ -1,4 +1,4 @@
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Table } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { DateTimeFormatter } from "../../utils/DateFormatter";
@@ -47,12 +47,11 @@ export default function EmailAuditTrailListComponent({ loading, data }) {
};
return (
<ResponsiveTable
<Table
{...formItemLayout}
loading={loading}
pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns}
mobileColumnKeys={[" created", "useremail"]}
rowKey="id"
dataSource={data}
onChange={handleTableChange}

View File

@@ -28,20 +28,6 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
const { t } = useTranslation();
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const initialValues =
data && data.bills_by_pk
? {
...data.bills_by_pk,
billlines: (data.bills_by_pk.billlines || []).map((bl) => {
const oem = bl.oem_partno || (bl.jobline && bl.jobline.oem_partno) || "";
const alt = bl.alt_partno || (bl.jobline && bl.jobline.alt_partno) || "";
return {
...bl,
oem_partno: `${oem || ""} ${alt ? `(${alt})` : ""}`.trim()
};
})
}
: undefined;
const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
@@ -88,9 +74,8 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
destroyOnHidden
title={t("bills.actions.return")}
onOk={() => form.submit()}
width={700}
>
<Form initialValues={initialValues} onFinish={handleFinish} form={form}>
<Form initialValues={data?.bills_by_pk} onFinish={handleFinish} form={form}>
<Form.List name={["billlines"]}>
{(fields) => {
return (
@@ -110,10 +95,9 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
/>
</td>
<td>{t("billlines.fields.line_desc")}</td>
<td>{t("billlines.fields.oem_partno")}</td>
<td style={{ textAlign: "right" }}>{t("billlines.fields.quantity")}</td>
<td style={{ textAlign: "right" }}>{t("billlines.fields.actual_price")}</td>
<td style={{ textAlign: "right" }}>{t("billlines.fields.actual_cost")}</td>
<td>{t("billlines.fields.quantity")}</td>
<td>{t("billlines.fields.actual_price")}</td>
<td>{t("billlines.fields.actual_cost")}</td>
</tr>
</thead>
<tbody>
@@ -143,15 +127,6 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
</Form.Item>
</td>
<td>
<Form.Item
// label={t("joblines.fields.oem_partno")}
key={`${index}jobline.oem_partno`}
name={[field.name, "oem_partno"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td style={{ textAlign: "right" }}>
<Form.Item
// label={t("joblines.fields.quantity")}
key={`${index}quantity`}
@@ -160,7 +135,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td style={{ textAlign: "right" }}>
<td>
<Form.Item
// label={t("joblines.fields.actual_price")}
key={`${index}actual_price`}
@@ -169,7 +144,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, data, disabled }) {
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td style={{ textAlign: "right" }}>
<td>
<Form.Item
// label={t("joblines.fields.actual_cost")}
key={`${index}actual_cost`}

View File

@@ -7,8 +7,10 @@ export default function BillDetailEditcontainer() {
const search = queryString.parse(useLocation().search);
const history = useNavigate();
const screens = Grid.useBreakpoint();
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
@@ -17,14 +19,7 @@ export default function BillDetailEditcontainer() {
xl: "90%",
xxl: "90%"
};
let drawerPercentage = "100%";
if (screens.xxl) drawerPercentage = bpoints.xxl;
else if (screens.xl) drawerPercentage = bpoints.xl;
else if (screens.lg) drawerPercentage = bpoints.lg;
else if (screens.md) drawerPercentage = bpoints.md;
else if (screens.sm) drawerPercentage = bpoints.sm;
else if (screens.xs) drawerPercentage = bpoints.xs;
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
return (
<Drawer

View File

@@ -108,7 +108,7 @@ function BillEnterAiScan({
setIsAiScan(true);
const formdata = new FormData();
formdata.append("billScan", file);
formdata.append("jobid", billEnterModal.context.job?.id);
formdata.append("jobid", form.getFieldValue("jobid") || billEnterModal.context.job?.id);
formdata.append("bodyshopid", bodyshop.id);
formdata.append("partsorderid", billEnterModal.context.parts_order?.id);

View File

@@ -58,7 +58,6 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
const notification = useNotification();
const fileInputRef = useRef(null);
const pollingIntervalRef = useRef(null);
const formTopRef = useRef(null);
const {
treatments: { Enhanced_Payroll, Imgproxy, Bill_OCR_AI }
@@ -67,7 +66,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
names: ["Enhanced_Payroll", "Imgproxy", "Bill_OCR_AI"],
splitKey: bodyshop.imexshopid
});
const formValues = useMemo(() => {
return {
...billEnterModal.context.bill,
@@ -500,25 +499,17 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
autoComplete={"off"}
layout="vertical"
form={form}
onFinishFailed={(errorInfo) => {
onFinishFailed={() => {
setEnterAgain(false);
// Scroll to the top of the form to show validation errors
if (errorInfo.errorFields && errorInfo.errorFields.length > 0) {
setTimeout(() => {
formTopRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
}, 100);
}
}}
>
<div ref={formTopRef}>
<RbacWrapper action="bills:enter">
<BillFormContainer
form={form}
isAiScan={isAiScan}
disableInvNumber={billEnterModal.context.disableInvNumber}
/>
</RbacWrapper>
</div>
<RbacWrapper action="bills:enter">
<BillFormContainer
form={form}
isAiScan={isAiScan}
disableInvNumber={billEnterModal.context.disableInvNumber}
/>
</RbacWrapper>
</Form>
</Modal>
);

View File

@@ -1,5 +1,4 @@
import { Form, Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Form, Input, Table } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -109,14 +108,7 @@ export default function BillFormLinesExtended({ lineData, discount, form, respon
<Form.Item noStyle name="billlineskeys">
<button onClick={() => console.log(form.getFieldsValue())}>form</button>
<Input onChange={(e) => setSearch(e.target.value)} allowClear />
<ResponsiveTable
pagination={false}
size="small"
columns={columns}
mobileColumnKeys={["line_desc", "oem_partno", "part_type", "act_price"]}
rowKey="id"
dataSource={data}
/>
<Table pagination={false} size="small" columns={columns} rowKey="id" dataSource={data} />
</Form.Item>
);
}

View File

@@ -99,22 +99,20 @@ export function BillFormItemsExtendedFormItem({
}}
</Form.Item>
<Form.Item label={t("billlines.fields.cost_center")} name={["billlineskeys", record.id, "cost_center"]}>
<Select
showSearch
style={{ minWidth: "3rem" }}
disabled={disabled}
options={
bodyshopHasDmsKey(bodyshop)
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => ({ value: item.name, label: item.name }))
}
/>
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
{bodyshopHasDmsKey(bodyshop)
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item label={t("billlines.fields.location")} name={["billlineskeys", record.id, "location"]}>
<Select
disabled={disabled}
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
/>
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("billlines.fields.deductedfromlbr")}
@@ -138,10 +136,22 @@ export function BillFormItemsExtendedFormItem({
]}
name={["billlineskeys", record.id, "lbr_adjustment", "mod_lbr_ty"]}
>
<Select
allowClear
options={CiecaSelect(false, true)}
/>
<Select allowClear>
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t("jobs.labels.adjustmentrate")}

View File

@@ -349,12 +349,13 @@ export function BillFormComponent({
</Form.Item>
{!billEdit && (
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
<Select
style={{ width: "10rem" }}
disabled={disabled}
allowClear
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
/>
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
)}
</LayoutFormRow>

View File

@@ -434,17 +434,11 @@ export function BillEnterModalLinesComponent({
rules: [{ required: true }]
}),
formInput: () => (
<Select
showSearch
style={{ minWidth: "3rem" }}
disabled={disabled}
tabIndex={0}
options={
bodyshopHasDmsKey(bodyshop)
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => ({ value: item.name, label: item.name }))
}
/>
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled} tabIndex={0}>
{bodyshopHasDmsKey(bodyshop)
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
</Select>
)
},
...(billEdit
@@ -460,11 +454,13 @@ export function BillEnterModalLinesComponent({
name: [field.name, "location"]
}),
formInput: () => (
<Select
disabled={disabled}
tabIndex={0}
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
/>
<Select disabled={disabled} tabIndex={0}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
)
}
]),
@@ -505,10 +501,22 @@ export function BillEnterModalLinesComponent({
rules={[{ required: true }]}
name={[record.name, "lbr_adjustment", "mod_lbr_ty"]}
>
<Select
allowClear
options={CiecaSelect(false, true)}
/>
<Select allowClear>
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
</Select>
</Form.Item>
{Enhanced_Payroll.treatment === "on" ? (

View File

@@ -1,5 +1,5 @@
import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Input, Space } from "antd";
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { FaTasks } from "react-icons/fa";
@@ -18,7 +18,6 @@ import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
const mapStateToProps = createStructuredSelector({
@@ -238,13 +237,12 @@ export function BillsListTableComponent({
</Space>
}
>
<ResponsiveTable
<Table
loading={billsQuery.loading}
scroll={{
x: true // y: "50rem"
}}
columns={columns}
mobileColumnKeys={["vendorname", "invoice_number", "date", "total", "actions"]}
rowKey="id"
dataSource={hasBillsAccess ? filteredBills : []}
onChange={handleTableChange}

View File

@@ -3,8 +3,7 @@ import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { useQuery } from "@apollo/client/react";
import queryString from "query-string";
import { useLocation, useNavigate } from "react-router-dom";
import { Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Input, Table } from "antd";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
import AlertComponent from "../alert/alert.component";
@@ -80,7 +79,7 @@ export default function BillsVendorsList() {
: (data && data.vendors) || [];
return (
<ResponsiveTable
<Table
loading={loading}
title={() => {
return (
@@ -92,7 +91,6 @@ export default function BillsVendorsList() {
dataSource={dataSource}
pagination={{ placement: "top" }}
columns={columns}
mobileColumnKeys={["name", "cost_center", "city"]}
rowKey="id"
onChange={handleTableChange}
rowSelection={{

View File

@@ -19,12 +19,13 @@ export default function ChatTagRoComponent({ roOptions, loading, handleSearch, h
placeholder={t("general.labels.search")}
onSelect={handleInsertTag}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
options={roOptions.map((item, idx) => ({
key: item.id || idx,
value: item.id || idx,
label: ` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`
}))}
/>
>
{roOptions.map((item, idx) => (
<Select.Option key={item.id || idx}>
{` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`}
</Select.Option>
))}
</Select>
</div>
{loading ? <LoadingOutlined /> : null}

View File

@@ -1,5 +1,4 @@
import { Card, Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Card, Input, Table } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
@@ -104,11 +103,10 @@ export default function ContractsCarsComponent({ loading, data, selectedCarId, h
/>
}
>
<ResponsiveTable
<Table
loading={loading}
pagination={{ placement: "top" }}
columns={columns}
mobileColumnKeys={["status", "fleetnumber", "readiness", "year"]}
rowKey="id"
dataSource={filteredData}
onChange={handleTableChange}

View File

@@ -309,13 +309,13 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
}
]}
>
<Select
options={bodyshop.md_ins_cos.map((s) => ({
key: s.name,
value: s.name,
label: s.name
}))}
/>
<Select>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name={"class"}
@@ -327,13 +327,13 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
}
]}
>
<Select
options={bodyshop.md_classes.map((s) => ({
key: s,
value: s,
label: s
}))}
/>
<Select>
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("contracts.labels.convertform.applycleanupcharge")}

View File

@@ -1,5 +1,4 @@
import { Card, Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Card, Input, Table } from "antd";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
@@ -128,11 +127,10 @@ export default function ContractsJobsComponent({ loading, data, selectedJob, han
/>
}
>
<ResponsiveTable
<Table
loading={loading}
pagination={{ placement: "top", defaultPageSize: pageLimit, defaultCurrent: defaultCurrent }}
columns={columns}
mobileColumnKeys={["ro_number", "owner", "status", "vehicle", "plate_no"]}
rowKey="id"
dataSource={filteredData}
onChange={handleTableChange}

View File

@@ -2,6 +2,8 @@ import { useEffect, useState } from "react";
import { Select } from "antd";
import { useTranslation } from "react-i18next";
const { Option } = Select;
const ContractStatusComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
@@ -13,17 +15,11 @@ const ContractStatusComponent = ({ value, onChange, ref }) => {
}, [value, option, onChange]);
return (
<Select
ref={ref}
value={option}
style={{ width: 100 }}
onChange={setOption}
options={[
{ value: "contracts.status.new", label: t("contracts.status.new") },
{ value: "contracts.status.out", label: t("contracts.status.out") },
{ value: "contracts.status.returned", label: t("contracts.status.out") }
]}
/>
<Select ref={ref} value={option} style={{ width: 100 }} onChange={setOption}>
<Option value="contracts.status.new">{t("contracts.status.new")}</Option>
<Option value="contracts.status.out">{t("contracts.status.out")}</Option>
<Option value="contracts.status.returned">{t("contracts.status.out")}</Option>
</Select>
);
};

View File

@@ -1,6 +1,5 @@
import { useLazyQuery } from "@apollo/client/react";
import { Button, Form, Modal } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Button, Form, Modal, Table } from "antd";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -65,7 +64,7 @@ export function ContractsFindModalContainer({ contractFinderModal, toggleModalVi
{t("general.labels.search")}
</Button>
{error && <AlertComponent type="error" title={JSON.stringify(error)} />}
<ResponsiveTable
<Table
loading={loading}
columns={[
{
@@ -125,7 +124,6 @@ export function ContractsFindModalContainer({ contractFinderModal, toggleModalVi
render: (text, record) => <DateTimeFormatter>{record.actualreturn}</DateTimeFormatter>
}
]}
mobileColumnKeys={["agreementnumber", "job.ro_number", "driver_ln", "status"]}
rowKey="id"
dataSource={data?.cccontracts}
/>

View File

@@ -1,5 +1,5 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Typography } from "antd";
import { Button, Card, Input, Space, Table, Typography } from "antd";
import queryString from "query-string";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -14,7 +14,6 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { pageLimit } from "../../utils/config";
import ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -171,14 +170,13 @@ export function ContractsList({ bodyshop, loading, contracts, refetch, total, se
}
>
<ContractsFindModalContainer />
<ResponsiveTable
<Table
loading={loading}
scroll={{
x: "50%" //y: "40rem"
}}
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(page || 1, 10), total: total }}
columns={columns}
mobileColumnKeys={["agreementnumber", "driver_ln", "status", "scheduledreturn"]}
rowKey="id"
dataSource={contracts}
onChange={handleTableChange}

View File

@@ -1,5 +1,4 @@
import { Card } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Card, Table } from "antd";
import queryString from "query-string";
import { useTranslation } from "react-i18next";
import { Link, useLocation, useNavigate } from "react-router-dom";
@@ -74,11 +73,10 @@ export default function CourtesyCarContractListComponent({ contracts, totalContr
return (
<Card title={t("menus.header.courtesycars-contracts")}>
<ResponsiveTable
<Table
scroll={{ x: true }}
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(page || 1), total: totalContracts }}
columns={columns}
mobileColumnKeys={["agreementnumber", "driver_ln", "status", "job.ro_number"]}
rowKey="id"
dataSource={contracts}
onChange={handleTableChange}

View File

@@ -2,6 +2,8 @@ import { Select } from "antd";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
const { Option } = Select;
const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
@@ -21,11 +23,10 @@ const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => {
width: 100
}}
onChange={setOption}
options={[
{ value: "courtesycars.readiness.ready", label: t("courtesycars.readiness.ready") },
{ value: "courtesycars.readiness.notready", label: t("courtesycars.readiness.notready") }
]}
/>
>
<Option value="courtesycars.readiness.ready">{t("courtesycars.readiness.ready")}</Option>
<Option value="courtesycars.readiness.notready">{t("courtesycars.readiness.notready")}</Option>
</Select>
);
};
export default CourtesyCarReadinessComponent;

View File

@@ -2,6 +2,8 @@ import { useEffect, useState } from "react";
import { Select } from "antd";
import { useTranslation } from "react-i18next";
const { Option } = Select;
const CourtesyCarStatusComponent = ({ value, onChange, ref }) => {
const [option, setOption] = useState(value);
const { t } = useTranslation();
@@ -20,15 +22,14 @@ const CourtesyCarStatusComponent = ({ value, onChange, ref }) => {
width: 100
}}
onChange={setOption}
options={[
{ value: "courtesycars.status.in", label: t("courtesycars.status.in") },
{ value: "courtesycars.status.inservice", label: t("courtesycars.status.inservice") },
{ value: "courtesycars.status.out", label: t("courtesycars.status.out") },
{ value: "courtesycars.status.sold", label: t("courtesycars.status.sold") },
{ value: "courtesycars.status.leasereturn", label: t("courtesycars.status.leasereturn") },
{ value: "courtesycars.status.unavailable", label: t("courtesycars.status.unavailable") }
]}
/>
>
<Option value="courtesycars.status.in">{t("courtesycars.status.in")}</Option>
<Option value="courtesycars.status.inservice">{t("courtesycars.status.inservice")}</Option>
<Option value="courtesycars.status.out">{t("courtesycars.status.out")}</Option>
<Option value="courtesycars.status.sold">{t("courtesycars.status.sold")}</Option>
<Option value="courtesycars.status.leasereturn">{t("courtesycars.status.leasereturn")}</Option>
<Option value="courtesycars.status.unavailable">{t("courtesycars.status.unavailable")}</Option>
</Select>
);
};
export default CourtesyCarStatusComponent;

View File

@@ -1,6 +1,5 @@
import { SyncOutlined, WarningFilled } from "@ant-design/icons";
import { Button, Card, Dropdown, Input, Space, Tooltip } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Button, Card, Dropdown, Input, Space, Table, Tooltip } from "antd";
import dayjs from "../../utils/day";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -276,11 +275,10 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
</Space>
}
>
<ResponsiveTable
<Table
loading={loading}
pagination={{ placement: "top" }}
columns={columns}
mobileColumnKeys={["status", "fleetnumber", "vin", "readiness"]}
rowKey="id"
dataSource={tableData}
onChange={handleTableChange}

View File

@@ -1,6 +1,5 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Button, Card, Table } from "antd";
import queryString from "query-string";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -87,11 +86,10 @@ export default function CsiResponseListPaginated({ refetch, loading, responses,
return (
<Card extra={<Button onClick={() => refetch()} icon={<SyncOutlined />} />}>
<ResponsiveTable
<Table
loading={loading}
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(state.page || 1), total: total }}
columns={columns}
mobileColumnKeys={["ro_number", "owner_name", "completedon"]}
rowKey="id"
dataSource={responses}
onChange={handleTableChange}

View File

@@ -1,5 +1,4 @@
import { Card, Tag } from "antd";
import ResponsiveTable from "../../responsive-table/responsive-table.component";
import { Card, Table, Tag } from "antd";
import axios from "axios";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -183,11 +182,10 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
</div>
</Card>
<Card style={{ marginTop: "5px" }} type="inner" title={t("job_lifecycle.titles.top_durations")}>
<ResponsiveTable
<Table
size="small"
pagination={false}
columns={columns}
mobileColumnKeys={["status", "humanReadable", "averageHumanReadable", "statusCount"]}
rowKey={(record) => record.status}
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}
/>

View File

@@ -1,4 +1,4 @@
import { Card, Input, Space, Typography } from "antd";
import { Card, Input, Space, Table, Typography } from "antd";
import axios from "axios";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -7,7 +7,6 @@ import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import Dinero from "dinero.js";
import DashboardRefreshRequired from "../refresh-required.component";
import { pageLimit } from "../../../utils/config";
import ResponsiveTable from "../../responsive-table/responsive-table.component.jsx";
export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
const { t } = useTranslation();
@@ -104,33 +103,31 @@ export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
{...cardProps}
>
<LoadingSkeleton loading={loading}>
<div style={{ height: "100%", minHeight: 0, width: "100%", overflow: "auto" }}>
<ResponsiveTable
size="small"
tableLayout="fixed"
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}
pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns}
scroll={{ x: "max-content" }}
scroll={{ x: true, y: "calc(100% - 4em)" }}
rowKey="id"
style={{ width: "100%" }}
style={{ height: "100%" }}
dataSource={filteredData}
summary={() => (
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>
<Table.Summary.Row>
<Table.Summary.Cell>
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
<Table.Summary.Cell>
{Dinero(costingData?.allSummaryData && costingData.allSummaryData.totalSales).toFormat()}
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
<Table.Summary.Cell>
{Dinero(costingData?.allSummaryData && costingData.allSummaryData.totalCost).toFormat()}
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
<Table.Summary.Cell>
{Dinero(costingData?.allSummaryData && costingData.allSummaryData.gpdollars).toFormat()}
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell></ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
</Table.Summary.Cell>
<Table.Summary.Cell></Table.Summary.Cell>
</Table.Summary.Row>
)}
/>
</div>

View File

@@ -363,7 +363,6 @@ export default function DashboardScheduledDeliveryToday({ data, ...cardProps })
onChange={handleTableChange}
pagination={false}
columns={isTvModeScheduledDelivery ? tvColumns : columns}
// mobileColumnKeys={["ro_number", "owner", "status", "vehicle"]}
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}

View File

@@ -368,7 +368,6 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
onChange={handleTableChange}
pagination={false}
columns={isTvModeScheduledIn ? tvColumns : columns}
mobileColumnKeys={["ro_number", "owner", "vehicle", "start"]}
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}

View File

@@ -363,7 +363,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
onChange={handleTableChange}
pagination={false}
columns={isTvModeScheduledOut ? tvColumns : columns}
// mobileColumnKeys={["ro_number", "owner", "status", "vehicle"]}
scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id"
style={{ height: "85%" }}

View File

@@ -1,6 +1,5 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Form, Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Button, Card, Form, Input, Table } from "antd";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -79,7 +78,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
dataIndex: "Lines",
key: "Lines",
render: (text, record) => (
<ResponsiveTable style={{ tableLayout: "auto", width: "100%" }}>
<table style={{ tableLayout: "auto", width: "100%" }}>
<tr>
<th>{t("bills.fields.invoice_number")}</th>
<th>{t("bodyshop.fields.dms.dms_acctnumber")}</th>
@@ -92,7 +91,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
<td>{l.Amount}</td>
</tr>
))}
</ResponsiveTable>
</table>
)
}
];
@@ -116,10 +115,9 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
/>
}
>
<ResponsiveTable
<Table
pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns}
mobileColumnKeys={["status", "reference", "Lines"]}
rowKey={(record) => `${record.InvoiceNumber}${record.Account}`}
dataSource={allocationsSummary}
locale={{ emptyText: t("dms.labels.refreshallocations") }}

View File

@@ -1,5 +1,4 @@
import { Alert, Button, Card, Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Alert, Button, Card, Table, Typography } from "antd";
import { SyncOutlined } from "@ant-design/icons";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -117,10 +116,9 @@ export function DmsAllocationsSummary({ mode, socket, bodyshop, jobId, title, on
<Alert type="warning" title={t("jobs.labels.dms.disablebillwip")} />
)}
<ResponsiveTable
<Table
pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns}
mobileColumnKeys={["center", "sale", "cost", "sale_dms_acctnumber"]}
rowKey="center"
dataSource={allocationsSummary}
locale={{ emptyText: t("dms.labels.refreshallocations") }}
@@ -137,17 +135,15 @@ export function DmsAllocationsSummary({ mode, socket, bodyshop, jobId, title, on
const hasNonZeroSaleTotal = totals.totalSale.getAmount() !== 0;
return (
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>
<Table.Summary.Row>
<Table.Summary.Cell>
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell>
{hasNonZeroSaleTotal ? totals.totalSale.toFormat() : null}
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell />
<ResponsiveTable.Summary.Cell />
<ResponsiveTable.Summary.Cell />
</ResponsiveTable.Summary.Row>
</Table.Summary.Cell>
<Table.Summary.Cell>{hasNonZeroSaleTotal ? totals.totalSale.toFormat() : null}</Table.Summary.Cell>
<Table.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell />
</Table.Summary.Row>
);
}}
/>

View File

@@ -1,5 +1,4 @@
import { Alert, Button, Card, Tabs, Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Alert, Button, Card, Table, Tabs, Typography } from "antd";
import { SyncOutlined } from "@ant-design/icons";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -262,10 +261,9 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
into taxable / non-taxable segments.
</Typography.Paragraph>
<ResponsiveTable
<Table
pagination={false}
columns={roggColumns}
mobileColumnKeys={["jobNo", "opCode", "breakOut", "itemType"]}
rowKey="key"
dataSource={roggRows}
locale={{ emptyText: "No ROGOG lines would be generated." }}
@@ -288,23 +286,19 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
const hasCostTotal = Number(roggTotals.totalDlrCost) !== 0;
return (
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell index={0}>
<Table.Summary.Row>
<Table.Summary.Cell index={0}>
<Typography.Title level={5}>{t("general.labels.totals")}</Typography.Title>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell index={1} />
<ResponsiveTable.Summary.Cell index={2} />
<ResponsiveTable.Summary.Cell index={3} />
<ResponsiveTable.Summary.Cell index={4} />
<ResponsiveTable.Summary.Cell index={5} />
<ResponsiveTable.Summary.Cell index={6} />
<ResponsiveTable.Summary.Cell index={7}>
{hasCustTotal ? roggTotals.totalCustPrice : null}
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell index={8}>
{hasCostTotal ? roggTotals.totalDlrCost : null}
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
</Table.Summary.Cell>
<Table.Summary.Cell index={1} />
<Table.Summary.Cell index={2} />
<Table.Summary.Cell index={3} />
<Table.Summary.Cell index={4} />
<Table.Summary.Cell index={5} />
<Table.Summary.Cell index={6} />
<Table.Summary.Cell index={7}>{hasCustTotal ? roggTotals.totalCustPrice : null}</Table.Summary.Cell>
<Table.Summary.Cell index={8}>{hasCostTotal ? roggTotals.totalDlrCost : null}</Table.Summary.Cell>
</Table.Summary.Row>
);
}}
/>
@@ -319,10 +313,9 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
This mirrors the shell that would be sent for ROLABOR when all financials are carried in GOG.
</Typography.Paragraph>
<ResponsiveTable
<Table
pagination={false}
columns={rolaborColumns}
mobileColumnKeys={["jobNo", "opCode", "breakOut", "itemType"]}
rowKey="key"
dataSource={rolaborRows}
locale={{ emptyText: "No ROLABOR lines would be generated." }}

View File

@@ -1,6 +1,5 @@
import { useLazyQuery } from "@apollo/client/react";
import { Button, Input, Modal } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Button, Input, Modal, Table } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -61,7 +60,7 @@ export function DmsCdkVehicles({ form, job }) {
okButtonProps={{ disabled: !selectedModel }}
>
{error && <AlertComponent title={error.message} type="error" />}
<ResponsiveTable
<Table
title={() => (
<Input.Search
onSearch={(val) => callSearch({ variables: { search: val } })}
@@ -70,7 +69,6 @@ export function DmsCdkVehicles({ form, job }) {
/>
)}
columns={columns}
mobileColumnKeys={["make", "model", "makecode", "modelcode"]}
loading={loading}
rowKey="id"
dataSource={data ? data.search_dms_vehicles : []}

View File

@@ -1,5 +1,4 @@
import { Button, Checkbox, Col } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Button, Checkbox, Col, Table } from "antd";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
@@ -73,7 +72,7 @@ export default function CDKCustomerSelector({ bodyshop, socket }) {
return (
<Col span={24}>
<ResponsiveTable
<Table
title={() => (
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<Button onClick={onUseSelected} disabled={!selectedCustomer}>
@@ -87,7 +86,6 @@ export default function CDKCustomerSelector({ bodyshop, socket }) {
)}
pagination={{ placement: "top" }}
columns={columns}
mobileColumnKeys={["id", "vinOwner", "name1", "address"]}
rowKey={rowKey}
dataSource={customerList}
rowSelection={{

View File

@@ -1,5 +1,4 @@
import { Button, Checkbox, Col } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Button, Checkbox, Col, Table } from "antd";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
@@ -79,7 +78,7 @@ export default function FortellisCustomerSelector({ bodyshop, jobid, socket }) {
return (
<Col span={24}>
<ResponsiveTable
<Table
title={() => (
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<Button onClick={onUseSelected} disabled={!selectedCustomer}>
@@ -93,7 +92,6 @@ export default function FortellisCustomerSelector({ bodyshop, jobid, socket }) {
)}
pagination={{ placement: "top" }}
columns={columns}
mobileColumnKeys={["id", "vinOwner", "firstName", "address"]}
rowKey={(r) => r.customerId}
dataSource={customerList}
rowSelection={{

View File

@@ -1,5 +1,4 @@
import { Button, Col } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Button, Col, Table } from "antd";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
@@ -67,7 +66,7 @@ export default function PBSCustomerSelector({ bodyshop, socket }) {
return (
<Col span={24}>
<ResponsiveTable
<Table
title={() => (
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<Button onClick={onUseSelected} disabled={!selectedCustomer}>
@@ -81,7 +80,6 @@ export default function PBSCustomerSelector({ bodyshop, socket }) {
)}
pagination={{ placement: "top" }}
columns={columns}
mobileColumnKeys={["ContactId", "name1", "address"]}
rowKey={(r) => r.ContactId}
dataSource={customerList}
rowSelection={{

View File

@@ -1,5 +1,4 @@
import { Alert, Button, Checkbox, message, Modal, Space } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Alert, Button, Checkbox, message, Modal, Space, Table } from "antd";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
@@ -73,14 +72,14 @@ export default function RRCustomerSelector({
if (!socket) return;
const handleRrSelectCustomer = (list) => {
const normalized = normalizeRrList(list);
// If list is empty, it means early RO exists and customer selection should be skipped
// Don't open the modal in this case
if (normalized.length === 0) {
setRefreshing(false);
return;
}
setOpen(true);
setCustomerList(normalized);
const firstOwner = normalized.find((r) => r.vinOwner)?.custNo;
@@ -196,8 +195,8 @@ export default function RRCustomerSelector({
description={
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div>
We created the Repair Order. Please validate the totals and taxes in the DMS system. When done, click{" "}
<strong>Finished</strong> to finalize and mark this export as complete.
We created the Repair Order. Please validate the totals and taxes in the DMS system. When done,
click <strong>Finished</strong> to finalize and mark this export as complete.
</div>
<div>
<Space>
@@ -216,8 +215,14 @@ export default function RRCustomerSelector({
}
return (
<Modal open={open} onCancel={handleClose} footer={null} width={800} title={t("dms.selectCustomer")}>
<ResponsiveTable
<Modal
open={open}
onCancel={handleClose}
footer={null}
width={800}
title={t("dms.selectCustomer")}
>
<Table
title={() => (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{/* Open RO limit banner */}
@@ -299,7 +304,6 @@ export default function RRCustomerSelector({
)}
pagination={{ placement: "top" }}
columns={columns}
mobileColumnKeys={["custNo", "vinOwner", "name", "address"]}
rowKey={(r) => r.custNo}
dataSource={customerList}
rowSelection={{

View File

@@ -272,19 +272,11 @@ export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode,
name={[field.name, "name"]}
rules={[{ required: true }]}
>
<Select
showSearch={{
optionFilterProp: "label",
filterOption: (input, option) => option.label.toLowerCase().includes(input.toLowerCase())
}}
style={{ width: "100%" }}
onSelect={(value) => handlePayerSelect(value, index)}
options={bodyshop.cdk_configuration?.payers?.map((payer) => ({
key: payer.name,
value: payer.name,
label: payer.name
}))}
/>
<Select style={{ width: "100%" }} onSelect={(value) => handlePayerSelect(value, index)}>
{bodyshop.cdk_configuration?.payers?.map((payer) => (
<Select.Option key={payer.name}>{payer.name}</Select.Option>
))}
</Select>
</Form.Item>
</Col>

View File

@@ -1,6 +1,5 @@
import { ReloadOutlined } from "@ant-design/icons";
import { Alert, Button, Form, Input, InputNumber, Modal, Radio, Select, Space, Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Alert, Button, Form, Input, InputNumber, Modal, Radio, Select, Space, Table, Typography } from "antd";
import { useEffect, useMemo, useState } from "react";
// Simple customer selector table
@@ -27,14 +26,7 @@ function CustomerSelectorTable({ customers, onSelect, isSubmitting }) {
return (
<div>
<ResponsiveTable
columns={columns}
mobileColumnKeys={["name", "select", "custNo", "vinOwner"]}
dataSource={customers}
rowKey="custNo"
pagination={false}
size="small"
/>
<Table columns={columns} dataSource={customers} rowKey="custNo" pagination={false} size="small" />
<div style={{ marginTop: 16, display: "flex", gap: 8 }}>
<Button
type="primary"

View File

@@ -86,13 +86,11 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
}
]}
>
<Select
options={[
{ key: currentUser.email, value: currentUser.email, label: currentUser.email },
{ key: bodyshop.email, value: bodyshop.email, label: bodyshop.email },
...(bodyshop.md_from_emails ? bodyshop.md_from_emails.map((e) => ({ key: e, value: e, label: e })) : [])
]}
/>
<Select>
<Select.Option key={currentUser.email}>{currentUser.email}</Select.Option>
<Select.Option key={bodyshop.email}>{bodyshop.email}</Select.Option>
{bodyshop.md_from_emails && bodyshop.md_from_emails.map((e) => <Select.Option key={e}>{e}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item
label={

View File

@@ -163,7 +163,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
<Modal
destroyOnHidden
open={modalVisible}
mask={{ closable: false }}
maskClosable={false}
width={"80%"}
onOk={() => form.submit()}
title={t("emails.labels.emailpreview")}

View File

@@ -1,6 +1,7 @@
import { Select, Space, Tag } from "antd";
import { useTranslation } from "react-i18next";
const { Option } = Select;
//To be used as a form element only.
const EmployeeSearchSelectEmail = ({ options, ...props }) => {
@@ -11,24 +12,26 @@ const EmployeeSearchSelectEmail = ({ options, ...props }) => {
showSearch={{
optionFilterProp: "search"
}}
// value={option}
style={{
width: 400
}}
options={options?.map((o) => ({
key: o.id,
value: o.user_email,
search: `${o.employee_number} ${o.first_name} ${o.last_name}`,
label: (
<Space>
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
<Tag color="green">
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
</Tag>
</Space>
)
}))}
{...props}
/>
>
{options
? options.map((o) => (
<Option key={o.id} value={o.user_email} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
<Space>
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
<Tag color="green">
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
</Tag>
</Space>
</Option>
))
: null}
</Select>
);
};
export default EmployeeSearchSelectEmail;

View File

@@ -1,6 +1,7 @@
import { Select, Space, Tag } from "antd";
import { useTranslation } from "react-i18next";
const { Option } = Select;
//To be used as a form element only.
const EmployeeSearchSelect = ({ options, showEmail, ...props }) => {
@@ -11,29 +12,30 @@ const EmployeeSearchSelect = ({ options, showEmail, ...props }) => {
showSearch={{
optionFilterProp: "search"
}}
// value={option}
style={{
width: 400
}}
options={options?.map((o) => ({
key: o.id,
value: o.id,
search: `${o.employee_number} ${o.first_name} ${o.last_name}`,
label: (
<Space size="small">
{`${o.employee_number ?? ""} ${o.first_name} ${o.last_name}`}
<Tag color="green" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
</Tag>
{showEmail && o.user_email ? (
<Tag color="blue" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
{o.user_email}
</Tag>
) : null}
</Space>
)
}))}
{...props}
/>
>
{options
? options.map((o) => (
<Option key={o.id} value={o.id} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
<Space size="small">
{`${o.employee_number ?? ""} ${o.first_name} ${o.last_name}`}
<Tag color="green" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
</Tag>
{showEmail && o.user_email ? (
<Tag color="blue" style={{ padding: "0.1 0.1rem", marginRight: "1px", marginLeft: "1px" }}>
{o.user_email}
</Tag>
) : null}
</Space>
</Option>
))
: null}
</Select>
);
};
export default EmployeeSearchSelect;

View File

@@ -39,13 +39,11 @@ export default function FormsFieldChanged({ form, skipPrompt }) {
{errors.length > 0 && (
<AlertComponent
type="error"
message={t("general.labels.validationerror")}
description={
title={
<div>
<ul>{errors.map((e, idx) => e.errors.map((e2, idx2) => <li key={`${idx}${idx2}`}>{e2}</li>))}</ul>
</div>
}
showIcon
/>
)}
</Space>

View File

@@ -184,29 +184,22 @@ export default function GlobalSearchOs() {
return (
<AutoComplete
options={data}
defaultActiveFirstOption
onSearch={handleSearch}
onKeyDown={(e) => {
if (e.key !== "Enter") return;
const firstUrlForSearch = data?.[0]?.options?.[0]?.label?.props?.to;
if (!firstUrlForSearch) return;
navigate(firstUrlForSearch);
}}
defaultActiveFirstOption
onClear={() => setData([])}
>
<Input.Search
// className="global-search-autocomplete-fix"
size="large"
placeholder={t("general.labels.globalsearch")}
enterButton
allowClear
loading={loading}
onChange={(e) => {
const value = e.target.value;
if (!value) {
setData([]);
} else {
handleSearch(value);
}
}}
/>
</AutoComplete>
);

View File

@@ -160,6 +160,9 @@ export default function GlobalSearch() {
return (
<AutoComplete
options={options}
showSearch={{
onSearch: handleSearch
}}
defaultActiveFirstOption
onKeyDown={(e) => {
if (e.key !== "Enter") return;
@@ -169,13 +172,11 @@ export default function GlobalSearch() {
}}
>
<Input.Search
// className="global-search-autocomplete-fix"
size="large"
placeholder={t("general.labels.globalsearch")}
enterButton
allowClear
loading={loading}
onChange={(e) => handleSearch(e.target.value)}
/>
</AutoComplete>
);

View File

@@ -1,5 +1,5 @@
import { EditFilled, FileAddFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space, Typography } from "antd";
import { Button, Card, Input, Space, Table, Typography } from "antd";
import queryString from "query-string";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -10,7 +10,6 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component";
import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component";
import { pageLimit } from "../../utils/config";
import ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
@@ -186,11 +185,10 @@ export function JobsList({ refetch, loading, jobs, total, setInventoryUpsertCont
</Space>
}
>
<ResponsiveTable
<Table
loading={loading}
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(page || 1), total: total }}
columns={columns}
mobileColumnKeys={["line_desc", "actual_price", "consumedbyjob", "actions"]}
rowKey="id"
dataSource={jobs}
onChange={handleTableChange}

View File

@@ -67,19 +67,16 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
);
};
const handleInsSelect = (value) => {
const selectedVendor = bodyshop.md_ins_cos.find(s => s.name === value);
if (selectedVendor) {
form.setFieldsValue({
addr1: selectedVendor.name,
addr2: selectedVendor.street1,
addr3: selectedVendor.street2,
city: selectedVendor.city,
state: selectedVendor.state,
zip: selectedVendor.zip,
vendorid: null
});
}
const handleInsSelect = (value, option) => {
form.setFieldsValue({
addr1: option.obj.name,
addr2: option.obj.street1,
addr3: option.obj.street2,
city: option.obj.city,
state: option.obj.state,
zip: option.obj.zip,
vendorid: null
});
};
const handleVendorSelect = (vendorid) => {
@@ -106,13 +103,13 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job, technician }) {
<VendorSearchSelect options={VendorAutoCompleteData?.vendors} onSelect={handleVendorSelect} />
</Form.Item>
<Form.Item label={t("bodyshop.fields.md_ins_co.name")} name="ins_co_id">
<Select
onSelect={handleInsSelect}
options={bodyshop.md_ins_cos.map((s) => ({
value: s.name,
label: s.name
}))}
/>
<Select onSelect={handleInsSelect}>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} obj={s} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
<LayoutFormRow grow>
<Form.Item label={t("printcenter.jobs.3rdpartyfields.addr1")} name="addr1">

View File

@@ -1,7 +1,6 @@
import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client/react";
import { Button, Card, Col, Row, Tag } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Button, Card, Col, Row, Table, Tag } from "antd";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -164,24 +163,12 @@ export function JobAuditTrail({ bodyshop, jobId }) {
/>
}
>
<ResponsiveTable
loading={loading}
columns={columns}
mobileColumnKeys={["status", "created", "useremail", "operation"]}
rowKey="id"
dataSource={data ? data.audit_trail : []}
/>
<Table loading={loading} columns={columns} rowKey="id" dataSource={data ? data.audit_trail : []} />
</Card>
</Col>
<Col span={24}>
<Card title={t("jobs.labels.emailaudit")}>
<ResponsiveTable
loading={loading}
columns={emailColumns}
mobileColumnKeys={["status", "created", "useremail", "operation"]}
rowKey="id"
dataSource={data ? data.email_audit_trail : []}
/>
<Table loading={loading} columns={emailColumns} rowKey="id" dataSource={data ? data.email_audit_trail : []} />
</Card>
</Col>
</Row>

View File

@@ -1,6 +1,5 @@
import { useEffect } from "react";
import { Alert, Card } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Alert, Card, Table } from "antd";
import { t } from "i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -69,15 +68,7 @@ export function JobCloseRGuardPpd({ job, warningCallback }) {
return (
<Card title={t("jobs.labels.ppdnotexported")}>
<ResponsiveTable
dataSource={linesWithPPD}
columns={columns}
mobileColumnKeys={["line_desc", "ppd", "act_price", "act_price_before_ppc"]}
pagination={false}
rowKey="id"
bordered
size="small"
/>
<Table dataSource={linesWithPPD} columns={columns} pagination={false} rowKey="id" bordered size="small" />
{linesWithPPD.length > 0 && (
<Alert style={{ margin: "8px 0px" }} type="warning" title={t("jobs.labels.outstanding_ppd")} />
)}

View File

@@ -1,7 +1,6 @@
import { useEffect } from "react";
import { Alert, Card } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Alert, Card, Table } from "antd";
import { t } from "i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -63,15 +62,7 @@ export function JobCloseRGuardSublet({ job, warningCallback }) {
return (
<Card title={t("jobs.labels.subletsnotcompleted")}>
<ResponsiveTable
dataSource={subletsNotDone}
columns={columns}
mobileColumnKeys={["line_desc", "act_price", "part_qty", "notes"]}
pagination={false}
rowKey="id"
bordered
size="small"
/>
<Table dataSource={subletsNotDone} columns={columns} pagination={false} rowKey="id" bordered size="small" />
{subletsNotDone.length > 0 && (
<Alert style={{ margin: "8px 0px" }} type="warning" title={t("jobs.labels.outstanding_sublets")} />
)}

View File

@@ -1,5 +1,4 @@
import { Input, Space, Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Input, Space, Table, Typography } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
@@ -66,7 +65,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
return (
<div>
<ResponsiveTable
<Table
title={() => {
return (
<Space wrap>
@@ -88,19 +87,18 @@ export default function JobCostingPartsTable({ data, summaryData }) {
onChange={handleTableChange}
pagination={{ placement: "top", defaultPageSize: pageLimit }}
columns={columns}
mobileColumnKeys={["cost_center", "sales", "costs", "gpdollars", "gppercent"]}
rowKey="id"
dataSource={filteredData}
summary={() => (
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>
<Table.Summary.Row>
<Table.Summary.Cell>
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell>{Dinero(summaryData.totalSales).toFormat()}</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell>{Dinero(summaryData.totalCost).toFormat()}</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell>{Dinero(summaryData.gpdollars).toFormat()}</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell></ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
</Table.Summary.Cell>
<Table.Summary.Cell>{Dinero(summaryData.totalSales).toFormat()}</Table.Summary.Cell>
<Table.Summary.Cell>{Dinero(summaryData.totalCost).toFormat()}</Table.Summary.Cell>
<Table.Summary.Cell>{Dinero(summaryData.gpdollars).toFormat()}</Table.Summary.Cell>
<Table.Summary.Cell></Table.Summary.Cell>
</Table.Summary.Row>
)}
/>
</div>

View File

@@ -58,8 +58,10 @@ const span = {
export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTrail }) {
const { scenarioNotificationsOn } = useSocket();
const [updateJob] = useMutation(UPDATE_JOB);
const screens = Grid.useBreakpoint();
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
@@ -68,14 +70,7 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTra
xl: "75%",
xxl: "75%"
};
let drawerPercentage = "100%";
if (screens.xxl) drawerPercentage = bpoints.xxl;
else if (screens.xl) drawerPercentage = bpoints.xl;
else if (screens.lg) drawerPercentage = bpoints.lg;
else if (screens.md) drawerPercentage = bpoints.md;
else if (screens.sm) drawerPercentage = bpoints.sm;
else if (screens.xs) drawerPercentage = bpoints.xs;
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;

View File

@@ -1,4 +1,4 @@
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Table } from "antd";
import { useTranslation } from "react-i18next";
import JobLineNotePopup from "../job-line-note-popup/job-line-note-popup.component";
import PartsStatusPie from "../parts-status-pie/parts-status-pie.component";
@@ -101,12 +101,7 @@ function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
<div>
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
<PartsStatusPie joblines_status={joblines_status} />
<ResponsiveTable
rowKey="id"
columns={columns}
mobileColumnKeys={["status", "line_desc", "part_type", "part_qty"]}
dataSource={filteredJobLines || []}
/>
<Table rowKey="id" columns={columns} dataSource={filteredJobLines || []} />
</CardTemplate>
</div>
);

View File

@@ -690,7 +690,6 @@ export function JobLinesComponent({
<Table
columns={columns}
// mobileColumnKeys={["status", "line_desc", "actions", "line_no"]}
rowKey="id"
loading={loading}
pagination={false}

View File

@@ -1,7 +1,6 @@
import { useQuery } from "@apollo/client/react";
import { gql } from "@apollo/client";
import { Badge, Card, Space, Tag } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Badge, Card, Space, Table, Tag } from "antd";
import axios from "axios";
import { isEmpty } from "lodash";
import { useCallback, useEffect, useState } from "react";
@@ -312,13 +311,12 @@ export function JobLifecycleComponent({ bodyshop, job, statuses }) {
</>
}
>
<ResponsiveTable
<Table
style={{
overflow: "auto",
width: "100%"
}}
columns={columns}
mobileColumnKeys={["value", "start", "start_readable", "end"]}
dataSource={lifecycleData.lifecycle}
rowKey="start"
/>

View File

@@ -88,15 +88,17 @@ export function JoblineBulkAssign({ setSelectedLines, selectedLines, insertAudit
>
<Select
showSearch={{
optionFilterProp: "label",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
options={bodyshop.employee_teams.map((team) => ({
value: team.id,
label: team.name
}))}
/>
>
{bodyshop.employee_teams.map((team) => (
<Select.Option value={team.id} key={team.id} name={team.name}>
{team.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap>

View File

@@ -122,26 +122,22 @@ export function JobLineConvertToLabor({
}
]}
>
<Select
allowClear
showSearch
options={[
{ value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
{ value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
{ value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
{ value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
{ value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
{ value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
{ value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
{ value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
{ value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
{ value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
{ value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
{ value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
{ value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
]}
/>
<Select allowClear showSearch={{ optionFilterProp: "children" }}>
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
</Select>
</Form.Item>
<Form.Item shouldUpdate>

View File

@@ -115,18 +115,19 @@ export function JobLineDispatchButton({
>
<Select
showSearch={{
optionFilterProp: "label",
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
options={bodyshop.employees
>
{bodyshop.employees
.filter((emp) => emp.active)
.map((emp) => ({
value: emp.id,
key: emp.id,
label: `${emp.first_name} ${emp.last_name}`
}))}
/>
.map((emp) => (
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
</Form.Item>
<Space wrap>

View File

@@ -64,12 +64,13 @@ export function JobLineStatusPopup({ bodyshop, jobline, disabled }) {
onSelect={handleChange}
onBlur={handleSave}
onClear={() => handleChange(null)}
options={Object.values(bodyshop.md_order_statuses).map((s, idx) => ({
key: idx,
value: s,
label: s
}))}
/>
>
{Object.values(bodyshop.md_order_statuses).map((s, idx) => (
<Select.Option key={idx} value={s}>
{s}
</Select.Option>
))}
</Select>
</LoadingSpinner>
</div>
);

View File

@@ -75,12 +75,13 @@ export function JoblineTeamAssignment({ bodyshop, jobline, disabled, jobId, inse
onSelect={handleChange}
onBlur={handleSave}
onClear={() => handleChange(null)}
options={Object.values(bodyshop.employee_teams).map((s) => ({
key: s.id,
value: s.id,
label: s.name
}))}
/>
>
{Object.values(bodyshop.employee_teams).map((s, idx) => (
<Select.Option key={idx} value={s.id}>
{s.name}
</Select.Option>
))}
</Select>
</LoadingSpinner>
</div>
);

View File

@@ -67,22 +67,22 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa
</LayoutFormRow>
<LayoutFormRow grow>
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
<Select allowClear options={[
{ value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
{ value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
{ value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
{ value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
{ value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
{ value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
{ value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
{ value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
{ value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
{ value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
{ value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
{ value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
{ value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
]} />
<Select allowClear>
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
</Select>
</Form.Item>
<Form.Item label={t("joblines.fields.op_code_desc")} name="op_code_desc">
<Input />
@@ -128,17 +128,17 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa
</LayoutFormRow>
<LayoutFormRow>
<Form.Item label={t("joblines.fields.part_type")} name="part_type">
<Select allowClear options={[
{ value: "PAA", label: t("joblines.fields.part_types.PAA") },
{ value: "PAC", label: t("joblines.fields.part_types.PAC") },
{ value: "PAE", label: t("joblines.fields.part_types.PAE") },
{ value: "PAL", label: t("joblines.fields.part_types.PAL") },
{ value: "PAM", label: t("joblines.fields.part_types.PAM") },
{ value: "PAN", label: t("joblines.fields.part_types.PAN") },
{ value: "PAO", label: t("joblines.fields.part_types.PAO") },
{ value: "PAR", label: t("joblines.fields.part_types.PAR") },
{ value: "PAS", label: t("joblines.fields.part_types.PAS") }
]} />
<Select allowClear>
<Select.Option value="PAA">{t("joblines.fields.part_types.PAA")}</Select.Option>
<Select.Option value="PAC">{t("joblines.fields.part_types.PAC")}</Select.Option>
<Select.Option value="PAE">{t("joblines.fields.part_types.PAE")}</Select.Option>
<Select.Option value="PAL">{t("joblines.fields.part_types.PAL")}</Select.Option>
<Select.Option value="PAM">{t("joblines.fields.part_types.PAM")}</Select.Option>
<Select.Option value="PAN">{t("joblines.fields.part_types.PAN")}</Select.Option>
<Select.Option value="PAO">{t("joblines.fields.part_types.PAO")}</Select.Option>
<Select.Option value="PAR">{t("joblines.fields.part_types.PAR")}</Select.Option>
<Select.Option value="PAS">{t("joblines.fields.part_types.PAS")}</Select.Option>
</Select>
</Form.Item>
<Form.Item label={t("joblines.fields.oem_partno")} name="oem_partno">
<Input />

View File

@@ -1,6 +1,5 @@
import { EditFilled } from "@ant-design/icons";
import { Button, Card, Space } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Button, Card, Space, Table } from "antd";
import Dinero from "dinero.js";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -187,9 +186,8 @@ export function JobPayments({ job, bodyshop, setPaymentContext, setCardPaymentCo
</Space>
}
>
<ResponsiveTable
<Table
columns={columns}
mobileColumnKeys={["date", "amount", "actions", "payer"]}
rowKey="id"
pagination={false}
onChange={handleTableChange}
@@ -201,18 +199,18 @@ export function JobPayments({ job, bodyshop, setPaymentContext, setCardPaymentCo
expandedRowRender: (record) => <PaymentExpandedRowComponent record={record} bodyshop={bodyshop} />
}}
summary={() => (
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>
<Table.Summary.Row>
<Table.Summary.Cell>
<strong>{t("payments.labels.totalpayments")}</strong>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell />
<ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
<Table.Summary.Cell />
<Table.Summary.Cell>
<strong>{total.toFormat()}</strong>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell />
<ResponsiveTable.Summary.Cell />
<ResponsiveTable.Summary.Cell />
</ResponsiveTable.Summary.Row>
</Table.Summary.Cell>
<Table.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell />
</Table.Summary.Row>
)}
/>
</Card>

View File

@@ -1,5 +1,4 @@
import { Checkbox, Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Checkbox, Table, Typography } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -80,12 +79,11 @@ export default function JobReconciliationBillsTable({ billLineState, invoiceLine
return (
<div>
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
<ResponsiveTable
<Table
pagination={false}
size="small"
scroll={{ y: "60vh" }}
columns={columns}
mobileColumnKeys={["line_desc", "from", "actual_price", "actual_cost"]}
rowKey="id"
dataSource={invoiceLineData}
onChange={handleTableChange}

View File

@@ -1,5 +1,4 @@
import { Typography } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Table, Typography } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -97,10 +96,9 @@ export default function JobReconcilitionPartsTable({ jobLineState, jobLineData }
return (
<div>
<Typography.Title level={4}>{t("jobs.labels.lines")}</Typography.Title>
<ResponsiveTable
<Table
pagination={false}
columns={columns}
mobileColumnKeys={["status", "line_desc", "total", "oem_partno"]}
size="small"
scroll={{ y: "60vh" }}
rowKey="id"

View File

@@ -8,6 +8,8 @@ import { SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_FOR_AUTOCOMPLETE } from
import AlertComponent from "../alert/alert.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
const { Option } = Select;
const JobSearchSelect = ({
disabled,
convertedOnly = false,
@@ -85,24 +87,24 @@ const JobSearchSelect = ({
style={{ width: "100%" }}
suffixIcon={(loading || idLoading) && <Spin />} // matches OLD spinner semantics
notFoundContent={loading ? <LoadingOutlined /> : null} // matches OLD (loading only)
options={theOptions?.map((o) => ({
key: o.id,
value: o.id,
status: o.status,
label: (
<Space align="center">
<span>
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${o.ro_number || t("general.labels.na")} | ${OwnerNameDisplayFunction(
o
)} | ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""}`}
</span>
<Tag>
<strong>{o.status}</strong>
</Tag>
</Space>
)
}))}
/>
>
{theOptions
? theOptions.map((o) => (
<Option key={o.id} value={o.id} status={o.status}>
<Space align="center">
<span>
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${o.ro_number || t("general.labels.na")} | ${OwnerNameDisplayFunction(
o
)} | ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${o.v_model_desc || ""}`}
</span>
<Tag>
<strong>{o.status}</strong>
</Tag>
</Space>
</Option>
))
: null}
</Select>
{error ? <AlertComponent title={error.message} type="error" /> : null}
{idError ? <AlertComponent title={idError.message} type="error" /> : null}

View File

@@ -1,5 +1,4 @@
import { Space } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Space, Table } from "antd";
import Dinero from "dinero.js";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -105,9 +104,8 @@ export default function JobTotalsTableLabor({ job }) {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<ResponsiveTable
<Table
columns={columns}
mobileColumnKeys={["total", "profitcenter_labor", "rate", "mod_lb_hrs"]}
rowKey="id"
pagination={false}
onChange={handleTableChange}
@@ -117,29 +115,29 @@ export default function JobTotalsTableLabor({ job }) {
}}
summary={() => (
<>
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>
<Table.Summary.Row>
<Table.Summary.Cell>
<strong>{t("jobs.labels.labor_rates_subtotal")}</strong>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell />
<ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
<Table.Summary.Cell />
<Table.Summary.Cell>
{(job.job_totals.rates.mapa.hours + job.job_totals.rates.mash.hours).toFixed(1)}
</ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
{InstanceRenderManager({
imex: null,
rome: (
<>
<ResponsiveTable.Summary.Cell />
<ResponsiveTable.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell />
</>
)
})}
<ResponsiveTable.Summary.Cell align="right">
<Table.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}</strong>
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>
<Space>
{t("jobs.labels.mapa")}
{InstanceRenderManager({
@@ -158,34 +156,34 @@ export default function JobTotalsTableLabor({ job }) {
})
})}
</Space>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
<CurrencyFormatter>{job.job_totals.rates.mapa.rate}</CurrencyFormatter>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell>{job.job_totals.rates.mapa.hours.toFixed(1)}</ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
<Table.Summary.Cell>{job.job_totals.rates.mapa.hours.toFixed(1)}</Table.Summary.Cell>
{InstanceRenderManager({
imex: (
<ResponsiveTable.Summary.Cell align="right">
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
</ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
),
rome: (
<>
<ResponsiveTable.Summary.Cell align="right">
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.base).toFormat()}
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.adjustment).toFormat()}
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
</ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
</>
)
})}
</ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>
<Space wrap>
{t("jobs.labels.mash")}
{InstanceRenderManager({
@@ -204,51 +202,51 @@ export default function JobTotalsTableLabor({ job }) {
})
})}
</Space>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
<CurrencyFormatter>{job.job_totals.rates.mash.rate}</CurrencyFormatter>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell>{job.job_totals.rates.mash.hours.toFixed(1)}</ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
<Table.Summary.Cell>{job.job_totals.rates.mash.hours.toFixed(1)}</Table.Summary.Cell>
{InstanceRenderManager({
imex: (
<ResponsiveTable.Summary.Cell align="right">
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.total).toFormat()}
</ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
),
rome: (
<>
<ResponsiveTable.Summary.Cell align="right">
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.base).toFormat()}
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.adjustment).toFormat()}
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">
</Table.Summary.Cell>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.rates.mash.total).toFormat()}
</ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
</>
)
})}
</ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>
<strong>{t("jobs.labels.rates_subtotal")}</strong>
</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell />
<ResponsiveTable.Summary.Cell />
</Table.Summary.Cell>
<Table.Summary.Cell />
<Table.Summary.Cell />
{InstanceRenderManager({
imex: null,
rome: (
<>
<ResponsiveTable.Summary.Cell />
<ResponsiveTable.Summary.Cell />
<Table.Summary.Cell />
<Table.Summary.Cell />
</>
)
})}
<ResponsiveTable.Summary.Cell align="right">
<Table.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.rates.subtotal).toFormat()}</strong>
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
</Table.Summary.Cell>
</Table.Summary.Row>
</>
)}
/>

View File

@@ -1,4 +1,4 @@
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Table } from "antd";
import Dinero from "dinero.js";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -65,9 +65,8 @@ export default function JobTotalsTableOther({ job }) {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<ResponsiveTable
<Table
columns={columns}
mobileColumnKeys={["total", "key"]}
rowKey="key"
pagination={false}
onChange={handleTableChange}
@@ -77,24 +76,24 @@ export default function JobTotalsTableOther({ job }) {
}}
summary={() => (
<>
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>
<Table.Summary.Row>
<Table.Summary.Cell>
<strong>{t("jobs.labels.additionaltotal")}</strong>
</ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">
<Table.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.additional.total).toFormat()}</strong>
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>
<strong>{t("jobs.labels.subletstotal")}</strong>
</ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">
<Table.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.parts.sublets.total).toFormat()}</strong>
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
</Table.Summary.Cell>
</Table.Summary.Row>
</>
)}
/>

View File

@@ -1,4 +1,4 @@
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Table } from "antd";
import Dinero from "dinero.js";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -64,9 +64,8 @@ export default function JobTotalsTableParts({ job }) {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
<ResponsiveTable
<Table
columns={columns}
mobileColumnKeys={["total", "id"]}
rowKey="id"
pagination={false}
onChange={handleTableChange}
@@ -76,38 +75,36 @@ export default function JobTotalsTableParts({ job }) {
}}
summary={() => (
<>
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>{t("jobs.labels.prt_dsmk_total")}</ResponsiveTable.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">
<Table.Summary.Row>
<Table.Summary.Cell>{t("jobs.labels.prt_dsmk_total")}</Table.Summary.Cell>
<Table.Summary.Cell align="right">
{Dinero(job.job_totals.parts.parts.prt_dsmk_total).toFormat()}
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
</Table.Summary.Cell>
</Table.Summary.Row>
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell>
<Table.Summary.Row>
<Table.Summary.Cell>
<strong>{t("jobs.labels.partstotal")}</strong>
</ResponsiveTable.Summary.Cell>
</Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">
<Table.Summary.Cell align="right">
<strong>{Dinero(job.job_totals.parts.parts.total).toFormat()}</strong>
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
</Table.Summary.Cell>
</Table.Summary.Row>
{
//TODO:AIO This shoudl only be in the US version. need to verify whether this causes problems for the CA version.
insuranceAdjustments.length > 0 && (
<ResponsiveTable.Summary.Row>
<ResponsiveTable.Summary.Cell colSpan={24}>
{t("jobs.labels.profileadjustments")}
</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell colSpan={24}>{t("jobs.labels.profileadjustments")}</Table.Summary.Cell>
</Table.Summary.Row>
)
}
{insuranceAdjustments.map((adj, idx) => (
<ResponsiveTable.Summary.Row key={idx}>
<ResponsiveTable.Summary.Cell>{t(`jobs.fields.${adj.id.toLowerCase()}`)}</ResponsiveTable.Summary.Cell>
<Table.Summary.Row key={idx}>
<Table.Summary.Cell>{t(`jobs.fields.${adj.id.toLowerCase()}`)}</Table.Summary.Cell>
<ResponsiveTable.Summary.Cell align="right">{adj.amount.toFormat()}</ResponsiveTable.Summary.Cell>
</ResponsiveTable.Summary.Row>
<Table.Summary.Cell align="right">{adj.amount.toFormat()}</Table.Summary.Cell>
</Table.Summary.Row>
))}
</>
)}

View File

@@ -1,4 +1,4 @@
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Table } from "antd";
import Dinero from "dinero.js";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
@@ -245,9 +245,8 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
];
return (
<ResponsiveTable
<Table
columns={columns}
mobileColumnKeys={["total", "key"]}
rowKey="key"
showHeader={false}
pagination={false}

View File

@@ -59,12 +59,13 @@ export function JobsAdminClass({ bodyshop, job }) {
}
]}
>
<Select
options={bodyshop.md_classes.map((s) => ({
value: s,
label: s
}))}
/>
<Select>
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
</Form>

View File

@@ -1,5 +1,5 @@
import { DownloadOutlined, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Input, Space } from "antd";
import { Button, Card, Input, Space, Table } from "antd";
import axios from "axios";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -9,7 +9,6 @@ import { useNotification } from "../../contexts/Notifications/notificationContex
import { selectPartnerVersion } from "../../redux/application/application.selectors";
import { alphaSort } from "../../utils/sorters";
import { logImEXEvent } from "../../firebase/firebase.utils.js";
import ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -142,11 +141,10 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
</Space>
}
>
<ResponsiveTable
<Table
loading={loading}
pagination={{ placement: "top" }}
columns={columns}
mobileColumnKeys={["cieca_id", "owner", "vehicle", "actions"]}
rowKey="id"
dataSource={data}
onChange={handleTableChange}

View File

@@ -1,6 +1,6 @@
import { DeleteFilled, DownloadOutlined, PlusCircleFilled, SyncOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client/react";
import { Alert, Button, Card, Input, Space } from "antd";
import { Alert, Button, Card, Input, Space, Table } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -13,7 +13,6 @@ import { TimeAgoFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils.js";
import ResponsiveTable from "../responsive-table/responsive-table.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -210,14 +209,7 @@ export function JobsAvailableComponent({ bodyshop, loading, data, refetch, addJo
</Space>
}
>
<ResponsiveTable
loading={loading}
columns={columns}
mobileColumnKeys={["cieca_id", "job_id", "ownr_name", "vehicle_info", "actions"]}
rowKey="id"
dataSource={availableJobs}
onChange={handleTableChange}
/>
<Table loading={loading} columns={columns} rowKey="id" dataSource={availableJobs} onChange={handleTableChange} />
</Card>
);
}

View File

@@ -141,11 +141,13 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
disabled={jobRO}
options={bodyshop.md_responsibility_centers.profits.map((p) => ({
value: p.name,
label: p.name
}))}
/>
>
{bodyshop.md_responsibility_centers.profits.map((p) => (
<Select.Option key={p.name} value={p.name}>
{p.name}
</Select.Option>
))}
</Select>
</Form.Item>
</td>
<td>
@@ -169,11 +171,13 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
disabled={jobRO}
options={bodyshop.md_responsibility_centers.profits.map((p) => ({
value: p.name,
label: p.name
}))}
/>
>
{bodyshop.md_responsibility_centers.profits.map((p) => (
<Select.Option key={p.name} value={p.name}>
{p.name}
</Select.Option>
))}
</Select>
</Form.Item>
</td>
</tr>

View File

@@ -2,7 +2,7 @@ import { useMutation } from "@apollo/client/react";
import { Button, Divider, Form, Input, Modal, Select, Space, Switch } from "antd";
import axios from "axios";
import { some } from "lodash";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -19,10 +19,10 @@ import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import RREarlyROForm from "../dms-post-form/rr-early-ro-form";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
@@ -37,17 +37,16 @@ const mapDispatchToProps = (dispatch) => ({
export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTrail, parentFormIsFieldsTouched }) {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [earlyRoCreated, setEarlyRoCreated] = useState(!!job?.dms_id);
const [earlyRoCreatedThisSession, setEarlyRoCreatedThisSession] = useState(false);
const [earlyRoCreated, setEarlyRoCreated] = useState(!!job?.dms_id); // Track early RO creation state
const [earlyRoCreatedThisSession, setEarlyRoCreatedThisSession] = useState(false); // Track if created in THIS modal session
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
const { t } = useTranslation();
const [form] = Form.useForm();
const notification = useNotification();
const allFormValues = Form.useWatch([], form);
const { socket } = useSocket();
const { socket } = useSocket(); // Extract socket from context
// Get Fortellis treatment for proper DMS mode detection
const {
treatments: { Fortellis }
} = useTreatmentsWithConfig({
@@ -56,64 +55,16 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
splitKey: bodyshop?.imexshopid
});
// Check if bodyshop has Reynolds integration using the proper getDmsMode function
const dmsMode = getDmsMode(bodyshop, Fortellis.treatment);
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
const insuranceOptions = useMemo(
() =>
(bodyshop?.md_ins_cos ?? []).map((s) => ({
value: s.name,
label: s.name
})),
[bodyshop?.md_ins_cos]
);
const classOptions = useMemo(
() =>
(bodyshop?.md_classes ?? []).map((s) => ({
value: s,
label: s
})),
[bodyshop?.md_classes]
);
const referralOptions = useMemo(
() =>
(bodyshop?.md_referral_sources ?? []).map((s) => ({
value: s,
label: s
})),
[bodyshop?.md_referral_sources]
);
const csrOptions = useMemo(
() =>
(bodyshop?.employees ?? [])
.filter((emp) => emp.active)
.map((emp) => ({
value: emp.id,
label: `${emp.first_name} ${emp.last_name}`
})),
[bodyshop?.employees]
);
const categoryOptions = useMemo(
() =>
(bodyshop?.md_categories ?? []).map((s) => ({
value: s,
label: s
})),
[bodyshop?.md_categories]
);
const handleConvert = async ({ employee_csr, category, ...values }) => {
if (parentFormIsFieldsTouched()) {
alert(t("jobs.labels.savebeforeconversion"));
return;
}
setLoading(true);
const res = await mutationConvertJob({
variables: {
jobId: job.id,
@@ -127,11 +78,13 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
});
if (values.ca_gst_registrant) {
await axios.post("/job/totalsssu", { id: job.id });
await axios.post("/job/totalsssu", {
id: job.id
});
}
if (!res.errors) {
refetch?.();
refetch();
notification.success({
title: t("jobs.successes.converted")
});
@@ -144,20 +97,19 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
setOpen(false);
}
setLoading(false);
};
const submitDisabled = useCallback(() => some(allFormValues, (v) => v === undefined), [allFormValues]);
const handleEarlyROSuccess = (result) => {
setEarlyRoCreated(true);
setEarlyRoCreatedThisSession(true);
setEarlyRoCreated(true); // Mark early RO as created
setEarlyRoCreatedThisSession(true); // Mark as created in this session
notification.success({
title: t("jobs.successes.early_ro_created"),
description: `RO Number: ${result.roNumber || "N/A"}`
});
// Delay refetch to keep success message visible for 2 seconds
setTimeout(() => {
refetch?.();
}, 2000);
@@ -178,28 +130,29 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
disabled={job.converted || jobRO}
loading={loading}
onClick={() => {
setEarlyRoCreated(!!job?.dms_id);
setEarlyRoCreatedThisSession(false);
setEarlyRoCreated(!!job?.dms_id); // Initialize state based on current job
setEarlyRoCreatedThisSession(false); // Reset session state when opening modal
setOpen(true);
}}
>
{t("jobs.actions.convert")}
</Button>
{/* Convert Job Modal */}
<Modal
open={open}
onCancel={handleModalClose}
closable={!(earlyRoCreatedThisSession && !job.converted)}
mask={{ closable: !(earlyRoCreatedThisSession && !job.converted) }}
closable={!(earlyRoCreatedThisSession && !job.converted)} // Only restrict if created in THIS session
maskClosable={!(earlyRoCreatedThisSession && !job.converted)} // Only restrict if created in THIS session
title={t("jobs.actions.convert")}
footer={null}
width={700}
destroyOnHidden
>
{/* Standard Convert Form */}
<Form
layout="vertical"
form={form}
preserve={false}
onFinish={handleConvert}
initialValues={{
driveable: true,
@@ -211,6 +164,7 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
referral_source_extra: job.referral_source_extra ?? ""
}}
>
{/* Show Reynolds Early RO section at the top if applicable */}
{isReynoldsMode && !job.dms_id && !earlyRoCreated && (
<>
<RREarlyROForm
@@ -227,78 +181,127 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
<Form.Item
name={["ins_co_nm"]}
label={t("jobs.fields.ins_co_nm")}
rules={[{ required: true }]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Select
showSearch={{
optionFilterProp:'label'
}}
options={insuranceOptions}
/>
<Select showSearch>
{bodyshop.md_ins_cos.map((s, i) => (
<Select.Option key={i} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
{bodyshop.enforce_class && (
<Form.Item name="class" label={t("jobs.fields.class")} rules={[{ required: bodyshop.enforce_class }]}>
<Select options={classOptions} />
<Form.Item
name={"class"}
label={t("jobs.fields.class")}
rules={[
{
required: bodyshop.enforce_class
//message: t("general.validation.required"),
}
]}
>
<Select>
{bodyshop.md_classes.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
)}
{bodyshop.enforce_referral && (
<>
<Form.Item
name="referral_source"
name={"referral_source"}
label={t("jobs.fields.referralsource")}
rules={[{ required: bodyshop.enforce_referral }]}
rules={[
{
required: bodyshop.enforce_referral
//message: t("general.validation.required"),
}
]}
>
<Select options={referralOptions} />
<Select>
{bodyshop.md_referral_sources.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
<Input />
</Form.Item>
</>
)}
{bodyshop.enforce_conversion_csr && (
<Form.Item
name="employee_csr"
name={"employee_csr"}
label={t(
InstanceRenderManager({
imex: "jobs.fields.employee_csr",
rome: "jobs.fields.employee_csr_writer"
})
)}
rules={[{ required: bodyshop.enforce_conversion_csr }]}
rules={[
{
required: bodyshop.enforce_conversion_csr
//message: t("general.validation.required"),
}
]}
>
<Select
showSearch={{
optionFilterProp: 'label',
filterOption: (input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
optionFilterProp: "children",
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}}
style={{ width: 200 }}
options={csrOptions}
/>
>
{bodyshop.employees
.filter((emp) => emp.active)
.map((emp) => (
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
{`${emp.first_name} ${emp.last_name}`}
</Select.Option>
))}
</Select>
</Form.Item>
)}
{bodyshop.enforce_conversion_category && (
<Form.Item name="category" label={t("jobs.fields.category")} rules={[{ required: bodyshop.enforce_conversion_category }]}>
<Select allowClear options={categoryOptions} />
<Form.Item
name={"category"}
label={t("jobs.fields.category")}
rules={[
{
required: bodyshop.enforce_conversion_category
//message: t("general.validation.required"),
}
]}
>
<Select allowClear>
{bodyshop.md_categories.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
)}
{bodyshop.region_config.toLowerCase().startsWith("ca") && (
<Form.Item label={t("jobs.fields.ca_gst_registrant")} name="ca_gst_registrant" valuePropName="checked">
<Switch />
</Form.Item>
)}
<Form.Item label={t("jobs.fields.driveable")} name="driveable" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label={t("jobs.fields.towin")} name="towin" valuePropName="checked">
<Switch />
</Form.Item>
@@ -313,7 +316,6 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
>
{t("jobs.actions.convert")}
</Button>
<Button onClick={handleModalClose} disabled={earlyRoCreatedThisSession && !job.converted}>
{t("general.actions.close")}
</Button>

View File

@@ -60,13 +60,13 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
<Select
onChange={handleInsCoChange}
options={bodyshop.md_ins_cos.map((s) => ({
value: s.name,
label: s.name
}))}
/>
<Select onChange={handleInsCoChange}>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
<Input />
@@ -192,12 +192,13 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.referralsource")} name="referral_source">
<Select
options={bodyshop.md_referral_sources.map((s) => ({
value: s,
label: s
}))}
/>
<Select>
{bodyshop.md_referral_sources.map((s) => (
<Select.Option key={s} value={s}>
{s}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
<Input />
@@ -220,13 +221,10 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<CurrencyInput min={0} />
</Form.Item>
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
<Select
allowClear
options={[
{ value: "W", label: t("jobs.labels.deductible.waived") },
{ value: "Y", label: t("jobs.labels.deductible.stands") }
]}
/>
<Select allowClear>
<Select.Option value="W">{t("jobs.labels.deductible.waived")}</Select.Option>
<Select.Option value="Y">{t("jobs.labels.deductible.stands")}</Select.Option>
</Select>
</Form.Item>
<Form.Item label={t("jobs.fields.depreciation_taxes")} name="depreciation_taxes">
<CurrencyInput />

View File

@@ -1,5 +1,4 @@
import { Card, Input } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Card, Input, Table } from "antd";
import { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
@@ -96,12 +95,11 @@ export default function JobsCreateOwnerInfoSearchComponent({ loading, owners })
/>
}
>
<ResponsiveTable
<Table
loading={loading}
scroll={{ x: true }}
pagination={{ placement: "top" }}
columns={columns}
mobileColumnKeys={["ownr_ln", "ownr_ph1", "ownr_ph2", "ownr_fn"]}
rowKey="id"
dataSource={owners}
onChange={handleTableChange}

View File

@@ -1,6 +1,5 @@
import { PlusOutlined, SearchOutlined } from "@ant-design/icons";
import { Button, Input, Popover } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component";
import { Button, Input, Popover, Table } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import PredefinedVehicles from "./predefined-vehicles.js";
@@ -23,9 +22,9 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
const popContent = () => (
<div>
<ResponsiveTable
<Table
size="small"
title={() => <Input.Search onSearch={(value) => setSearch(value)} enterButton />}
title={() => <Input.Search onSearch={(value) => setSearch(value)} enterButton/>}
dataSource={filteredPredefinedVehicles}
columns={[
{
@@ -62,7 +61,6 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
)
}
]}
mobileColumnKeys={["make", "model", "select"]}
/>
</div>
);

Some files were not shown because too many files have changed in this diff Show More