Merged in feature/IO-3499-React-19 (pull request #2849)

Feature/IO-3499 React 19
This commit is contained in:
Dave Richer
2026-01-19 19:32:12 +00:00
7 changed files with 147 additions and 112 deletions

View File

@@ -13,31 +13,32 @@ export default function DataLabel({
if (!open || (hideIfNull && !children)) return null;
return (
<div {...props} style={{ display: "flex" }}>
<div {...props} style={{ display: "flex", alignItems: "flex-start" }}>
<div
style={{
// flex: 2,
marginRight: ".2rem"
marginRight: ".2rem",
flexShrink: 0, // <-- key: don't let the label collapse
whiteSpace: "nowrap" // <-- key: keep "Email:" on one line
}}
>
<Typography.Text type="secondary">{`${label}:`}</Typography.Text>
</div>
<div
style={{
flex: 4,
flex: 1, // <-- key: take remaining space
minWidth: 0, // <-- key: allow this flex item to shrink
marginLeft: ".3rem",
fontWeight: "bolder",
wordWrap: "break-word",
cursor: onValueClick !== undefined ? "pointer" : ""
overflowWrap: "anywhere", // <-- key: break long tokens (email/vin)
wordBreak: "break-word", // (backup behavior across browsers)
cursor: onValueClick !== undefined ? "pointer" : "",
...(styles?.value ?? {}) // apply your per-field overrides to ALL children types
}}
className={valueClassName}
onClick={onValueClick}
>
{typeof children === "string" ? (
<Typography.Text style={styles?.value}>{children}</Typography.Text>
) : (
children
)}
{typeof children === "string" ? <Typography.Text>{children}</Typography.Text> : children}
</div>
</div>
);

View File

@@ -32,6 +32,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
const [loading, setLoading] = useState(false);
const [sending, setSending] = useState(false);
const [rawHtml, setRawHtml] = useState("");
const [htmlSize, setHtmlSize] = useState(0);
const [pdfCopytoAttach, setPdfCopytoAttach] = useState({
filename: null,
pdf: null
@@ -151,6 +152,13 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
if (modalVisible) render();
}, [modalVisible]);
useEffect(() => {
const html = form.getFieldValue("html");
if (html) {
setHtmlSize(new Blob([html]).size);
}
}, [form, rawHtml]);
return (
<Modal
destroyOnHidden
@@ -169,7 +177,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
disabled:
selectedMedia &&
(selectedMedia.filter((s) => s.isSelected).reduce((acc, val) => (acc = acc + val.size), 0) >=
10485760 - new Blob([form.getFieldValue("html")]).size ||
10485760 - htmlSize ||
selectedMedia.filter((s) => s.isSelected).length > 10)
}}
>
@@ -195,7 +203,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
disabled={
selectedMedia &&
(selectedMedia.filter((s) => s.isSelected).reduce((acc, val) => (acc = acc + val.size), 0) >=
10485760 - new Blob([form.getFieldValue("html")]).size ||
10485760 - htmlSize ||
selectedMedia.filter((s) => s.isSelected).length > 10)
}
type="primary"

View File

@@ -120,6 +120,13 @@ export function ScheduleEventComponent({
);
const handleConvert = async (values) => {
if (!event.job?.id) {
notification.error({
title: t("appointments.errors.nojob")
});
return;
}
const res = await mutationUpdateJob({
variables: {
jobId: event.job.id,
@@ -397,21 +404,21 @@ export function ScheduleEventComponent({
(HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
<Link
to={{
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
pathname: `/manage/jobs/${event.job.id}/intake`,
search: `?appointmentId=${event.id}`
}}
>
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
</Link>
) : (
<Popover //open={open}
<Popover
content={popMenu}
open={popOverVisible}
onOpenChange={setPopOverVisible}
onClick={(e) => {
if (event.job?.id) {
e.stopPropagation();
getJobDetails({ id: event.job.id });
getJobDetails({ variables: { id: event.job.id } });
}
}}
getPopupContainer={(trigger) => trigger.parentNode}
@@ -434,37 +441,36 @@ export function ScheduleEventComponent({
return baseColor;
};
const RegularEvent = event.isintake ? (
<Space
wrap
size="small"
style={{
backgroundColor: getEventBackground()
}}
>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
<OwnerNameDisplay ownerObject={event.job} />
{`${(event.job && event.job.v_model_yr) || ""} ${
(event.job && event.job.v_make_desc) || ""
} ${(event.job && event.job.v_model_desc) || ""}`}
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
})`}
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
{event?.job?.comment && `C: ${event.job.comment}`}
</Space>
) : (
<div
style={{
height: "100%",
width: "100%",
backgroundColor: getEventBackground()
}}
>
<strong>{`${event.title || ""}`}</strong>
</div>
);
const RegularEvent =
event.isintake && event.job ? (
<Space
wrap
size="small"
style={{
backgroundColor: getEventBackground()
}}
>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
<OwnerNameDisplay ownerObject={event.job} />
{`${event.job.v_model_yr || ""} ${event.job.v_make_desc || ""} ${event.job.v_model_desc || ""}`}
{`(${event.job.labhrs?.aggregate?.sum?.mod_lb_hrs || "0"} / ${
event.job.larhrs?.aggregate?.sum?.mod_lb_hrs || "0"
})`}
{event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
{event.job.comment && `C: ${event.job.comment}`}
</Space>
) : (
<div
style={{
height: "100%",
width: "100%",
backgroundColor: getEventBackground()
}}
>
<strong>{`${event.title || ""}`}</strong>
</div>
);
return (
<Popover

View File

@@ -86,10 +86,10 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail, isPar
const statusMenu = {
items: [
...availableStatuses.map((item) => ({
...(availableStatuses?.map((item) => ({
key: item,
label: item
}))
})) ?? [])
],
onClick: (e) => updateJobStatus(e.key)
};

View File

@@ -65,8 +65,8 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
const colSpan = {
xs: { span: 24 },
sm: { span: 24 },
md: { span: isPartsEntry ? 8 : 12 },
lg: { span: isPartsEntry ? 8 : 6 },
md: { span: 12 },
lg: { span: 12 },
xl: { span: isPartsEntry ? 8 : 6 }
};
@@ -260,19 +260,19 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
<ChatOpenButton type={job.ownr_ph1_ty} phone={job.ownr_ph1} jobid={job.id} />
)}
</DataLabel>
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
<DataLabel key="3" label={t("jobs.fields.ownr_ph2")}>
{disabled || isPartsEntry ? (
<PhoneNumberFormatter type={job.ownr_ph2_ty}>{job.ownr_ph2}</PhoneNumberFormatter>
) : (
<ChatOpenButton type={job.ownr_ph2_ty} phone={job.ownr_ph2} jobid={job.id} />
)}
</DataLabel>
<DataLabel key="3" label={t("owners.fields.address")}>
<DataLabel key="4" label={t("owners.fields.address")}>
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
job.ownr_city || ""
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
</DataLabel>
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
<DataLabel key="5" label={t("owners.fields.ownr_ea")}>
{disabled || isPartsEntry ? (
<>{job.ownr_ea || ""}</>
) : job.ownr_ea ? (
@@ -280,13 +280,14 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
) : null}
</DataLabel>
{job.owner?.tax_number && (
<DataLabel key="5" label={t("owners.fields.tax_number")}>
<DataLabel key="6" label={t("owners.fields.tax_number")}>
{job.owner?.tax_number || ""}
</DataLabel>
)}
<DataLabel
label={t("owners.fields.note")}
styles={{ value: { overflow: "hidden", textOverflow: "ellipsis" } }}
key="7"
>
{job.owner?.note || ""}
</DataLabel>

View File

@@ -18,6 +18,7 @@ export default function ProductionListColumnComment({ record }) {
const handleSaveNote = (e) => {
e.stopPropagation();
e.preventDefault();
setOpen(false);
updateAlert({
variables: {
@@ -33,7 +34,6 @@ export default function ProductionListColumnComment({ record }) {
};
const handleChange = (e) => {
e.stopPropagation();
setNote(e.target.value);
};
@@ -42,26 +42,38 @@ export default function ProductionListColumnComment({ record }) {
if (flag) setNote(record.comment || "");
};
const content = (
<div
style={{ width: "30em" }}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<Input.TextArea
rows={5}
value={note}
onChange={handleChange}
autoFocus
allowClear
style={{ marginBottom: "1em" }}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
/>
<div>
<Button onClick={handleSaveNote} type="primary">
{t("general.actions.save")}
</Button>
</div>
</div>
);
return (
<Popover
onOpenChange={handleOpenChange}
open={open}
content={
<div style={{ width: "30em" }}>
<Input.TextArea
rows={5}
value={note}
onChange={handleChange}
// onPressEnter={handleSaveNote}
autoFocus
allowClear
/>
<div>
<Button onClick={handleSaveNote}>{t("general.actions.save")}</Button>
</div>
</div>
}
trigger={["click"]}
<Popover
onOpenChange={handleOpenChange}
open={open}
content={content}
trigger="click"
fresh
getPopupContainer={(trigger) => trigger.parentElement}
>
<div
style={{
@@ -69,9 +81,9 @@ export default function ProductionListColumnComment({ record }) {
height: "19px",
cursor: "pointer",
overflow: "hidden",
textOverflow: "ellipsis",
display: "inline-block"
textOverflow: "ellipsis"
}}
onClick={(e) => e.stopPropagation()}
>
<Icon component={FaRegStickyNote} style={{ marginRight: ".2rem" }} />
<Tooltip title={record.comment}>{record.comment || " "}</Tooltip>

View File

@@ -1,7 +1,7 @@
import Icon from "@ant-design/icons";
import { useMutation } from "@apollo/client/react";
import { Button, Input, Popover, Space } from "antd";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { FaRegStickyNote } from "react-icons/fa";
import { logImEXEvent } from "../../firebase/firebase.utils";
@@ -27,6 +27,7 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
(e) => {
logImEXEvent("production_add_note");
e.stopPropagation();
e.preventDefault();
setOpen(false);
updateAlert({
variables: {
@@ -46,7 +47,6 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
);
const handleChange = useCallback((e) => {
e.stopPropagation();
setNote(e.target.value);
}, []);
@@ -58,42 +58,48 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
[record]
);
const popoverContent = useMemo(
() => (
<div style={{ width: "30em" }}>
<Input.TextArea
rows={5}
value={note}
onChange={handleChange}
autoFocus
allowClear
style={{ marginBottom: "1em" }}
/>
<Space>
<Button onClick={handleSaveNote} type="primary">
{t("general.actions.save")}
</Button>
<Button
onClick={() => {
setOpen(false);
setNoteUpsertContext({
context: {
jobId: record.id,
text: note
}
});
}}
>
{t("notes.actions.savetojobnotes")}
</Button>
</Space>
</div>
),
[note, handleSaveNote, handleChange, record, setNoteUpsertContext, t]
const content = (
<div style={{ width: "30em" }} onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
<Input.TextArea
rows={5}
value={note}
onChange={handleChange}
autoFocus
allowClear
style={{ marginBottom: "1em" }}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
/>
<Space>
<Button onClick={handleSaveNote} type="primary">
{t("general.actions.save")}
</Button>
<Button
onClick={() => {
setOpen(false);
setNoteUpsertContext({
context: {
jobId: record.id,
text: note
}
});
}}
>
{t("notes.actions.savetojobnotes")}
</Button>
</Space>
</div>
);
return (
<Popover onOpenChange={handleOpenChange} open={open} content={popoverContent} trigger={["click"]}>
<Popover
onOpenChange={handleOpenChange}
open={open}
content={content}
trigger="click"
fresh
getPopupContainer={(trigger) => trigger.parentElement}
>
<div
style={{
width: "100%",
@@ -102,6 +108,7 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
overflow: "hidden",
textOverflow: "ellipsis"
}}
onClick={(e) => e.stopPropagation()}
>
<Icon component={FaRegStickyNote} style={{ marginRight: ".2rem" }} />
{record.production_vars?.note || " "}