feature/IO-3255-simplified-parts-management - Merge release

This commit is contained in:
Dave Richer
2025-07-22 12:22:52 -04:00
15 changed files with 1630 additions and 276 deletions

File diff suppressed because it is too large Load Diff

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";
@@ -104,7 +105,8 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, pa
};
return (
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
<>
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
<Col {...colSpan}>
<Card title={"Job Status"} style={{ height: "100%" }}>
<div>
@@ -226,122 +228,122 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, pa
</>
)}
</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 ? (
</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>
{!partsManagementOnly && (
<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>
{!partsManagementOnly && (
<Col {...colSpan}>
<Card
style={{ height: "100%" }}
@@ -358,7 +360,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, pa
</Card>
</Col>
)}
</Row>
</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;

View File

@@ -713,6 +713,19 @@ export const GET_JOB_BY_PK = gql`
v_model_yr
v_model_desc
v_vin
notes(where:{pinned: {_eq: true}}, order_by: {updated_at: desc}) {
created_at
created_by
critical
id
jobid
private
text
updated_at
audit
type
pinned
}
vehicle {
id
jobs {
@@ -959,6 +972,8 @@ export const QUERY_JOB_CARD_DETAILS = gql`
critical
private
created_at
pinned
type
}
updated_at
clm_total
@@ -984,6 +999,7 @@ export const QUERY_JOB_CARD_DETAILS = gql`
key
type
}
}
}
`;
@@ -1048,6 +1064,8 @@ export const QUERY_TECH_JOB_DETAILS = gql`
critical
private
created_at
pinned
type
}
updated_at
documents(order_by: { created_at: desc }) {
@@ -2369,7 +2387,7 @@ export const QUERY_JOBS_TECH_ASIGNED_TO_BY_TEAM = gql`
`;
export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
query QUERY_JOB_CARD_DETAILS($id: uuid!) {
query QUERY_PARTS_QUEUE_CARD_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
actual_completion
actual_delivery
@@ -2395,6 +2413,19 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
start
status
}
notes(where:{pinned: {_eq: true}}, order_by: {updated_at: desc}) {
created_at
created_by
critical
id
jobid
private
text
updated_at
audit
type
pinned
}
clm_no
clm_total
comment

View File

@@ -14,6 +14,7 @@ export const INSERT_NEW_NOTE = gql`
updated_at
audit
type
pinned
}
}
}
@@ -43,6 +44,7 @@ export const QUERY_NOTES_BY_JOB_PK = gql`
updated_at
audit
type
pinned
}
}
}
@@ -63,6 +65,7 @@ export const UPDATE_NOTE = gql`
updated_at
audit
type
pinned
}
}
}

View File

