IO-3292 Add note pinning functionality.

This commit is contained in:
Patrick Fic
2025-07-22 09:03:41 -07:00
parent 953e70efef
commit 1934ae0758
15 changed files with 1757 additions and 403 deletions

View File

@@ -0,0 +1,52 @@
import { PushpinFilled, PushpinOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries";
import { UPDATE_NOTE } from "../../graphql/notes.queries";
function JobNotesPinToggle({ note }) {
const [updateNote] = useMutation(UPDATE_NOTE, {
update(cache, { data: { updateNote: updatedNote } }) {
try {
const existingJob = cache.readQuery({
query: GET_JOB_BY_PK,
variables: { id: note.jobid }
});
if (existingJob) {
cache.writeQuery({
query: GET_JOB_BY_PK,
variables: { id: note.jobid },
data: {
...existingJob,
job: {
...existingJob.job,
notes: updatedNote.pinned
? [updatedNote, ...existingJob.job.notes]
: existingJob.job.notes.filter((n) => n.id !== updatedNote.id)
}
}
});
}
} catch (error) {
// Query not yet executed is most likely. No logging as this isn't a fatal error.
}
}
});
const handlePinToggle = () => {
updateNote({
variables: {
noteId: note.id,
note: { pinned: !note.pinned }
}
});
};
return note.pinned ? (
<PushpinFilled size="large" onClick={handlePinToggle} style={{ color: "gold" }} />
) : (
<PushpinOutlined size="large" onClick={handlePinToggle} />
);
}
export default JobNotesPinToggle;

View File

@@ -23,6 +23,7 @@ import JobAltTransportChange from "../job-at-change/job-at-change.component";
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import PinnedJobNotes from "../pinned-job-notes/pinned-job-notes.component.jsx";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
@@ -102,254 +103,257 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail })
};
return (
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
<Col {...colSpan}>
<Card title={"Job Status"} style={{ height: "100%" }}>
<div>
<DataLabel label={t("jobs.fields.status")}>
<>
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
<Col {...colSpan}>
<Card title={"Job Status"} style={{ height: "100%" }}>
<div>
<DataLabel label={t("jobs.fields.status")}>
<Space wrap>
{job.status}
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
{job.iouparent && (
<Link to={`/manage/jobs/${job.iouparent}`}>
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
</Link>
)}
{job.production_vars && job.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{job.status === bodyshop.md_ro_statuses.default_scheduled && job.scheduled_in ? (
<Tag>
<Link to={`/manage/schedule?date=${dayjs(job.scheduled_in).format("YYYY-MM-DD")}`}>
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
</Link>
</Tag>
) : null}
</Space>
</DataLabel>
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
<ProductionListColumnComment record={job} />
</DataLabel>
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
{job.po_number}
</DataLabel>
<DataLabel label={t("jobs.fields.repairtotal")}>
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
<span style={{ margin: "0rem .5rem" }}>/</span>
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
</DataLabel>
<DataLabel label={t("jobs.fields.alt_transport")}>
{job.alt_transport}
<JobAltTransportChange job={job} />
</DataLabel>
{job?.cccontracts?.length > 0 && (
<DataLabel label={t("jobs.labels.contracts")}>
{job.cccontracts.map((c, index) => (
<Space key={c.id} wrap>
<Link to={`/manage/courtesycars/contracts/${c.id}`}>
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
{index !== job.cccontracts.length - 1 ? "," : null}
</Link>
</Space>
))}
</DataLabel>
)}
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
<Space>
<Checkbox
checked={!!job.estimate_sent_approval}
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
disabled={disabled}
>
{job.estimate_sent_approval && (
<span style={{ color: "#888" }}>
<DateTimeFormatter>{job.estimate_sent_approval}</DateTimeFormatter>
</span>
)}
</Checkbox>
</Space>
</DataLabel>
<DataLabel label={t("jobs.fields.estimate_approved")}>
<Space>
<Checkbox
checked={!!job.estimate_approved}
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
disabled={disabled}
>
{job.estimate_approved && (
<span style={{ color: "#888" }}>
<DateTimeFormatter>{job.estimate_approved}</DateTimeFormatter>
</span>
)}
</Checkbox>
</Space>
</DataLabel>
<Space wrap>
{job.status}
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
{job.iouparent && (
<Link to={`/manage/jobs/${job.iouparent}`}>
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
</Link>
)}
{job.production_vars && job.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{job.status === bodyshop.md_ro_statuses.default_scheduled && job.scheduled_in ? (
<Tag>
<Link to={`/manage/schedule?date=${dayjs(job.scheduled_in).format("YYYY-MM-DD")}`}>
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
</Link>
{job.special_coverage_policy && (
<Tag color="tomato">
<Space>
<WarningFilled />
<span>{t("jobs.labels.specialcoveragepolicy")}</span>
</Space>
</Tag>
) : null}
)}
{job.ca_gst_registrant && (
<Tag color="geekblue">
<Space>
<WarningFilled />
<span>{t("jobs.fields.ca_gst_registrant")}</span>
</Space>
</Tag>
)}
{job.hit_and_run && (
<Tag color="green">
<Space>
<WarningFilled />
<span>{t("jobs.fields.hit_and_run")}</span>
</Space>
</Tag>
)}
</Space>
</DataLabel>
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
<ProductionListColumnComment record={job} />
</DataLabel>
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
{job.po_number}
</DataLabel>
<DataLabel label={t("jobs.fields.repairtotal")}>
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
<span style={{ margin: "0rem .5rem" }}>/</span>
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
</DataLabel>
<DataLabel label={t("jobs.fields.alt_transport")}>
{job.alt_transport}
<JobAltTransportChange job={job} />
</DataLabel>
{job?.cccontracts?.length > 0 && (
<DataLabel label={t("jobs.labels.contracts")}>
{job.cccontracts.map((c, index) => (
<Space key={c.id} wrap>
<Link to={`/manage/courtesycars/contracts/${c.id}`}>
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
{index !== job.cccontracts.length - 1 ? "," : null}
</Link>
</Space>
))}
</DataLabel>
)}
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
<Space>
<Checkbox
checked={!!job.estimate_sent_approval}
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
disabled={disabled}
>
{job.estimate_sent_approval && (
<span style={{ color: "#888" }}>
<DateTimeFormatter>{job.estimate_sent_approval}</DateTimeFormatter>
</span>
)}
</Checkbox>
</Space>
</DataLabel>
<DataLabel label={t("jobs.fields.estimate_approved")}>
<Space>
<Checkbox
checked={!!job.estimate_approved}
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
disabled={disabled}
>
{job.estimate_approved && (
<span style={{ color: "#888" }}>
<DateTimeFormatter>{job.estimate_approved}</DateTimeFormatter>
</span>
)}
</Checkbox>
</Space>
</DataLabel>
<Space wrap>
{job.special_coverage_policy && (
<Tag color="tomato">
<Space>
<WarningFilled />
<span>{t("jobs.labels.specialcoveragepolicy")}</span>
</Space>
</Tag>
)}
{job.ca_gst_registrant && (
<Tag color="geekblue">
<Space>
<WarningFilled />
<span>{t("jobs.fields.ca_gst_registrant")}</span>
</Space>
</Tag>
)}
{job.hit_and_run && (
<Tag color="green">
<Space>
<WarningFilled />
<span>{t("jobs.fields.hit_and_run")}</span>
</Space>
</Tag>
)}
</Space>
</div>
</Card>
</Col>
<Col {...colSpan}>
<Card
style={{ height: "100%" }}
title={
disabled ? (
<>{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}</>
) : (
<Link to={`/manage/owners/${job.owner.id}`}>
{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}
</Link>
)
}
>
<div>
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
{disabled ? (
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
) : (
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
)}
</DataLabel>
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
{disabled ? (
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
) : (
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
)}
</DataLabel>
<DataLabel key="3" 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")}>
{disabled ? (
<>{job.ownr_ea || ""}</>
) : job.ownr_ea ? (
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
) : null}
</DataLabel>
{job.owner?.tax_number && (
<DataLabel key="5" label={t("owners.fields.tax_number")}>
{job.owner?.tax_number || ""}
</DataLabel>
)}
<DataLabel label={t("owners.fields.note")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
{job.owner?.note || ""}
</DataLabel>
</div>
</Card>
</Col>
<Col {...colSpan}>
<Card
style={{ height: "100%" }}
title={
job.vehicle ? (
</div>
</Card>
</Col>
<Col {...colSpan}>
<Card
style={{ height: "100%" }}
title={
disabled ? (
<>{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")} </>
<>{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}</>
) : (
<Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")}
<Link to={`/manage/owners/${job.owner.id}`}>
{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}
</Link>
)
) : (
<span></span>
)
}
>
<div>
<DataLabel key="2" label={t("vehicles.fields.plate_no")}>
{`${job.plate_no || t("general.labels.na")} (${`${job.plate_st || t("general.labels.na")}`})`}
</DataLabel>
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
<VehicleVinDisplay>{`${job.v_vin || t("general.labels.na")}`}</VehicleVinDisplay>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
job.v_vin?.length !== 17 ? (
<WarningFilled style={{ color: "tomato", marginLeft: ".3rem" }} />
) : null
) : null}
</DataLabel>
<DataLabel label={t("jobs.fields.regie_number")}>{job.regie_number || t("general.labels.na")}</DataLabel>
<DataLabel label={t("jobs.labels.relatedros")}>
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
</DataLabel>
{job.vehicle && job.vehicle.notes && (
<DataLabel
label={t("vehicles.fields.notes")}
valueStyle={{ whiteSpace: "pre-wrap" }}
valueClassName={notesClamped ? "clamp" : ""}
onValueClick={() => setNotesClamped(!notesClamped)}
>
{job.vehicle.notes}
}
>
<div>
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
{disabled ? (
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
) : (
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
)}
</DataLabel>
)}
{job.vehicle && job.vehicle.v_paint_codes && (
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}>
<span style={{ whiteSpace: "pre" }}>
{Object.keys(job.vehicle.v_paint_codes)
.filter(
(key) =>
job.vehicle.v_paint_codes[key] !== "" &&
job.vehicle.v_paint_codes[key] !== null &&
job.vehicle.v_paint_codes[key] !== undefined
)
.map((key, idx) => (
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
))}
</span>
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
{disabled ? (
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
) : (
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
)}
</DataLabel>
)}
</div>
</Card>
</Col>
<Col {...colSpan}>
<Card
style={{ height: "100%" }}
title={<span id="job-employee-assignments-title">{t("jobs.labels.employeeassignments")}</span>}
id={"job-employee-assignments"}
>
<div>
<JobEmployeeAssignments job={job} />
<Divider style={{ margin: ".5rem" }} />
<DataLabel label={t("jobs.labels.labor_hrs")}>
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} / {(bodyHrs + refinishHrs).toFixed(1)}
</DataLabel>
</div>
</Card>
</Col>
</Row>
<DataLabel key="3" 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")}>
{disabled ? (
<>{job.ownr_ea || ""}</>
) : job.ownr_ea ? (
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
) : null}
</DataLabel>
{job.owner?.tax_number && (
<DataLabel key="5" label={t("owners.fields.tax_number")}>
{job.owner?.tax_number || ""}
</DataLabel>
)}
<DataLabel label={t("owners.fields.note")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
{job.owner?.note || ""}
</DataLabel>
</div>
</Card>
</Col>
<Col {...colSpan}>
<Card
style={{ height: "100%" }}
title={
job.vehicle ? (
disabled ? (
<>{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")} </>
) : (
<Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")}
</Link>
)
) : (
<span></span>
)
}
>
<div>
<DataLabel key="2" label={t("vehicles.fields.plate_no")}>
{`${job.plate_no || t("general.labels.na")} (${`${job.plate_st || t("general.labels.na")}`})`}
</DataLabel>
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
<VehicleVinDisplay>{`${job.v_vin || t("general.labels.na")}`}</VehicleVinDisplay>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
job.v_vin?.length !== 17 ? (
<WarningFilled style={{ color: "tomato", marginLeft: ".3rem" }} />
) : null
) : null}
</DataLabel>
<DataLabel label={t("jobs.fields.regie_number")}>{job.regie_number || t("general.labels.na")}</DataLabel>
<DataLabel label={t("jobs.labels.relatedros")}>
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
</DataLabel>
{job.vehicle && job.vehicle.notes && (
<DataLabel
label={t("vehicles.fields.notes")}
valueStyle={{ whiteSpace: "pre-wrap" }}
valueClassName={notesClamped ? "clamp" : ""}
onValueClick={() => setNotesClamped(!notesClamped)}
>
{job.vehicle.notes}
</DataLabel>
)}
{job.vehicle && job.vehicle.v_paint_codes && (
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}>
<span style={{ whiteSpace: "pre" }}>
{Object.keys(job.vehicle.v_paint_codes)
.filter(
(key) =>
job.vehicle.v_paint_codes[key] !== "" &&
job.vehicle.v_paint_codes[key] !== null &&
job.vehicle.v_paint_codes[key] !== undefined
)
.map((key, idx) => (
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
))}
</span>
</DataLabel>
)}
</div>
</Card>
</Col>
<Col {...colSpan}>
<Card
style={{ height: "100%" }}
title={<span id="job-employee-assignments-title">{t("jobs.labels.employeeassignments")}</span>}
id={"job-employee-assignments"}
>
<div>
<JobEmployeeAssignments job={job} />
<Divider style={{ margin: ".5rem" }} />
<DataLabel label={t("jobs.labels.labor_hrs")}>
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} / {(bodyHrs + refinishHrs).toFixed(1)}
</DataLabel>
</div>
</Card>
</Col>
</Row>
<PinnedJobNotes job={job} />
</>
);
}

