diff --git a/babel-translations.babel b/babel-translations.babel
index 11c2b9d..1fdf8d4 100644
--- a/babel-translations.babel
+++ b/babel-translations.babel
@@ -639,6 +639,27 @@
+
+ status
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
diff --git a/components/global-search/global-search.jsx b/components/global-search/global-search.jsx
index 645d520..0de24db 100644
--- a/components/global-search/global-search.jsx
+++ b/components/global-search/global-search.jsx
@@ -3,7 +3,7 @@ import { useLocalSearchParams } from "expo-router";
import debounce from "lodash/debounce";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
-import { FlatList, View } from "react-native";
+import { FlatList, KeyboardAvoidingView, Platform } from "react-native";
import { ActivityIndicator, Icon, Text } from "react-native-paper";
import env from "../../env";
import ErrorDisplay from "../error/error-display";
@@ -40,7 +40,7 @@ export default function GlobalSearch() {
const [error, setError] = useState(null);
const [results, setResults] = useState([]);
const { t } = useTranslation();
- // Placeholder: Replace with actual API call (e.g., Apollo client query, REST fetch, Redux saga dispatch)
+
const performSearch = useCallback(async (query) => {
// Defensive trimr
const q = (query || "").trim();
@@ -57,7 +57,7 @@ export default function GlobalSearch() {
?.filter((hit) => hit._index === "jobs")
.map((hit) => hit._source);
- setResults(jobResults);
+ setResults([...jobResults, { id: "footer-spacer", height: 128 }]);
} else {
setError("No results available. Try again.");
}
@@ -79,23 +79,35 @@ export default function GlobalSearch() {
if (globalSearch === undefined || globalSearch.trim() === "") {
return (
-
+
{t("globalsearch.labels.entersearch")}
-
+
);
}
return (
-
- {loading && }
+
+ {loading && }
{error && }
-
- {results.length} results found
-
+ {!loading && (
+
+ {results.length} results found
+
+ )}
item.id?.toString()}
renderItem={(object) => }
/>
-
+
);
}
diff --git a/components/job-notes/job-notes.jsx b/components/job-notes/job-notes.jsx
index e76a14f..f952230 100644
--- a/components/job-notes/job-notes.jsx
+++ b/components/job-notes/job-notes.jsx
@@ -3,10 +3,16 @@ import { useQuery } from "@apollo/client";
import { AntDesign } from "@expo/vector-icons";
import { useGlobalSearchParams } from "expo-router";
import { DateTime } from "luxon";
-import React, { useCallback, useState } from "react";
+import React, { memo, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { FlatList, RefreshControl, View } from "react-native";
-import { ActivityIndicator, Button, Card, Text } from "react-native-paper";
+import {
+ ActivityIndicator,
+ Button,
+ Card,
+ Divider,
+ Text,
+} from "react-native-paper";
import ErrorDisplay from "../error/error-display";
import NewNoteModal from "./new-note-modal";
@@ -33,17 +39,41 @@ export default function JobNotes() {
return refetch();
};
+ const job = data?.jobs_by_pk;
+
+ // Memoized list data (only when job & notes exist)
+ const listData = useMemo(() => {
+ if (!job?.notes) return [];
+ const notes = job.notes;
+ const pinnedNotes = [];
+ const otherNotes = [];
+ for (const n of notes) {
+ (n.pinned ? pinnedNotes : otherNotes).push(n);
+ }
+ pinnedNotes.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
+ otherNotes.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
+ const DIVIDER_ID = "__divider__";
+ return pinnedNotes.length > 0 && otherNotes.length > 0
+ ? [...pinnedNotes, { id: DIVIDER_ID, type: "divider" }, ...otherNotes]
+ : [...pinnedNotes, ...otherNotes];
+ }, [job?.notes]);
+
+ const keyExtractor = useCallback((item) => item.id, []);
+ const renderItem = useCallback(
+ ({ item }) =>
+ item.type === "divider" ? : ,
+ []
+ );
+
if (loading) {
return ;
}
if (error) {
return ;
}
-
- if (!data?.jobs_by_pk) {
+ if (!job) {
return ;
}
- const job = data.jobs_by_pk;
return (
@@ -55,7 +85,7 @@ export default function JobNotes() {
>
{t("jobdetail.actions.addnote")}
- {job.notes.length === 0 ? (
+ {listData.length === 0 ? (
{t("jobdetail.labels.nojobnotes")}
@@ -67,8 +97,13 @@ export default function JobNotes() {
}
style={{ flex: 1 }}
- data={job.notes}
- renderItem={(object) => }
+ data={listData}
+ keyExtractor={keyExtractor}
+ renderItem={renderItem}
+ removeClippedSubviews
+ initialNumToRender={12}
+ maxToRenderPerBatch={8}
+ windowSize={7}
/>
)}
@@ -90,26 +125,20 @@ function NoteListItem({ item }) {
{item.text}
+ {item.pinned && (
+
+ )}
{item.private && (
-
+
)}
{item.critical && (
-
+
)}
{item.created_by}
@@ -122,4 +151,8 @@ function NoteListItem({ item }) {
);
-}
+});
+
+const DividerItem = memo(function DividerItem() {
+ return ;
+});
diff --git a/components/job-notes/new-note-modal.jsx b/components/job-notes/new-note-modal.jsx
index f3ce601..720332e 100644
--- a/components/job-notes/new-note-modal.jsx
+++ b/components/job-notes/new-note-modal.jsx
@@ -124,7 +124,7 @@ const NewNoteModal = ({
type: values.type,
created_by: currentUser?.email,
};
- console.log("*** ~ handleSubmit ~ noteInput:", noteInput);
+
// TODO: If backend supports attaching note to multiple related ROs, perform additional mutation(s)
// here after creating the base note. This may involve an insert into a join table like note_jobs.
// values.relatedros contains boolean flags keyed by job id.
@@ -272,7 +272,12 @@ const NewNoteModal = ({
{ paddingBottom: Math.max(insets.bottom, 12) },
]}
>
-
- {!!vehicle && (
-
- {vehicle}
-
- )}
+
+ {vehicle}
+
+
{item.status}
diff --git a/components/settings/settings.jsx b/components/settings/settings.jsx
index 0c0d3ce..c4dd123 100644
--- a/components/settings/settings.jsx
+++ b/components/settings/settings.jsx
@@ -136,6 +136,7 @@ function Tab({ bodyshop, currentUser, signOutStart }) {
{
try {
await registerForPushNotificationsAsync();
diff --git a/graphql/jobs.queries.js b/graphql/jobs.queries.js
index 82e59a9..4500608 100644
--- a/graphql/jobs.queries.js
+++ b/graphql/jobs.queries.js
@@ -1,4 +1,4 @@
-import gql from "graphql-tag";
+import { gql } from "graphql-tag";
export const QUERY_ALL_ACTIVE_JOBS = gql`
query QUERY_ALL_ACTIVE_JOBS($statuses: [String!]!) {
@@ -208,6 +208,7 @@ export const GET_JOB_BY_PK = gql`
text
critical
private
+ pinned
created_at
updated_at
created_by
diff --git a/translations/en-US/common.json b/translations/en-US/common.json
index 579fe1f..2d7a404 100644
--- a/translations/en-US/common.json
+++ b/translations/en-US/common.json
@@ -46,7 +46,8 @@
"lines_price": "$",
"lines_qty": "Qty.",
"nojobnotes": "There are no notes.",
- "notes": "Notes"
+ "notes": "Notes",
+ "status": "Status"
},
"lbr_types": {
"LA1": "LA1",
diff --git a/translations/es-MX/common.json b/translations/es-MX/common.json
index 6e2cb7f..5a96fb6 100644
--- a/translations/es-MX/common.json
+++ b/translations/es-MX/common.json
@@ -46,7 +46,8 @@
"lines_price": "",
"lines_qty": "",
"nojobnotes": "",
- "notes": ""
+ "notes": "",
+ "status": ""
},
"lbr_types": {
"LA1": "",
diff --git a/translations/fr-CA/common.json b/translations/fr-CA/common.json
index a1afa92..628c4ab 100644
--- a/translations/fr-CA/common.json
+++ b/translations/fr-CA/common.json
@@ -46,7 +46,8 @@
"lines_price": "",
"lines_qty": "",
"nojobnotes": "",
- "notes": ""
+ "notes": "",
+ "status": ""
},
"lbr_types": {
"LA1": "",