@@ -426,6 +426,11 @@
"messagingtext": "Messaging Preset Text",
"noteslabel": "Note Label",
"notestext": "Note Text",
"notifications": {
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
"invalid_followers": "Invalid selection. Please select valid employees.",
"placeholder": "Search for employees"
},
"partslocation": "Parts Location",
"phone": "Phone",
"prodtargethrs": "Production Target Hours",
@@ -648,15 +653,9 @@
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
"uselocalmediaserver": "Use Local Media Server?",
"website": "Website",
"zip_post": "Zip/Postal Code",
"notifications": {
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
"placeholder": "Search for employees",
"invalid_followers": "Invalid selection. Please select valid employees."
}
"zip_post": "Zip/Postal Code"
},
"labels": {
"consent_settings": "Phone Number Opt-Out List",
"2tiername": "Name => RO",
"2tiersetup": "2 Tier Setup",
"2tiersource": "Source => RO",
@@ -667,6 +666,7 @@
"apptcolors": "Appointment Colors",
"businessinformation": "Business Information",
"checklists": "Checklists",
"consent_settings": "Phone Number Opt-Out List",
"csiq": "CSI Questions",
"customtemplates": "Custom Templates",
"defaultcostsmapping": "Default Costs Mapping",
@@ -704,6 +704,9 @@
"messagingpresets": "Messaging Presets",
"notemplatesavailable": "No templates available to add.",
"notespresets": "Notes Presets",
"notifications": {
"followers": "Notifications"
},
"orderstatuses": "Order Statuses",
"partslocations": "Parts Locations",
"partsscan": "Parts Scanning",
@@ -734,10 +737,7 @@
"ssbuckets": "Job Size Definitions",
"systemsettings": "System Settings",
"task-presets": "Task Presets",
"workingdays": "Working Days",
"notifications": {
"followers": "Notifications"
}
"workingdays": "Working Days"
},
"operations": {
"contains": "Contains",
@@ -783,6 +783,15 @@
"completed": "Job checklist completed."
}
},
"consent": {
"associated_owners": "Associated Owners",
"created_at": "Opt-Out Date",
"no_owners": "No Associated Owners",
"phone_1": "Phone 1",
"phone_2": "Phone 2",
"phone_number": "Phone Number",
"text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in."
},
"contracts": {
"actions": {
"changerate": "Change Contract Rates",
@@ -1235,11 +1244,11 @@
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
"notfound": "No record was found.",
"sizelimit": "The selected items exceed the size limit.",
"submit-for-testing": "Error submitting Job for testing.",
"sub_status": {
"expired": "The subscription for this shop has expired. Please contact Sales to reactivate.",
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate."
}
},
"submit-for-testing": "Error submitting Job for testing."
},
"itemtypes": {
"contract": "CC Contract",
@@ -1659,8 +1668,6 @@
"adjustment_bottom_line": "Adjustments",
"adjustmenthours": "Adjustment Hours",
"alt_transport": "Alt. Trans.",
"estimate_sent_approval": "Estimate Sent for Approval",
"estimate_approved": "Estimate Approved",
"area_of_damage_impact": {
"10": "Left Front Side",
"11": "Left Front Corner",
@@ -1783,6 +1790,8 @@
"est_ct_ln": "Estimator Last Name",
"est_ea": "Estimator Email",
"est_ph1": "Estimator Phone #",
"estimate_approved": "Estimate Approved",
"estimate_sent_approval": "Estimate Sent for Approval",
"federal_tax_payable": "Federal Tax Payable",
"federal_tax_rate": "Federal Tax Rate",
"flat_rate_ats": "Flat Rate ATS?",
@@ -1966,8 +1975,6 @@
"scheddates": "Schedule Dates"
},
"labels": {
"sent": "",
"approved": "",
"accountsreceivable": "Accounts Receivable",
"act_price_ppc": "New Part Price",
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
@@ -1982,6 +1989,7 @@
"alreadyaddedtoscoreboard": "Job has already been added to scoreboard. Saving will update the previous entry.",
"alreadyclosed": "This Job has already been closed.",
"appointmentconfirmation": "Send confirmation to customer?",
"approved": "",
"associationwarning": "Any changes to associations will require updating the data from the new parent record to the Job.",
"audit": "Audit Trail",
"available": "Available",
@@ -2172,6 +2180,7 @@
"sales": "Sales",
"savebeforeconversion": "You have unsaved changes on the Job. Please save them before converting it. ",
"scheduledinchange": "The scheduled in is based off the latest appointment. To change this date, please schedule or reschedule the Job. ",
"sent": "",
"specialcoveragepolicy": "Special Coverage Policy Applies",
"state_tax_amt": "Provincial/State Taxes",
"subletsnotcompleted": "Outstanding Sublets",
@@ -2388,15 +2397,16 @@
},
"errors": {
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
"no_consent": "This phone number has opted-out of Messaging.",
"noattachedjobs": "No Jobs have been associated to this conversation. ",
"updatinglabel": "Error updating label. {{error}}",
"no_consent": "This phone number has opted-out of Messaging."
"updatinglabel": "Error updating label. {{error}}"
},
"labels": {
"addlabel": "Add a label to this conversation.",
"archive": "Archive",
"maxtenimages": "You can only select up to a maximum of 10 images at a time.",
"messaging": "Messaging",
"no_consent": "Opted-out",
"noallowtxt": "This customer has not indicated their permission to be messaged.",
"nojobs": "Not associated to any Job.",
"nopush": "Polling Mode Enabled",
@@ -2406,8 +2416,7 @@
"selectmedia": "Select Media",
"sentby": "Sent by {{by}} at {{time}}",
"typeamessage": "Send a message...",
"unarchive": "Unarchive",
"no_consent": "Opted-out"
"unarchive": "Unarchive"
},
"render": {
"conversation_list": "Conversation List"
@@ -2427,6 +2436,7 @@
"fields": {
"createdby": "Created By",
"critical": "Critical",
"pinned": "Pinned",
"private": "Private",
"text": "Contents",
"type": "Type",
@@ -2445,6 +2455,7 @@
"addtorelatedro": "Add to Related ROs",
"newnoteplaceholder": "Add a note...",
"notetoadd": "Note to Add",
"pinned_note": "Pinned Note",
"systemnotes": "System Notes",
"usernotes": "User Notes"
},
@@ -2467,11 +2478,15 @@
"fcm": "Push"
},
"labels": {
"auto-add": "Automatically watch Jobs I import",
"auto-add-success": "Auto watcher status successfully changed.",
"auto-add-failure": "Something went wrong updating your auto watcher status.",
"add-watchers": "Add Watchers",
"add-watchers-team": "Add Team Members",
"auto-add": "Automatically watch Jobs I import",
"auto-add-description": "",
"auto-add-failure": "Something went wrong updating your auto watcher status.",
"auto-add-off": "",
"auto-add-on": "",
"auto-add-success": "Auto watcher status successfully changed.",
"employee-notification": "Notifications are disabled because you do not have an associated Employee record.",
"employee-search": "Search for an Employee",
"mark-all-read": "Mark All Read",
"new-notification-title": "New Notification:",
@@ -2488,8 +2503,7 @@
"teams-search": "Search for a Team",
"unwatch": "Unwatch",
"watch": "Watch",
"watching-issue": "Watching",
"employee-notification": "Notifications are disabled because you do not have an associated Employee record."
"watching-issue": "Watching"
},
"scenarios": {
"alternate-transport-changed": "Alternate Transport Changed",
@@ -3299,17 +3313,10 @@
"updated": "Scoreboard updated."
}
},
"settings": {
"title": "Phone Number Opt-Out List"
},
"tasks": {
"labels": {
"my_tasks_center": "Task Center",
"go_to_job": "Go to Job",
"overdue": "Overdue",
"due_today": "Today",
"upcoming": "Upcoming",
"no_due_date": "Incomplete",
"ro-number": "RO #{{ro_number}}",
"no_tasks": "No Tasks Found"
},
"actions": {
"edit": "Edit Task",
"new": "New Task",
@@ -3324,9 +3331,6 @@
"myTasks": "Mine",
"refresh": "Refresh"
},
"errors": {
"load_failure": "Failed to load Tasks."
},
"date_presets": {
"completion": "Completion",
"day": "Day",
@@ -3340,6 +3344,9 @@
"tomorrow": "Tomorrow",
"two_weeks": "Two Weeks"
},
"errors": {
"load_failure": "Failed to load Tasks."
},
"failures": {
"completed": "Failed to toggle Task completion.",
"created": "Failed to create Task.",
@@ -3374,6 +3381,16 @@
"remind_at": "Remind At",
"title": "Title"
},
"labels": {
"due_today": "Today",
"go_to_job": "Go to Job",
"my_tasks_center": "Task Center",
"no_due_date": "Incomplete",
"no_tasks": "No Tasks Found",
"overdue": "Overdue",
"ro-number": "RO #{{ro_number}}",
"upcoming": "Upcoming"
},
"placeholders": {
"assigned_to": "Select an Employee",
"billid": "Select a Bill",
@@ -3889,18 +3906,6 @@
"validation": {
"unique_vendor_name": "You must enter a unique vendor name."
}
},
"consent": {
"phone_number": "Phone Number",
"associated_owners": "Associated Owners",
"created_at": "Opt-Out Date",
"no_owners": "No Associated Owners",
"phone_1": "Phone 1",
"phone_2": "Phone 2",
"text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in."
},
"settings": {
"title": "Phone Number Opt-Out List"
}
}
}