View File

@@ -12,6 +12,7 @@ import useLocalStorage from "../../utils/useLocalStorage";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import JobNotesPinToggle from "../job-notes-pin-toggle/job-notes-pin-toggle.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly
@@ -47,6 +48,9 @@ export function JobNotesComponent({
key: "icons",
width: 80,
filteredValue: filter?.icons || null,
defaultSortOrder: "desc",
multiple: 1,
sorter: (a, b) => a.pinned - b.pinned,
filters: [
{
text: t("notes.labels.usernotes"),
@@ -63,6 +67,7 @@ export function JobNotesComponent({
{record.critical ? <WarningFilled style={{ margin: 4, color: "red" }} /> : null}
{record.private ? <EyeInvisibleFilled style={{ margin: 4 }} /> : null}
{record.audit ? <AuditOutlined style={{ margin: 4 }} /> : null}
<JobNotesPinToggle note={record} />
</span>
)
},
@@ -100,6 +105,7 @@ export function JobNotesComponent({
dataIndex: "updated_at",
key: "updated_at",
defaultSortOrder: "descend",
multiple: 2,
width: 200,
sorter: (a, b) => new Date(a.updated_at) - new Date(b.updated_at),
render: (text, record) => <DateTimeFormatter>{record.updated_at}</DateTimeFormatter>

View File

@@ -23,17 +23,22 @@ export function NoteUpsertModalComponent({ form, noteUpsertModal }) {
return (
<>
<Row gutter={[16, 16]}>
<Col span={8}>
<Col span={6}>
<Form.Item label={t("notes.fields.critical")} name="critical" valuePropName="checked">
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<Col span={6}>
<Form.Item label={t("notes.fields.private")} name="private" valuePropName="checked">
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<Col span={6}>
<Form.Item label={t("notes.fields.pinned")} name="pinned" valuePropName="checked">
<Switch />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label={t("notes.fields.type")} name="type" initialValue="general">
<Select
options={[

View File

@@ -1,10 +1,12 @@
import { useMutation } from "@apollo/client";
import { Form, Modal } from "antd";
import React, { useEffect } from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries.js";
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
@@ -12,7 +14,6 @@ import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import NoteUpsertModalComponent from "./note-upsert-modal.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -65,6 +66,33 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
variables: {
noteId: existingNote.id,
note: values
},
update(cache, { data: { updateNote: updatedNote } }) {
try {
const existingJob = cache.readQuery({
query: GET_JOB_BY_PK,
variables: { id: jobId }
});
if (existingJob) {
cache.writeQuery({
query: GET_JOB_BY_PK,
variables: { id: jobId },
data: {
...existingJob,
job: {
...existingJob.job,
notes: updatedNote.pinned
? [updatedNote, ...existingJob.job.notes]
: existingJob.job.notes.filter((n) => n.id !== updatedNote.id)
}
}
});
}
} catch (error) {
// Cache miss is okay, query hasn't been executed yet
console.log("Cache miss for GET_JOB_BY_PK");
}
}
}).then((r) => {
notification["success"]({
@@ -86,6 +114,33 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
variables: {
noteInput: [{ ...values, jobid: jobId, created_by: currentUser.email }]
},
update(cache, { data: { updateNote: updatedNote } }) {
try {
const existingJob = cache.readQuery({
query: GET_JOB_BY_PK,
variables: { id: jobId }
});
if (existingJob) {
cache.writeQuery({
query: GET_JOB_BY_PK,
variables: { id: jobId },
data: {
...existingJob,
job: {
...existingJob.job,
notes: updatedNote.pinned
? [updatedNote, ...existingJob.job.notes]
: existingJob.job.notes.filter((n) => n.id !== updatedNote.id)
}
}
});
}
} catch (error) {
// Cache miss is okay, query hasn't been executed yet
console.log("Cache miss for GET_JOB_BY_PK");
}
},
refetchQueries: ["QUERY_NOTES_BY_JOB_PK"]
});

View File

@@ -0,0 +1,30 @@
import { Card, Divider, Space } from "antd";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import JobNotesPinToggle from "../job-notes-pin-toggle/job-notes-pin-toggle.component";
function PinnedJobNotes({ job }) {
const { t } = useTranslation();
const pinnedNotes = useMemo(() => {
return job?.notes?.filter((note) => note.pinned); //This will be typically filtered, but adding this to maximize flexibility of the component.
}, [job.notes]);
return pinnedNotes?.length ? (
<>
<Divider />
<Space direction="vertical" style={{ width: "100%" }}>
{pinnedNotes?.map((note) => (
<Card
key={note.id}
title={`${t("notes.labels.pinned_note")} - ${t(`notes.fields.types.${note.type}`)}`}
extra={<JobNotesPinToggle note={note} />}
>
{note.text}
</Card>
))}
</Space>
</>
) : null;
}
export default PinnedJobNotes;