Skip to content
Merged
4 changes: 3 additions & 1 deletion apps/companion/app/(tabs)/assets/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ import {
borderRadius,
formatStatus,
getQuantityStatusLabel,
formatDate,
formatCurrency,
} from "@/lib/constants";
import { useTheme } from "@/lib/theme-context";
import { createStyles } from "@/lib/create-styles";
import { useDateFormatter } from "@/lib/use-date-formatter";
import { TeamMemberPicker } from "@/components/team-member-picker";
import { LocationPicker } from "@/components/location-picker";
import { QuantityInputSheet } from "@/components/quantity-input-sheet";
Expand Down Expand Up @@ -94,6 +94,8 @@ export default function AssetDetailScreen() {
const { user } = useAuth();
const { colors, statusBadge } = useTheme();
const styles = useStyles();
// Render dates in the acting user's format preferences + timezone.
const { formatDate } = useDateFormatter();

// Asset data
const {
Expand Down
3 changes: 2 additions & 1 deletion apps/companion/app/(tabs)/assets/kits/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ import {
spacing,
borderRadius,
formatStatus,
formatDate,
formatCurrency,
} from "@/lib/constants";
import { useDateFormatter } from "@/lib/use-date-formatter";
import { useTheme } from "@/lib/theme-context";
import { createStyles } from "@/lib/create-styles";
import { InfoRow } from "@/components/shared/info-row";
Expand Down Expand Up @@ -72,6 +72,7 @@ export default function KitDetailScreen() {
const { currentOrg } = useOrg();
const { colors, statusBadge } = useTheme();
const styles = useStyles();
const { formatDate } = useDateFormatter();

// Role-aware UI — the server re-enforces these on every API call.
const roles = currentOrg?.roles;
Expand Down
11 changes: 4 additions & 7 deletions apps/companion/app/(tabs)/audits/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,8 @@ import {
type AuditAssetStatus,
} from "@/lib/api";
import { useOrg } from "@/lib/org-context";
import {
fontSize,
spacing,
borderRadius,
formatDateTime,
} from "@/lib/constants";
import { fontSize, spacing, borderRadius } from "@/lib/constants";
import { useDateFormatter } from "@/lib/use-date-formatter";
import { useTheme } from "@/lib/theme-context";
import { createStyles } from "@/lib/create-styles";
import { ErrorBoundary } from "@/components/error-boundary";
Expand Down Expand Up @@ -101,6 +97,7 @@ function AuditDetailContent() {
const router = useRouter();
const { currentOrg } = useOrg();
const { colors, auditStatusBadge, auditAssetStatusBadge } = useTheme();
const { formatDateTime } = useDateFormatter();
const styles = useStyles();

// ── State ──────────────────────────────────────────────
Expand Down Expand Up @@ -454,7 +451,7 @@ function AuditDetailContent() {
</View>
);
},
[colors, auditAssetStatusBadge, styles]
[colors, auditAssetStatusBadge, styles, formatDateTime]
);

// ── Loading / Error states ────────────────────────────
Expand Down
6 changes: 4 additions & 2 deletions apps/companion/app/(tabs)/audits/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { api, type AuditListItem } from "@/lib/api";
import { useOrg } from "@/lib/org-context";
import { fontSize, spacing, borderRadius, hitSlop } from "@/lib/constants";
import { formatDue } from "@/lib/audit-format";
import { useFormatPrefs } from "@/lib/use-date-formatter";
import { useTheme } from "@/lib/theme-context";
import { createStyles } from "@/lib/create-styles";
import { ErrorBoundary } from "@/components/error-boundary";
Expand Down Expand Up @@ -51,6 +52,7 @@ function AuditsListContent() {
} = useOrg();
const { colors, auditStatusBadge } = useTheme();
const styles = useStyles();
const prefs = useFormatPrefs();
const [audits, setAudits] = useState<AuditListItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
Expand Down Expand Up @@ -273,7 +275,7 @@ function AuditsListContent() {
: 0;
const progressPercent = Math.round(progress * 100);

const due = formatDue(item.dueDate, isActive);
const due = formatDue(item.dueDate, isActive, prefs);
const dueColor =
due.tier === "overdue"
? colors.error
Expand Down Expand Up @@ -467,7 +469,7 @@ function AuditsListContent() {
</TouchableOpacity>
);
},
[router, colors, auditStatusBadge, styles]
[router, colors, auditStatusBadge, styles, prefs]
);

if (orgLoading) {
Expand Down
10 changes: 3 additions & 7 deletions apps/companion/app/(tabs)/bookings/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,8 @@ import {
type CheckinDisposition,
} from "@/lib/api";
import { useOrg } from "@/lib/org-context";
import {
fontSize,
spacing,
borderRadius,
formatStatus,
formatDateTime,
} from "@/lib/constants";
import { fontSize, spacing, borderRadius, formatStatus } from "@/lib/constants";
import { useDateFormatter } from "@/lib/use-date-formatter";
import { useTheme } from "@/lib/theme-context";
import { createStyles } from "@/lib/create-styles";
import { BookingDetailSkeleton } from "@/components/skeleton-loader";
Expand Down Expand Up @@ -110,6 +105,7 @@ export default function BookingDetailScreen() {
const { currentOrg } = useOrg();
const { colors, statusBadge, bookingStatusBadge } = useTheme();
const styles = useStyles();
const { formatDateTime } = useDateFormatter();

const [booking, setBooking] = useState<BookingDetail | null>(null);
const [checkedInAssetIds, setCheckedInAssetIds] = useState<string[]>([]);
Expand Down
30 changes: 16 additions & 14 deletions apps/companion/app/(tabs)/bookings/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { api, type BookingTag } from "@/lib/api";
import { useOrg } from "@/lib/org-context";
import { markBookingDirty } from "@/lib/booking-refresh";
import { fontSize, spacing, borderRadius } from "@/lib/constants";
import { useDateFormatter } from "@/lib/use-date-formatter";
import { useTheme } from "@/lib/theme-context";
import { createStyles } from "@/lib/create-styles";
import { labelForRequired } from "@/lib/a11y";
Expand All @@ -55,22 +56,17 @@ function toLocalWire(d: Date): string {
)}:${p(d.getMinutes())}`;
}

function formatDisplay(d: Date): string {
return d.toLocaleString(undefined, {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
}

export default function EditBookingScreen() {
const router = useRouter();
const { id } = useLocalSearchParams<{ id: string }>();
const { currentOrg } = useOrg();
const { colors } = useTheme();
const styles = useStyles();
// Picker button labels use the user's date/time FORMAT (order, 12/24h) but stay
// DEVICE-local (`localeOnly`) — the native picker and `toLocalWire` submission
// are device-local, so formatting them in the preferred timezone would show a
// different time than the one being edited and submitted (CodeRabbit, #2798).
const { formatDateTime } = useDateFormatter();

const [isLoading, setIsLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
Expand Down Expand Up @@ -404,15 +400,19 @@ export default function EditBookingScreen() {
}}
accessibilityRole="button"
accessibilityLabel={
from ? `Starts ${formatDisplay(from)}` : "Start"
from
? `Starts ${formatDateTime(from, { localeOnly: true })}`
: "Start"
}
>
<Text
style={
from ? styles.pickerSelectedText : styles.pickerPlaceholder
}
>
{from ? formatDisplay(from) : "Choose start..."}
{from
? formatDateTime(from, { localeOnly: true })
: "Choose start..."}
</Text>
{isDraft && (
<Ionicons
Expand Down Expand Up @@ -449,12 +449,14 @@ export default function EditBookingScreen() {
setShowFromPicker(false);
}}
accessibilityRole="button"
accessibilityLabel={to ? `Ends ${formatDisplay(to)}` : "End"}
accessibilityLabel={
to ? `Ends ${formatDateTime(to, { localeOnly: true })}` : "End"
}
>
<Text
style={to ? styles.pickerSelectedText : styles.pickerPlaceholder}
>
{to ? formatDisplay(to) : "Choose end..."}
{to ? formatDateTime(to, { localeOnly: true }) : "Choose end..."}
</Text>
{isDraft && (
<Ionicons
Expand Down
5 changes: 3 additions & 2 deletions apps/companion/app/(tabs)/bookings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ import {
spacing,
borderRadius,
formatStatus,
formatDateTime,
bookingCountdown,
hitSlop,
} from "@/lib/constants";
import { useDateFormatter } from "@/lib/use-date-formatter";
import { useTheme } from "@/lib/theme-context";
import { createStyles } from "@/lib/create-styles";
import { ErrorBoundary } from "@/components/error-boundary";
Expand Down Expand Up @@ -98,6 +98,7 @@ function BookingsListContent() {
} = useOrg();
const { colors, bookingStatusBadge } = useTheme();
const styles = useStyles();
const { formatDateTime } = useDateFormatter();
const [bookings, setBookings] = useState<BookingListItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
Expand Down Expand Up @@ -403,7 +404,7 @@ function BookingsListContent() {
</TouchableOpacity>
);
},
[router, colors, bookingStatusBadge, styles]
[router, colors, bookingStatusBadge, styles, formatDateTime]
);

if (orgLoading) {
Expand Down
35 changes: 20 additions & 15 deletions apps/companion/app/(tabs)/bookings/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import DateTimePicker, {
import { api, type BookingTag, type TeamMember } from "@/lib/api";
import { useOrg } from "@/lib/org-context";
import { fontSize, spacing, borderRadius } from "@/lib/constants";
import { useDateFormatter } from "@/lib/use-date-formatter";
import { useTheme } from "@/lib/theme-context";
import { createStyles } from "@/lib/create-styles";
import { labelForRequired } from "@/lib/a11y";
Expand All @@ -61,22 +62,16 @@ function toLocalWire(d: Date): string {
)}:${p(d.getMinutes())}`;
}

/** Human-readable date-time for the picker buttons. */
function formatDisplay(d: Date): string {
return d.toLocaleString(undefined, {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
}

export default function CreateBookingScreen() {
const router = useRouter();
const { currentOrg } = useOrg();
const { colors } = useTheme();
const styles = useStyles();
// Picker button labels use the user's date/time FORMAT (order, 12/24h) but stay
// DEVICE-local (`localeOnly`) — the native picker and `toLocalWire` submission
// are device-local, so formatting them in the preferred timezone would show a
// different time than the one being edited and submitted (CodeRabbit, #2798).
const { formatDateTime } = useDateFormatter();

// ── Form state ──────────────────────────────────
const [name, setName] = useState("");
Expand Down Expand Up @@ -347,7 +342,9 @@ export default function CreateBookingScreen() {
accessibilityRole="button"
accessibilityLabel={
from
? `Starts ${formatDisplay(from)}, tap to change`
? `Starts ${formatDateTime(from, {
localeOnly: true,
})}, tap to change`
: "Choose start"
}
>
Expand All @@ -356,7 +353,9 @@ export default function CreateBookingScreen() {
from ? styles.pickerSelectedText : styles.pickerPlaceholder
}
>
{from ? formatDisplay(from) : "Choose start date & time..."}
{from
? formatDateTime(from, { localeOnly: true })
: "Choose start date & time..."}
</Text>
<Ionicons
name="calendar-outline"
Expand Down Expand Up @@ -391,13 +390,19 @@ export default function CreateBookingScreen() {
}}
accessibilityRole="button"
accessibilityLabel={
to ? `Ends ${formatDisplay(to)}, tap to change` : "Choose end"
to
? `Ends ${formatDateTime(to, {
localeOnly: true,
})}, tap to change`
: "Choose end"
}
>
<Text
style={to ? styles.pickerSelectedText : styles.pickerPlaceholder}
>
{to ? formatDisplay(to) : "Choose end date & time..."}
{to
? formatDateTime(to, { localeOnly: true })
: "Choose end date & time..."}
</Text>
<Ionicons
name="calendar-outline"
Expand Down
17 changes: 8 additions & 9 deletions apps/companion/app/(tabs)/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { useOrg } from "@/lib/org-context";
import { pushIntoTab } from "@/lib/navigation";
import { formatDue } from "@/lib/audit-format";
import { useDateFormatter, useFormatPrefs } from "@/lib/use-date-formatter";
import { userHasPermission } from "@/lib/permissions";
import {
fontSize,
Expand Down Expand Up @@ -417,18 +418,12 @@ const BookingCard = memo(function BookingCard({
}) {
const { colors, bookingStatusBadge } = useTheme();
const styles = useStyles();
const { formatDate } = useDateFormatter();
const badge = bookingStatusBadge[booking.status] ?? {
bg: colors.backgroundTertiary,
text: colors.muted,
};

const formatShortDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString(undefined, {
month: "short",
day: "numeric",
});
};

return (
<TouchableOpacity
style={styles.bookingCard}
Expand Down Expand Up @@ -457,7 +452,10 @@ const BookingCard = memo(function BookingCard({
color={colors.mutedLight}
/>
<Text style={styles.metaText}>
{formatShortDate(booking.from)} – {formatShortDate(booking.to)}
{/* Compact month/day range (no year) to keep the dashboard card
glanceable, still in the user's timezone + format order. */}
{formatDate(booking.from, { month: "short", day: "numeric" })} –{" "}
{formatDate(booking.to, { month: "short", day: "numeric" })}
</Text>
Comment thread
DonKoko marked this conversation as resolved.
</View>
<View style={styles.metaItem}>
Expand Down Expand Up @@ -545,6 +543,7 @@ const AuditCard = memo(function AuditCard({
}) {
const { colors, auditStatusBadge } = useTheme();
const styles = useStyles();
const prefs = useFormatPrefs();
const badge = auditStatusBadge[audit.status] ?? {
bg: colors.backgroundTertiary,
text: colors.muted,
Expand All @@ -561,7 +560,7 @@ const AuditCard = memo(function AuditCard({
// here vs "Due tomorrow" there). Home only ever shows active audits, but
// pass the real flag so the helper stays honest if that changes.
const isActive = audit.status === "PENDING" || audit.status === "ACTIVE";
const due = formatDue(audit.dueDate, isActive);
const due = formatDue(audit.dueDate, isActive, prefs);
const dueColor =
due.tier === "overdue"
? colors.error
Expand Down
Loading
Loading