tiemzone udpate on all pages that requrie timezone updating
This commit is contained in:
66
docs/00-SYSTEM/TIMEZONE.md
Normal file
66
docs/00-SYSTEM/TIMEZONE.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Timezone Standard
|
||||
|
||||
## Purpose
|
||||
This document defines the single-source timezone standard for IGNY8 and how all future time-related features must use it.
|
||||
|
||||
## Single Source of Truth
|
||||
- **Account timezone** is the canonical timezone for all user-visible times and scheduling UI.
|
||||
- The value is stored on the Account model as `account_timezone` (IANA name, e.g., `America/New_York`).
|
||||
- Selection modes:
|
||||
- **Country-derived**: timezone is derived from billing country.
|
||||
- **Manual**: user picks an IANA timezone that maps to a UTC offset list.
|
||||
|
||||
## Storage Rules
|
||||
- **Persist timestamps in UTC** in the database.
|
||||
- **Never store local times** without timezone context.
|
||||
- Store user selection in `Account.account_timezone` and (when needed) `timezone_mode` and `timezone_offset` for UI display.
|
||||
|
||||
## Display Rules (Frontend)
|
||||
- All UI formatting must use the account timezone.
|
||||
- Use shared helpers:
|
||||
- `getAccountTimezone()` for the active timezone.
|
||||
- `formatDate()`, `formatDateTime()`, `formatRelativeDate()` for consistent formatting.
|
||||
- **Do not** call `toLocaleDateString()` or `toLocaleTimeString()` without passing the account timezone.
|
||||
|
||||
## Scheduling Rules
|
||||
- All scheduling inputs in UI are **account timezone**.
|
||||
- Convert to UTC before sending to the backend.
|
||||
- All API payloads for scheduling must send ISO-8601 with timezone offset.
|
||||
- The backend stores scheduled datetimes in UTC.
|
||||
|
||||
## Backend API Contract
|
||||
- Endpoints that return timestamps should return UTC ISO strings.
|
||||
- Endpoints that return “server time” should return **account-local time** for display, plus the account timezone identifier.
|
||||
- If the account timezone is invalid or missing, fall back to `UTC`.
|
||||
|
||||
## Country List Source
|
||||
- Country list must be fetched from `/v1/auth/countries/`.
|
||||
- No hardcoded country lists in UI or backend responses.
|
||||
|
||||
## Implementation Checklist (New Features)
|
||||
1. **Input**: confirm user inputs are in account timezone.
|
||||
2. **Conversion**: convert to UTC before persistence or scheduling.
|
||||
3. **Storage**: store in UTC only.
|
||||
4. **Output**: format all timestamps with account timezone helpers.
|
||||
5. **API**: ensure responses include timezone-aware context when needed.
|
||||
|
||||
## Guardrails
|
||||
- Never introduce a second timezone source per user/site.
|
||||
- Do not mix server timezone with account timezone in UI.
|
||||
- Avoid timezone math in the UI; prefer helpers and backend-provided values when possible.
|
||||
|
||||
## Examples
|
||||
- **Display date in UI**:
|
||||
- Use `formatDateTime(timestamp)` to render in account timezone.
|
||||
- **Schedule content**:
|
||||
- User selects date/time in account timezone → convert to ISO → send to `/schedule/`.
|
||||
|
||||
## Troubleshooting
|
||||
- If times appear “off”:
|
||||
- Check account timezone is set.
|
||||
- Confirm UI uses helpers.
|
||||
- Confirm backend converts to UTC before save.
|
||||
|
||||
---
|
||||
Owner: Platform
|
||||
Last updated: 2026-01-19
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { formatDateTime } from '../../../utils/date';
|
||||
|
||||
interface StageOutput {
|
||||
stage: number;
|
||||
@@ -99,19 +100,7 @@ const MeaningfulRunHistory: React.FC<MeaningfulRunHistoryProps> = ({
|
||||
return `${minutes}m`;
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
const formatDate = (dateStr: string) => formatDateTime(dateStr);
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { automationService, RunHistoryItem } from '../../services/automationService';
|
||||
import ComponentCard from '../common/ComponentCard';
|
||||
import { formatDateTime } from '../../utils/date';
|
||||
|
||||
interface RunHistoryProps {
|
||||
siteId: number;
|
||||
@@ -98,11 +99,11 @@ const RunHistory: React.FC<RunHistoryProps> = ({ siteId }) => {
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 capitalize">{run.trigger_type || 'manual'}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||
{run.started_at ? new Date(run.started_at).toLocaleString() : '-'}
|
||||
{run.started_at ? formatDateTime(run.started_at) : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||
{run.completed_at
|
||||
? new Date(run.completed_at).toLocaleString()
|
||||
? formatDateTime(run.completed_at)
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">{run.total_credits_used || 0}</td>
|
||||
|
||||
@@ -6,6 +6,7 @@ import Input from "../form/input/InputField";
|
||||
import Checkbox from "../form/input/Checkbox";
|
||||
import Button from "../ui/button/Button";
|
||||
import { useAuthStore } from "../../store/authStore";
|
||||
import { formatDateTime } from "../../utils/date";
|
||||
|
||||
interface LogoutReason {
|
||||
code: string;
|
||||
@@ -158,7 +159,7 @@ export default function SignInForm() {
|
||||
<div className="space-y-1 text-xs font-mono text-warning-700 dark:text-warning-400">
|
||||
<div><span className="font-bold">Code:</span> {logoutReason.code}</div>
|
||||
<div><span className="font-bold">Source:</span> {logoutReason.source}</div>
|
||||
<div><span className="font-bold">Time:</span> {new Date(logoutReason.timestamp).toLocaleString()}</div>
|
||||
<div><span className="font-bold">Time:</span> {formatDateTime(logoutReason.timestamp)}</div>
|
||||
{logoutReason.context && Object.keys(logoutReason.context).length > 0 && (
|
||||
<div className="mt-2">
|
||||
<span className="font-bold">Context:</span>
|
||||
|
||||
@@ -3,6 +3,7 @@ import ComponentCard from '../../components/common/ComponentCard';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { getCreditTransactions, type CreditTransaction } from '../../services/billing.api';
|
||||
import { formatDateTime } from '../../utils/date';
|
||||
|
||||
type Variant = 'card' | 'plain';
|
||||
|
||||
@@ -57,7 +58,7 @@ export default function BillingRecentTransactions({ limit = 10, variant = 'card'
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{new Date(transaction.created_at).toLocaleString()}
|
||||
{formatDateTime(transaction.created_at)}
|
||||
{transaction.reference_id && ` • Ref: ${transaction.reference_id}`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useBillingStore } from '../../store/billingStore';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { CompactPagination } from '../ui/pagination';
|
||||
import { formatDateTime } from '../../utils/date';
|
||||
|
||||
// Credit costs per operation (copied from Billing usage page)
|
||||
const CREDIT_COSTS: Record<string, { cost: number | string; description: string }> = {
|
||||
@@ -85,7 +86,7 @@ export default function BillingUsagePanel({ showOnlyActivity = false }: BillingU
|
||||
<tbody>
|
||||
{paginated.map((txn) => (
|
||||
<tr key={txn.id} className="border-b border-gray-100 dark:border-gray-800">
|
||||
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">{new Date(txn.created_at).toLocaleString()}</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">{formatDateTime(txn.created_at)}</td>
|
||||
<td className="py-3 px-4">
|
||||
<Badge variant="soft" tone={txn.amount >= 0 ? 'success' : 'danger'}>{txn.transaction_type}</Badge>
|
||||
</td>
|
||||
@@ -232,7 +233,7 @@ export default function BillingUsagePanel({ showOnlyActivity = false }: BillingU
|
||||
<tbody>
|
||||
{paginated.map((txn) => (
|
||||
<tr key={txn.id} className="border-b border-gray-100 dark:border-gray-800">
|
||||
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">{new Date(txn.created_at).toLocaleString()}</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">{formatDateTime(txn.created_at)}</td>
|
||||
<td className="py-3 px-4">
|
||||
<Badge variant="light" color={txn.amount >= 0 ? 'success' : 'error'}>{txn.transaction_type}</Badge>
|
||||
</td>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAuthStore } from '../../store/authStore';
|
||||
import { API_BASE_URL } from '../../services/api';
|
||||
import Button from '../ui/button/Button';
|
||||
import { CheckCircleIcon, XCircleIcon, ClockIcon, RefreshCwIcon } from '../../icons';
|
||||
import { formatDateTime } from '../../utils/date';
|
||||
|
||||
interface Payment {
|
||||
id: number;
|
||||
@@ -73,15 +74,7 @@ export default function PaymentHistory() {
|
||||
return badges[status as keyof typeof badges] || badges.pending_approval;
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
const formatDate = (dateString: string) => formatDateTime(dateString);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Modal } from '../ui/modal';
|
||||
import Button from '../ui/button/Button';
|
||||
import { ErrorIcon, CalendarIcon, BoltIcon, ExternalLinkIcon } from '../../icons';
|
||||
import { formatDateTime } from '../../utils/date';
|
||||
|
||||
interface Content {
|
||||
id: number;
|
||||
@@ -42,15 +43,7 @@ const ErrorDetailsModal: React.FC<ErrorDetailsModalProps> = ({
|
||||
const formatDate = (isoString: string | null) => {
|
||||
if (!isoString) return 'N/A';
|
||||
try {
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
return formatDateTime(isoString);
|
||||
} catch (error) {
|
||||
return isoString;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import ComponentCard from '../common/ComponentCard';
|
||||
import Button from '../ui/button/Button';
|
||||
import Badge from '../ui/badge/Badge';
|
||||
import { CreditBalance, Subscription, Plan } from '../../services/billing.api';
|
||||
import { formatDate } from '../../utils/date';
|
||||
import {
|
||||
CalendarIcon,
|
||||
CreditCardIcon,
|
||||
@@ -29,18 +30,9 @@ interface AccountInfoWidgetProps {
|
||||
}
|
||||
|
||||
// Helper to format dates
|
||||
function formatDate(dateStr: string | undefined): string {
|
||||
function formatAccountDate(dateStr: string | undefined): string {
|
||||
if (!dateStr) return '—';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
} catch {
|
||||
return '—';
|
||||
}
|
||||
return formatDate(dateStr);
|
||||
}
|
||||
|
||||
// Helper to get days until date
|
||||
@@ -162,7 +154,7 @@ export default function AccountInfoWidget({
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{formatDate(periodEnd)}
|
||||
{formatAccountDate(periodEnd)}
|
||||
</p>
|
||||
{daysUntilReset !== null && daysUntilReset > 0 && (
|
||||
<p className="text-sm text-gray-400 dark:text-gray-500">
|
||||
@@ -180,7 +172,7 @@ export default function AccountInfoWidget({
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{formatDate(periodEnd)}
|
||||
{formatAccountDate(periodEnd)}
|
||||
</p>
|
||||
{currentPlan?.price && (
|
||||
<p className="text-sm text-gray-400 dark:text-gray-500">
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
AlertIcon,
|
||||
ClockIcon,
|
||||
} from '../../icons';
|
||||
import { formatDateTime } from '../../utils/date';
|
||||
|
||||
export interface AutomationData {
|
||||
status: 'active' | 'paused' | 'failed' | 'not_configured';
|
||||
@@ -61,16 +62,6 @@ const statusConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
function formatDateTime(date: Date): string {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
});
|
||||
}
|
||||
|
||||
export default function AutomationStatusWidget({ data, onRunNow, loading }: AutomationStatusWidgetProps) {
|
||||
const config = statusConfig[data.status];
|
||||
const StatusIcon = config.icon;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
AlertIcon,
|
||||
CheckCircleIcon,
|
||||
} from '../../icons';
|
||||
import { formatDate } from '../../utils/date';
|
||||
|
||||
export interface ActivityItem {
|
||||
id: string;
|
||||
@@ -68,7 +69,7 @@ function formatRelativeTime(date: Date): string {
|
||||
if (diffDays === 1) return 'Yesterday';
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
|
||||
return date.toLocaleDateString();
|
||||
return formatDate(date);
|
||||
}
|
||||
|
||||
export default function RecentActivityWidget({ activities, loading }: RecentActivityWidgetProps) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { CheckCircleIcon, XCircleIcon, ClockIcon, RefreshCwIcon } from '../../icons';
|
||||
import { formatDateTime } from '../../utils/date';
|
||||
|
||||
interface IntegrationStatusProps {
|
||||
syncEnabled: boolean;
|
||||
@@ -47,8 +48,7 @@ export default function IntegrationStatus({
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return 'Never';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
return formatDateTime(dateString);
|
||||
} catch {
|
||||
return 'Invalid Date';
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Card } from '../ui/card';
|
||||
import Button from '../ui/button/Button';
|
||||
import IconButton from '../ui/button/IconButton';
|
||||
import Badge from '../ui/badge/Badge';
|
||||
import { formatDate } from '../../utils/date';
|
||||
|
||||
interface WordPressIntegration {
|
||||
id: number;
|
||||
@@ -122,7 +123,7 @@ export default function WordPressIntegrationCard({
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Last Sync</p>
|
||||
<p className="text-sm text-gray-900 dark:text-white">
|
||||
{integration.last_sync_at
|
||||
? new Date(integration.last_sync_at).toLocaleDateString()
|
||||
? formatDate(integration.last_sync_at)
|
||||
: 'Never'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import React, { ReactNode } from 'react';
|
||||
import { EnhancedTooltip } from './EnhancedTooltip';
|
||||
import { formatDateTime } from '../../../utils/date';
|
||||
|
||||
interface CalendarItemTooltipProps {
|
||||
children: ReactNode;
|
||||
@@ -63,7 +64,7 @@ export const CalendarItemTooltip: React.FC<CalendarItemTooltipProps> = ({
|
||||
{contentType && <div>Type: {contentType}</div>}
|
||||
{date && (
|
||||
<div>
|
||||
{dateLabel}: {new Date(date).toLocaleString()}
|
||||
{dateLabel}: {formatDateTime(date)}
|
||||
</div>
|
||||
)}
|
||||
{wordCount !== undefined && <div>Words: {wordCount}</div>}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { useSiteStore } from '../../store/siteStore';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { API_BASE_URL, fetchAPI } from '../../services/api';
|
||||
import { formatTime } from '../../utils/date';
|
||||
|
||||
// Types for WordPress integration debugging
|
||||
interface IntegrationHealth {
|
||||
@@ -498,7 +499,7 @@ export default function WordPressIntegrationDebug() {
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">{integrationHealth.api_message}</p>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Last: {new Date(integrationHealth.last_api_check).toLocaleTimeString()}
|
||||
Last: {formatTime(integrationHealth.last_api_check)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -533,7 +534,7 @@ export default function WordPressIntegrationDebug() {
|
||||
</p>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Last: {integrationHealth.last_sync ?
|
||||
new Date(integrationHealth.last_sync).toLocaleTimeString() : 'Never'}
|
||||
formatTime(integrationHealth.last_sync) : 'Never'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -579,7 +580,7 @@ export default function WordPressIntegrationDebug() {
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 whitespace-nowrap ml-4">
|
||||
{new Date(event.timestamp).toLocaleTimeString()}
|
||||
{formatTime(event.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useNotificationStore } from '../../store/notificationStore';
|
||||
import { usePageLoading } from '../../context/PageLoadingContext';
|
||||
import type { NotificationAPI } from '../../services/notifications.api';
|
||||
import { deleteNotification as deleteNotificationAPI } from '../../services/notifications.api';
|
||||
import { formatDateTime } from '../../utils/date';
|
||||
|
||||
interface FilterState {
|
||||
severity: string;
|
||||
@@ -101,13 +102,7 @@ export default function NotificationsPage() {
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 7) {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
});
|
||||
return formatDateTime(date);
|
||||
} else if (days > 0) {
|
||||
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
||||
} else if (hours > 0) {
|
||||
|
||||
@@ -142,3 +142,23 @@ export function formatDateTime(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in account timezone
|
||||
* @param dateString - ISO date string or Date object
|
||||
* @param options - Intl.DateTimeFormat options override
|
||||
*/
|
||||
export function formatTime(
|
||||
dateString: string | Date | null | undefined,
|
||||
options: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit', second: '2-digit' }
|
||||
): string {
|
||||
if (!dateString) return '-';
|
||||
|
||||
const date = typeof dateString === 'string' ? new Date(dateString) : dateString;
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
...options,
|
||||
timeZone: getAccountTimezone(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user