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; if (!open || (hideIfNull && !children)) return null;
return ( return (
<div {...props} style={{ display: "flex" }}> <div {...props} style={{ display: "flex", alignItems: "flex-start" }}>
<div <div
style={{ 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> <Typography.Text type="secondary">{`${label}:`}</Typography.Text>
</div> </div>
<div <div
style={{ style={{
flex: 4, flex: 1, // <-- key: take remaining space
minWidth: 0, // <-- key: allow this flex item to shrink
marginLeft: ".3rem", marginLeft: ".3rem",
fontWeight: "bolder", fontWeight: "bolder",
wordWrap: "break-word", overflowWrap: "anywhere", // <-- key: break long tokens (email/vin)
cursor: onValueClick !== undefined ? "pointer" : "" wordBreak: "break-word", // (backup behavior across browsers)
cursor: onValueClick !== undefined ? "pointer" : "",
...(styles?.value ?? {}) // apply your per-field overrides to ALL children types
}} }}
className={valueClassName} className={valueClassName}
onClick={onValueClick} onClick={onValueClick}
> >
{typeof children === "string" ? ( {typeof children === "string" ? <Typography.Text>{children}</Typography.Text> : children}
<Typography.Text style={styles?.value}>{children}</Typography.Text>
) : (
children
)}
</div> </div>
</div> </div>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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