View File

@@ -426,6 +426,11 @@
"messagingtext": "",
"noteslabel": "",
"notestext": "",
"notifications": {
"description": "",
"invalid_followers": "",
"placeholder": ""
},
"partslocation": "",
"phone": "",
"prodtargethrs": "",
@@ -648,15 +653,9 @@
"use_paint_scale_data": "",
"uselocalmediaserver": "",
"website": "",
"zip_post": "",
"notifications": {
"description": "",
"placeholder": "",
"invalid_followers": ""
}
"zip_post": ""
},
"labels": {
"consent_settings": "",
"2tiername": "",
"2tiersetup": "",
"2tiersource": "",
@@ -667,6 +666,7 @@
"apptcolors": "",
"businessinformation": "",
"checklists": "",
"consent_settings": "",
"csiq": "",
"customtemplates": "",
"defaultcostsmapping": "",
@@ -704,6 +704,9 @@
"messagingpresets": "",
"notemplatesavailable": "",
"notespresets": "",
"notifications": {
"followers": ""
},
"orderstatuses": "",
"partslocations": "",
"partsscan": "",
@@ -734,10 +737,7 @@
"ssbuckets": "",
"systemsettings": "",
"task-presets": "",
"workingdays": "",
"notifications": {
"followers": ""
}
"workingdays": ""
},
"operations": {
"contains": "",
@@ -783,6 +783,15 @@
"completed": ""
}
},
"consent": {
"associated_owners": "",
"created_at": "",
"no_owners": "",
"phone_1": "",
"phone_2": "",
"phone_number": "",
"text_body": ""
},
"contracts": {
"actions": {
"changerate": "",
@@ -1235,11 +1244,11 @@
"fcm": "",
"notfound": "",
"sizelimit": "",
"submit-for-testing": "",
"sub_status": {
"expired": "",
"trial-expired": ""
}
},
"submit-for-testing": ""
},
"itemtypes": {
"contract": "",
@@ -1651,8 +1660,6 @@
"voiding": ""
},
"fields": {
"estimate_sent_approval": "",
"estimate_approved": "",
"active_tasks": "",
"actual_completion": "Realización real",
"actual_delivery": "Entrega real",
@@ -1783,6 +1790,8 @@
"est_ct_ln": "Apellido del tasador",
"est_ea": "Correo electrónico del tasador",
"est_ph1": "Número de teléfono del tasador",
"estimate_approved": "",
"estimate_sent_approval": "",
"federal_tax_payable": "Impuesto federal por pagar",
"federal_tax_rate": "",
"flat_rate_ats": "",
@@ -1966,8 +1975,6 @@
"scheddates": ""
},
"labels": {
"sent": "",
"approved": "",
"accountsreceivable": "",
"act_price_ppc": "",
"actual_completion_inferred": "",
@@ -1982,6 +1989,7 @@
"alreadyaddedtoscoreboard": "",
"alreadyclosed": "",
"appointmentconfirmation": "¿Enviar confirmación al cliente?",
"approved": "",
"associationwarning": "",
"audit": "",
"available": "",
@@ -2172,6 +2180,7 @@
"sales": "",
"savebeforeconversion": "",
"scheduledinchange": "",
"sent": "",
"specialcoveragepolicy": "",
"state_tax_amt": "",
"subletsnotcompleted": "",
@@ -2388,15 +2397,16 @@
},
"errors": {
"invalidphone": "",
"no_consent": "",
"noattachedjobs": "",
"updatinglabel": "",
"no_consent": ""
"updatinglabel": ""
},
"labels": {
"addlabel": "",
"archive": "",
"maxtenimages": "",
"messaging": "Mensajería",
"no_consent": "",
"noallowtxt": "",
"nojobs": "",
"nopush": "",
@@ -2406,8 +2416,7 @@
"selectmedia": "",
"sentby": "",
"typeamessage": "Enviar un mensaje...",
"unarchive": "",
"no_consent": ""
"unarchive": ""
},
"render": {
"conversation_list": ""
@@ -2427,6 +2436,7 @@
"fields": {
"createdby": "Creado por",
"critical": "Crítico",
"pinned": "",
"private": "Privado",
"text": "Contenido",
"type": "",
@@ -2445,6 +2455,7 @@
"addtorelatedro": "",
"newnoteplaceholder": "Agrega una nota...",
"notetoadd": "",
"pinned_note": "",
"systemnotes": "",
"usernotes": ""
},
@@ -2467,13 +2478,15 @@
"fcm": ""
},
"labels": {
"auto-add-on": "",
"auto-add-off": "",
"auto-add-success": "",
"auto-add-failure": "",
"auto-add-description": "",
"add-watchers": "",
"add-watchers-team": "",
"auto-add": "",
"auto-add-description": "",
"auto-add-failure": "",
"auto-add-off": "",
"auto-add-on": "",
"auto-add-success": "",
"employee-notification": "",
"employee-search": "",
"mark-all-read": "",
"new-notification-title": "",
@@ -2490,8 +2503,7 @@
"teams-search": "",
"unwatch": "",
"watch": "",
"watching-issue": "",
"employee-notification": ""
"watching-issue": ""
},
"scenarios": {
"alternate-transport-changed": "",
@@ -3301,17 +3313,10 @@
"updated": ""
}
},
"settings": {
"title": ""
},
"tasks": {
"labels": {
"my_tasks_center": "",
"go_to_job": "",
"overdue": "",
"due_today": "",
"upcoming": "",
"no_due_date": "",
"ro-number": "",
"no_tasks": ""
},
"actions": {
"edit": "",
"new": "",
@@ -3326,9 +3331,6 @@
"myTasks": "",
"refresh": ""
},
"errors": {
"load_failure": ""
},
"date_presets": {
"completion": "",
"day": "",
@@ -3342,6 +3344,9 @@
"tomorrow": "",
"two_weeks": ""
},
"errors": {
"load_failure": ""
},
"failures": {
"completed": "",
"created": "",
@@ -3376,6 +3381,16 @@
"remind_at": "",
"title": ""
},
"labels": {
"due_today": "",
"go_to_job": "",
"my_tasks_center": "",
"no_due_date": "",
"no_tasks": "",
"overdue": "",
"ro-number": "",
"upcoming": ""
},
"placeholders": {
"assigned_to": "",
"billid": "",
@@ -3891,18 +3906,6 @@
"validation": {
"unique_vendor_name": ""
}
},
"consent": {
"phone_number": "",
"associated_owners": "",
"created_at": "",
"no_owners": "",
"phone_1": "",
"phone_2": "",
"text_body": ""
},
"settings": {
"title": ""
}
}
}

