Skip to content

Commit 13c48e7

Browse files
authored
Merge pull request #125 from Resgrid/develop
RG-T132 Fixed Weather alerts info, fixed timezone issue
2 parents 7de9c6f + 58774ab commit 13c48e7

6 files changed

Lines changed: 88 additions & 25 deletions

File tree

src/app/(app)/weather-alert-detail.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,15 @@ export default function WeatherAlertDetailScreen() {
109109
<VStack space="xs">
110110
<HStack className="items-center justify-between">
111111
<Text className={`text-sm ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>{t('weatherAlerts.detail.effective')}</Text>
112-
<Text className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{formatDateTime(alert.EffectiveUtc)}</Text>
112+
<Text className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{formatDateTime(alert.EffectiveOnUtc || alert.EffectiveUtc)}</Text>
113113
</HStack>
114114
<HStack className="items-center justify-between">
115115
<Text className={`text-sm ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>{t('weatherAlerts.detail.onset')}</Text>
116-
<Text className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{formatDateTime(alert.OnsetUtc)}</Text>
116+
<Text className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{formatDateTime(alert.OnsetOnUtc || alert.OnsetUtc)}</Text>
117117
</HStack>
118118
<HStack className="items-center justify-between">
119119
<Text className={`text-sm ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>{t('weatherAlerts.detail.expires')}</Text>
120-
<Text className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{formatDateTime(alert.ExpiresUtc)}</Text>
120+
<Text className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{formatDateTime(alert.ExpiresOnUtc || alert.ExpiresUtc)}</Text>
121121
</HStack>
122122
</VStack>
123123
</VStack>
@@ -168,10 +168,10 @@ export default function WeatherAlertDetailScreen() {
168168
<Text className={`text-sm ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>{t('weatherAlerts.detail.certainty')}</Text>
169169
<Text className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{t(CERTAINTY_KEYS[alert.Certainty] || 'weatherAlerts.certainty.unknown')}</Text>
170170
</HStack>
171-
{alert.SenderName ? (
171+
{alert.Sender ? (
172172
<HStack className="items-center justify-between">
173173
<Text className={`text-sm ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>{t('weatherAlerts.detail.sender')}</Text>
174-
<Text className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{alert.SenderName}</Text>
174+
<Text className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-900'}`}>{alert.Sender}</Text>
175175
</HStack>
176176
) : null}
177177
</VStack>

src/app/(app)/weather-alerts.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,17 @@ export default function WeatherAlertsScreen() {
7676

7777
switch (sort) {
7878
case 'expires':
79-
result.sort((a, b) => new Date(a.ExpiresUtc).getTime() - new Date(b.ExpiresUtc).getTime());
79+
result.sort((a, b) => new Date(a.ExpiresOnUtc || a.ExpiresUtc).getTime() - new Date(b.ExpiresOnUtc || b.ExpiresUtc).getTime());
8080
break;
8181
case 'newest':
82-
result.sort((a, b) => new Date(b.EffectiveUtc).getTime() - new Date(a.EffectiveUtc).getTime());
82+
result.sort((a, b) => new Date(b.EffectiveOnUtc || b.EffectiveUtc).getTime() - new Date(a.EffectiveOnUtc || a.EffectiveUtc).getTime());
8383
break;
8484
case 'severity':
8585
default:
8686
result.sort((a, b) => {
8787
// Core enum: Extreme=0 is most severe, so ascending puts the worst alerts first.
8888
if (a.Severity !== b.Severity) return a.Severity - b.Severity;
89-
return new Date(b.EffectiveUtc).getTime() - new Date(a.EffectiveUtc).getTime();
89+
return new Date(b.EffectiveOnUtc || b.EffectiveUtc).getTime() - new Date(a.EffectiveOnUtc || a.EffectiveUtc).getTime();
9090
});
9191
break;
9292
}
@@ -129,9 +129,9 @@ export default function WeatherAlertsScreen() {
129129
</Text>
130130
<HStack className="items-center justify-between">
131131
<Text className={`text-xs ${isDark ? 'text-gray-500' : 'text-gray-500'}`}>
132-
{translate('weatherAlerts.detail.effective')}: {formatDateTime(item.EffectiveUtc)}
132+
{translate('weatherAlerts.detail.effective')}: {formatDateTime(item.EffectiveOnUtc || item.EffectiveUtc)}
133133
</Text>
134-
<Text className={`text-xs font-medium ${isDark ? 'text-amber-400' : 'text-amber-600'}`}>{formatExpiry(item.ExpiresUtc)}</Text>
134+
<Text className={`text-xs font-medium ${isDark ? 'text-amber-400' : 'text-amber-600'}`}>{formatExpiry(item.ExpiresOnUtc || item.ExpiresUtc)}</Text>
135135
</HStack>
136136
</VStack>
137137
</HStack>

src/components/widgets/WeatherAlertsWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ const AlertCard: React.FC<AlertCardProps> = ({ alert, isDark, showHeadline, show
112112
) : null}
113113
{showExpiry ? (
114114
<Text className={`${isDark ? 'text-gray-500' : 'text-gray-500'}`} style={{ fontSize: fontSize - 2 }}>
115-
{formatExpiry(alert.ExpiresUtc)}
115+
{formatExpiry(alert.ExpiresOnUtc || alert.ExpiresUtc)}
116116
</Text>
117117
) : null}
118118
</VStack>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { getTimeAgoUtc } from '../utils';
2+
3+
describe('getTimeAgoUtc', () => {
4+
beforeEach(() => {
5+
jest.useFakeTimers();
6+
jest.setSystemTime(new Date('2026-08-19T20:00:00Z'));
7+
});
8+
9+
afterEach(() => {
10+
jest.useRealTimers();
11+
});
12+
13+
it('reads a Z-stamped UTC string without applying the legacy offset shift', () => {
14+
expect(getTimeAgoUtc('2026-08-19T19:55:00.000Z')).toBe('5 minutes ago');
15+
});
16+
17+
it('reads an offset-stamped string without applying the legacy offset shift', () => {
18+
expect(getTimeAgoUtc('2026-08-19T12:55:00-07:00')).toBe('5 minutes ago');
19+
});
20+
21+
it('still reads legacy zone-less UTC strings via the offset-shifted comparison', () => {
22+
// Zone-less strings parse as device-local; the function compensates by shifting "now"
23+
// by the same offset, so the result is correct in any device timezone.
24+
const zoneless = new Date(Date.now() - 5 * 60 * 1000).toISOString().replace(/\.\d{3}Z$/, '');
25+
expect(getTimeAgoUtc(zoneless)).toBe('5 minutes ago');
26+
});
27+
28+
it('treats Date objects as exact instants', () => {
29+
expect(getTimeAgoUtc(new Date(Date.now() - 5 * 60 * 1000))).toBe('5 minutes ago');
30+
});
31+
32+
it('treats epoch milliseconds as exact instants', () => {
33+
expect(getTimeAgoUtc(Date.now() - 5 * 60 * 1000)).toBe('5 minutes ago');
34+
});
35+
36+
it('returns Unknown for empty input', () => {
37+
expect(getTimeAgoUtc('')).toBe('Unknown');
38+
});
39+
});

src/lib/utils.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,18 +431,29 @@ export function getTimeAgoUtc(time: any): string {
431431
return 'Unknown';
432432
}
433433

434+
// Legacy API values are zone-less UTC strings that new Date() parses as device-local time;
435+
// those need "now" shifted by the same offset below. Strings carrying an explicit Z/±hh:mm
436+
// designator parse correctly and must not be shifted.
437+
let hasZone = false;
438+
434439
switch (typeof time) {
435440
case 'number':
441+
// Epoch milliseconds are an exact instant — never shift.
442+
hasZone = true;
436443
break;
437444
case 'string':
445+
hasZone = /(Z|[+-]\d{2}:?\d{2})$/i.test(time.trim());
438446
time = +new Date(time);
439447
break;
440448
case 'object':
441-
if (time.constructor === Date) {
449+
if (time instanceof Date) {
450+
// A Date is an exact instant — never shift.
451+
hasZone = true;
442452
time = time.getTime();
443453
}
444454
break;
445455
default:
456+
hasZone = true;
446457
time = +new Date();
447458
}
448459

@@ -466,7 +477,8 @@ export function getTimeAgoUtc(time: any): string {
466477
[5806080000, 'Last century', 'Next century'], // 60*60*24*7*4*12*100*2
467478
[58060800000, 'centuries', 2903040000], // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100
468479
];
469-
let seconds = (Number(new Date(currentDate).getTime() + new Date(currentDate).getTimezoneOffset() * 60 * 1000) - time) / 1000,
480+
const nowMs = hasZone ? currentDate.getTime() : currentDate.getTime() + currentDate.getTimezoneOffset() * 60 * 1000;
481+
let seconds = (nowMs - time) / 1000,
470482
token = 'ago',
471483
listChoice = 1;
472484

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
1+
// Mirrors Resgrid Core's v4 WeatherAlertResultData (Web/Resgrid.Web.Services/Models/v4/WeatherAlerts).
2+
// Enum-valued fields are ints (see WeatherAlertSeverity/Category/Urgency/Certainty/Status in Core).
13
export class WeatherAlertResultData {
24
public WeatherAlertId: string = '';
3-
public DepartmentId: string = '';
5+
public DepartmentId: number = 0;
6+
public WeatherAlertSourceId: string = '';
7+
public ExternalId: string = '';
8+
public Sender: string = '';
49
public Event: string = '';
5-
public Severity: number = 0;
10+
public AlertCategory: number = 4; // Other
11+
public Severity: number = 4; // Unknown
12+
public Urgency: number = 4; // Unknown
13+
public Certainty: number = 4; // Unknown
14+
public Status: number = 0; // Active
615
public Headline: string = '';
716
public Description: string = '';
817
public Instruction: string = '';
918
public AreaDescription: string = '';
1019
public Polygon: string = '';
20+
public Geocodes: string = '';
1121
public CenterGeoLocation: string = '';
22+
// Department-local display strings (despite the Utc names) — render verbatim only.
1223
public OnsetUtc: string = '';
1324
public ExpiresUtc: string = '';
1425
public EffectiveUtc: string = '';
15-
public Status: number = 0;
16-
public AlertCategory: number = 0;
17-
public Urgency: number = 0;
18-
public Certainty: number = 0;
19-
public Sender: string = '';
20-
public SenderName: string = '';
21-
public SourceType: number = 0;
22-
public ExternalId: string = '';
23-
public References: string = '';
24-
public Web: string = '';
26+
public SentUtc: string = '';
27+
public FirstSeenUtc: string = '';
28+
public LastUpdatedUtc: string = '';
29+
// Real UTC instants with explicit "Z" — use these for any date math. Empty on older servers.
30+
public EffectiveOnUtc?: string = '';
31+
public OnsetOnUtc?: string = '';
32+
public ExpiresOnUtc?: string = '';
33+
public SentOnUtc?: string = '';
34+
public ReferencesExternalId: string = '';
35+
public NotificationSent: boolean = false;
36+
public SystemMessageId?: number;
2537
}

0 commit comments

Comments
 (0)