View File

@@ -426,6 +426,11 @@
"messagingtext": "",
"noteslabel": "",
"notestext": "",
"notifications": {
"description": "",
"invalid_followers": "",
"placeholder": ""
},
"partslocation": "",
"phone": "",
"prodtargethrs": "",
@@ -648,15 +653,9 @@
"use_paint_scale_data": "",
"uselocalmediaserver": "",
"website": "",
"zip_post": "",
"notifications": {
"description": "",
"placeholder": "",
"invalid_followers": ""
}
"zip_post": ""
},
"labels": {
"consent_settings": "",
"2tiername": "",
"2tiersetup": "",
"2tiersource": "",
@@ -667,6 +666,7 @@
"apptcolors": "",
"businessinformation": "",
"checklists": "",
"consent_settings": "",
"csiq": "",
"customtemplates": "",
"defaultcostsmapping": "",
@@ -704,6 +704,9 @@
"messagingpresets": "",
"notemplatesavailable": "",
"notespresets": "",
"notifications": {
"followers": ""
},
"orderstatuses": "",
"partslocations": "",
"partsscan": "",
@@ -734,10 +737,7 @@
"ssbuckets": "",
"systemsettings": "",
"task-presets": "",
"workingdays": "",
"notifications": {
"followers": ""
}
"workingdays": ""
},
"operations": {
"contains": "",
@@ -783,6 +783,15 @@
"completed": ""
}
},
"consent": {
"associated_owners": "Associated Owners",
"created_at": "Opt-Out Date",
"no_owners": "No Associated Owners",
"phone_1": "Phone 1",
"phone_2": "Phone 2",
"phone_number": "Phone Number",
"text_body": ""
},
"contracts": {
"actions": {
"changerate": "",
@@ -1235,11 +1244,11 @@
"fcm": "",
"notfound": "",
"sizelimit": "",
"submit-for-testing": "",
"sub_status": {
"expired": "",
"trial-expired": ""
}
},
"submit-for-testing": ""
},
"itemtypes": {
"contract": "",
@@ -1651,8 +1660,6 @@
"voiding": ""
},
"fields": {
"estimate_sent_approval": "",
"estimate_approved": "",
"active_tasks": "",
"actual_completion": "Achèvement réel",
"actual_delivery": "Livraison réelle",
@@ -1783,6 +1790,8 @@
"est_ct_ln": "Nom de l'évaluateur",
"est_ea": "Courriel de l'évaluateur",
"est_ph1": "Numéro de téléphone de l'évaluateur",
"estimate_approved": "",
"estimate_sent_approval": "",
"federal_tax_payable": "Impôt fédéral à payer",
"federal_tax_rate": "",
"flat_rate_ats": "",
@@ -1966,8 +1975,6 @@
"scheddates": ""
},
"labels": {
"sent": "",
"approved": "",
"accountsreceivable": "",
"act_price_ppc": "",
"actual_completion_inferred": "",
@@ -1982,6 +1989,7 @@
"alreadyaddedtoscoreboard": "",
"alreadyclosed": "",
"appointmentconfirmation": "Envoyer une confirmation au client?",
"approved": "",
"associationwarning": "",
"audit": "",
"available": "",
@@ -2172,6 +2180,7 @@
"sales": "",
"savebeforeconversion": "",
"scheduledinchange": "",
"sent": "",
"specialcoveragepolicy": "",
"state_tax_amt": "",
"subletsnotcompleted": "",
@@ -2388,15 +2397,16 @@
},
"errors": {
"invalidphone": "",
"no_consent": "",
"noattachedjobs": "",
"updatinglabel": "",
"no_consent": ""
"updatinglabel": ""
},
"labels": {
"addlabel": "",
"archive": "",
"maxtenimages": "",
"messaging": "Messagerie",
"no_consent": "",
"noallowtxt": "",
"nojobs": "",
"nopush": "",
@@ -2406,8 +2416,7 @@
"selectmedia": "",
"sentby": "",
"typeamessage": "Envoyer un message...",
"unarchive": "",
"no_consent": ""
"unarchive": ""
},
"render": {
"conversation_list": ""
@@ -2427,6 +2436,7 @@
"fields": {
"createdby": "Créé par",
"critical": "Critique",
"pinned": "",
"private": "privé",
"text": "Contenu",
"type": "",
@@ -2445,6 +2455,7 @@
"addtorelatedro": "",
"newnoteplaceholder": "Ajouter une note...",
"notetoadd": "",
"pinned_note": "",
"systemnotes": "",
"usernotes": ""
},
@@ -2467,13 +2478,15 @@
"fcm": ""
},
"labels": {
"auto-add-on": "",
"auto-add-off": "",
"auto-add-success": "",
"auto-add-failure": "",
"auto-add-description": "",
"add-watchers": "",
"add-watchers-team": "",
"auto-add": "",
"auto-add-description": "",
"auto-add-failure": "",
"auto-add-off": "",
"auto-add-on": "",
"auto-add-success": "",
"employee-notification": "",
"employee-search": "",
"mark-all-read": "",
"new-notification-title": "",
@@ -2490,8 +2503,7 @@
"teams-search": "",
"unwatch": "",
"watch": "",
"watching-issue": "",
"employee-notification": ""
"watching-issue": ""
},
"scenarios": {
"alternate-transport-changed": "",
@@ -3301,17 +3313,10 @@
"updated": ""
}
},
"settings": {
"title": ""
},
"tasks": {
"labels": {
"my_tasks_center": "",
"go_to_job": "",
"overdue": "",
"due_today": "",
"upcoming": "",
"no_due_date": "",
"ro-number": "",
"no_tasks": ""
},
"actions": {
"edit": "",
"new": "",
@@ -3326,9 +3331,6 @@
"myTasks": "",
"refresh": ""
},
"errors": {
"load_failure": ""
},
"date_presets": {
"completion": "",
"day": "",
@@ -3342,6 +3344,9 @@
"tomorrow": "",
"two_weeks": ""
},
"errors": {
"load_failure": ""
},
"failures": {
"completed": "",
"created": "",
@@ -3376,6 +3381,16 @@
"remind_at": "",
"title": ""
},
"labels": {
"due_today": "",
"go_to_job": "",
"my_tasks_center": "",
"no_due_date": "",
"no_tasks": "",
"overdue": "",
"ro-number": "",
"upcoming": ""
},
"placeholders": {
"assigned_to": "",
"billid": "",
@@ -3891,18 +3906,6 @@
"validation": {
"unique_vendor_name": ""
}
},
"consent": {
"phone_number": "Phone Number",
"associated_owners": "Associated Owners",
"created_at": "Opt-Out Date",
"no_owners": "No Associated Owners",
"phone_1": "Phone 1",
"phone_2": "Phone 2",
"text_body": ""
},
"settings": {
"title": ""
}
}
}

View File

@@ -4909,6 +4909,7 @@
- critical
- id
- jobid
- pinned
- private
- text
- type
@@ -4923,6 +4924,7 @@
- critical
- id
- jobid
- pinned
- private
- text
- type
@@ -4947,6 +4949,7 @@
- critical
- id
- jobid
- pinned
- private
- text
- type

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."notes" add column "pinned" boolean
-- not null default 'false';

View File

@@ -0,0 +1,2 @@
alter table "public"."notes" add column "pinned" boolean
not null default 'false';