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 React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { formatDateTime } from '../../../utils/date';
|
||||||
|
|
||||||
interface StageOutput {
|
interface StageOutput {
|
||||||
stage: number;
|
stage: number;
|
||||||
@@ -99,19 +100,7 @@ const MeaningfulRunHistory: React.FC<MeaningfulRunHistoryProps> = ({
|
|||||||
return `${minutes}m`;
|
return `${minutes}m`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateStr: string) => {
|
const formatDate = (dateStr: string) => formatDateTime(dateStr);
|
||||||
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 getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { automationService, RunHistoryItem } from '../../services/automationService';
|
import { automationService, RunHistoryItem } from '../../services/automationService';
|
||||||
import ComponentCard from '../common/ComponentCard';
|
import ComponentCard from '../common/ComponentCard';
|
||||||
|
import { formatDateTime } from '../../utils/date';
|
||||||
|
|
||||||
interface RunHistoryProps {
|
interface RunHistoryProps {
|
||||||
siteId: number;
|
siteId: number;
|
||||||
@@ -98,11 +99,11 @@ const RunHistory: React.FC<RunHistoryProps> = ({ siteId }) => {
|
|||||||
</td>
|
</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 capitalize">{run.trigger_type || 'manual'}</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
<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>
|
||||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
|
||||||
{run.completed_at
|
{run.completed_at
|
||||||
? new Date(run.completed_at).toLocaleString()
|
? formatDateTime(run.completed_at)
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">{run.total_credits_used || 0}</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 Checkbox from "../form/input/Checkbox";
|
||||||
import Button from "../ui/button/Button";
|
import Button from "../ui/button/Button";
|
||||||
import { useAuthStore } from "../../store/authStore";
|
import { useAuthStore } from "../../store/authStore";
|
||||||
|
import { formatDateTime } from "../../utils/date";
|
||||||
|
|
||||||
interface LogoutReason {
|
interface LogoutReason {
|
||||||
code: string;
|
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 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">Code:</span> {logoutReason.code}</div>
|
||||||
<div><span className="font-bold">Source:</span> {logoutReason.source}</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 && (
|
{logoutReason.context && Object.keys(logoutReason.context).length > 0 && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<span className="font-bold">Context:</span>
|
<span className="font-bold">Context:</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ComponentCard from '../../components/common/ComponentCard';
|
|||||||
import Badge from '../../components/ui/badge/Badge';
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { getCreditTransactions, type CreditTransaction } from '../../services/billing.api';
|
import { getCreditTransactions, type CreditTransaction } from '../../services/billing.api';
|
||||||
|
import { formatDateTime } from '../../utils/date';
|
||||||
|
|
||||||
type Variant = 'card' | 'plain';
|
type Variant = 'card' | 'plain';
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ export default function BillingRecentTransactions({ limit = 10, variant = 'card'
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
<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}`}
|
{transaction.reference_id && ` • Ref: ${transaction.reference_id}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useBillingStore } from '../../store/billingStore';
|
|||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Badge from '../../components/ui/badge/Badge';
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
import { CompactPagination } from '../ui/pagination';
|
import { CompactPagination } from '../ui/pagination';
|
||||||
|
import { formatDateTime } from '../../utils/date';
|
||||||
|
|
||||||
// Credit costs per operation (copied from Billing usage page)
|
// Credit costs per operation (copied from Billing usage page)
|
||||||
const CREDIT_COSTS: Record<string, { cost: number | string; description: string }> = {
|
const CREDIT_COSTS: Record<string, { cost: number | string; description: string }> = {
|
||||||
@@ -85,7 +86,7 @@ export default function BillingUsagePanel({ showOnlyActivity = false }: BillingU
|
|||||||
<tbody>
|
<tbody>
|
||||||
{paginated.map((txn) => (
|
{paginated.map((txn) => (
|
||||||
<tr key={txn.id} className="border-b border-gray-100 dark:border-gray-800">
|
<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">
|
<td className="py-3 px-4">
|
||||||
<Badge variant="soft" tone={txn.amount >= 0 ? 'success' : 'danger'}>{txn.transaction_type}</Badge>
|
<Badge variant="soft" tone={txn.amount >= 0 ? 'success' : 'danger'}>{txn.transaction_type}</Badge>
|
||||||
</td>
|
</td>
|
||||||
@@ -232,7 +233,7 @@ export default function BillingUsagePanel({ showOnlyActivity = false }: BillingU
|
|||||||
<tbody>
|
<tbody>
|
||||||
{paginated.map((txn) => (
|
{paginated.map((txn) => (
|
||||||
<tr key={txn.id} className="border-b border-gray-100 dark:border-gray-800">
|
<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">
|
<td className="py-3 px-4">
|
||||||
<Badge variant="light" color={txn.amount >= 0 ? 'success' : 'error'}>{txn.transaction_type}</Badge>
|
<Badge variant="light" color={txn.amount >= 0 ? 'success' : 'error'}>{txn.transaction_type}</Badge>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useAuthStore } from '../../store/authStore';
|
|||||||
import { API_BASE_URL } from '../../services/api';
|
import { API_BASE_URL } from '../../services/api';
|
||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
import { CheckCircleIcon, XCircleIcon, ClockIcon, RefreshCwIcon } from '../../icons';
|
import { CheckCircleIcon, XCircleIcon, ClockIcon, RefreshCwIcon } from '../../icons';
|
||||||
|
import { formatDateTime } from '../../utils/date';
|
||||||
|
|
||||||
interface Payment {
|
interface Payment {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -73,15 +74,7 @@ export default function PaymentHistory() {
|
|||||||
return badges[status as keyof typeof badges] || badges.pending_approval;
|
return badges[status as keyof typeof badges] || badges.pending_approval;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => formatDateTime(dateString);
|
||||||
return new Date(dateString).toLocaleDateString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { Modal } from '../ui/modal';
|
import { Modal } from '../ui/modal';
|
||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
import { ErrorIcon, CalendarIcon, BoltIcon, ExternalLinkIcon } from '../../icons';
|
import { ErrorIcon, CalendarIcon, BoltIcon, ExternalLinkIcon } from '../../icons';
|
||||||
|
import { formatDateTime } from '../../utils/date';
|
||||||
|
|
||||||
interface Content {
|
interface Content {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -42,15 +43,7 @@ const ErrorDetailsModal: React.FC<ErrorDetailsModalProps> = ({
|
|||||||
const formatDate = (isoString: string | null) => {
|
const formatDate = (isoString: string | null) => {
|
||||||
if (!isoString) return 'N/A';
|
if (!isoString) return 'N/A';
|
||||||
try {
|
try {
|
||||||
const date = new Date(isoString);
|
return formatDateTime(isoString);
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return isoString;
|
return isoString;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import ComponentCard from '../common/ComponentCard';
|
|||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
import Badge from '../ui/badge/Badge';
|
import Badge from '../ui/badge/Badge';
|
||||||
import { CreditBalance, Subscription, Plan } from '../../services/billing.api';
|
import { CreditBalance, Subscription, Plan } from '../../services/billing.api';
|
||||||
|
import { formatDate } from '../../utils/date';
|
||||||
import {
|
import {
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
CreditCardIcon,
|
CreditCardIcon,
|
||||||
@@ -29,18 +30,9 @@ interface AccountInfoWidgetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to format dates
|
// Helper to format dates
|
||||||
function formatDate(dateStr: string | undefined): string {
|
function formatAccountDate(dateStr: string | undefined): string {
|
||||||
if (!dateStr) return '—';
|
if (!dateStr) return '—';
|
||||||
try {
|
return formatDate(dateStr);
|
||||||
const date = new Date(dateStr);
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to get days until date
|
// Helper to get days until date
|
||||||
@@ -162,7 +154,7 @@ export default function AccountInfoWidget({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-base font-semibold text-gray-900 dark:text-white">
|
<p className="text-base font-semibold text-gray-900 dark:text-white">
|
||||||
{formatDate(periodEnd)}
|
{formatAccountDate(periodEnd)}
|
||||||
</p>
|
</p>
|
||||||
{daysUntilReset !== null && daysUntilReset > 0 && (
|
{daysUntilReset !== null && daysUntilReset > 0 && (
|
||||||
<p className="text-sm text-gray-400 dark:text-gray-500">
|
<p className="text-sm text-gray-400 dark:text-gray-500">
|
||||||
@@ -180,7 +172,7 @@ export default function AccountInfoWidget({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-base font-semibold text-gray-900 dark:text-white">
|
<p className="text-base font-semibold text-gray-900 dark:text-white">
|
||||||
{formatDate(periodEnd)}
|
{formatAccountDate(periodEnd)}
|
||||||
</p>
|
</p>
|
||||||
{currentPlan?.price && (
|
{currentPlan?.price && (
|
||||||
<p className="text-sm text-gray-400 dark:text-gray-500">
|
<p className="text-sm text-gray-400 dark:text-gray-500">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
AlertIcon,
|
AlertIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
} from '../../icons';
|
} from '../../icons';
|
||||||
|
import { formatDateTime } from '../../utils/date';
|
||||||
|
|
||||||
export interface AutomationData {
|
export interface AutomationData {
|
||||||
status: 'active' | 'paused' | 'failed' | 'not_configured';
|
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) {
|
export default function AutomationStatusWidget({ data, onRunNow, loading }: AutomationStatusWidgetProps) {
|
||||||
const config = statusConfig[data.status];
|
const config = statusConfig[data.status];
|
||||||
const StatusIcon = config.icon;
|
const StatusIcon = config.icon;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
AlertIcon,
|
AlertIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
} from '../../icons';
|
} from '../../icons';
|
||||||
|
import { formatDate } from '../../utils/date';
|
||||||
|
|
||||||
export interface ActivityItem {
|
export interface ActivityItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -68,7 +69,7 @@ function formatRelativeTime(date: Date): string {
|
|||||||
if (diffDays === 1) return 'Yesterday';
|
if (diffDays === 1) return 'Yesterday';
|
||||||
if (diffDays < 7) return `${diffDays}d ago`;
|
if (diffDays < 7) return `${diffDays}d ago`;
|
||||||
|
|
||||||
return date.toLocaleDateString();
|
return formatDate(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RecentActivityWidget({ activities, loading }: RecentActivityWidgetProps) {
|
export default function RecentActivityWidget({ activities, loading }: RecentActivityWidgetProps) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CheckCircleIcon, XCircleIcon, ClockIcon, RefreshCwIcon } from '../../icons';
|
import { CheckCircleIcon, XCircleIcon, ClockIcon, RefreshCwIcon } from '../../icons';
|
||||||
|
import { formatDateTime } from '../../utils/date';
|
||||||
|
|
||||||
interface IntegrationStatusProps {
|
interface IntegrationStatusProps {
|
||||||
syncEnabled: boolean;
|
syncEnabled: boolean;
|
||||||
@@ -47,8 +48,7 @@ export default function IntegrationStatus({
|
|||||||
const formatDate = (dateString: string | null) => {
|
const formatDate = (dateString: string | null) => {
|
||||||
if (!dateString) return 'Never';
|
if (!dateString) return 'Never';
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
return formatDateTime(dateString);
|
||||||
return date.toLocaleString();
|
|
||||||
} catch {
|
} catch {
|
||||||
return 'Invalid Date';
|
return 'Invalid Date';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Card } from '../ui/card';
|
|||||||
import Button from '../ui/button/Button';
|
import Button from '../ui/button/Button';
|
||||||
import IconButton from '../ui/button/IconButton';
|
import IconButton from '../ui/button/IconButton';
|
||||||
import Badge from '../ui/badge/Badge';
|
import Badge from '../ui/badge/Badge';
|
||||||
|
import { formatDate } from '../../utils/date';
|
||||||
|
|
||||||
interface WordPressIntegration {
|
interface WordPressIntegration {
|
||||||
id: number;
|
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-xs text-gray-500 dark:text-gray-400 mb-1">Last Sync</p>
|
||||||
<p className="text-sm text-gray-900 dark:text-white">
|
<p className="text-sm text-gray-900 dark:text-white">
|
||||||
{integration.last_sync_at
|
{integration.last_sync_at
|
||||||
? new Date(integration.last_sync_at).toLocaleDateString()
|
? formatDate(integration.last_sync_at)
|
||||||
: 'Never'}
|
: 'Never'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { EnhancedTooltip } from './EnhancedTooltip';
|
import { EnhancedTooltip } from './EnhancedTooltip';
|
||||||
|
import { formatDateTime } from '../../../utils/date';
|
||||||
|
|
||||||
interface CalendarItemTooltipProps {
|
interface CalendarItemTooltipProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -63,7 +64,7 @@ export const CalendarItemTooltip: React.FC<CalendarItemTooltipProps> = ({
|
|||||||
{contentType && <div>Type: {contentType}</div>}
|
{contentType && <div>Type: {contentType}</div>}
|
||||||
{date && (
|
{date && (
|
||||||
<div>
|
<div>
|
||||||
{dateLabel}: {new Date(date).toLocaleString()}
|
{dateLabel}: {formatDateTime(date)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{wordCount !== undefined && <div>Words: {wordCount}</div>}
|
{wordCount !== undefined && <div>Words: {wordCount}</div>}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { useSiteStore } from '../../store/siteStore';
|
import { useSiteStore } from '../../store/siteStore';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { API_BASE_URL, fetchAPI } from '../../services/api';
|
import { API_BASE_URL, fetchAPI } from '../../services/api';
|
||||||
|
import { formatTime } from '../../utils/date';
|
||||||
|
|
||||||
// Types for WordPress integration debugging
|
// Types for WordPress integration debugging
|
||||||
interface IntegrationHealth {
|
interface IntegrationHealth {
|
||||||
@@ -498,7 +499,7 @@ export default function WordPressIntegrationDebug() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-600 dark:text-gray-400">{integrationHealth.api_message}</p>
|
<p className="text-xs text-gray-600 dark:text-gray-400">{integrationHealth.api_message}</p>
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -533,7 +534,7 @@ export default function WordPressIntegrationDebug() {
|
|||||||
</p>
|
</p>
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
Last: {integrationHealth.last_sync ?
|
Last: {integrationHealth.last_sync ?
|
||||||
new Date(integrationHealth.last_sync).toLocaleTimeString() : 'Never'}
|
formatTime(integrationHealth.last_sync) : 'Never'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -579,7 +580,7 @@ export default function WordPressIntegrationDebug() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 whitespace-nowrap ml-4">
|
<div className="text-xs text-gray-500 whitespace-nowrap ml-4">
|
||||||
{new Date(event.timestamp).toLocaleTimeString()}
|
{formatTime(event.timestamp)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { useNotificationStore } from '../../store/notificationStore';
|
|||||||
import { usePageLoading } from '../../context/PageLoadingContext';
|
import { usePageLoading } from '../../context/PageLoadingContext';
|
||||||
import type { NotificationAPI } from '../../services/notifications.api';
|
import type { NotificationAPI } from '../../services/notifications.api';
|
||||||
import { deleteNotification as deleteNotificationAPI } from '../../services/notifications.api';
|
import { deleteNotification as deleteNotificationAPI } from '../../services/notifications.api';
|
||||||
|
import { formatDateTime } from '../../utils/date';
|
||||||
|
|
||||||
interface FilterState {
|
interface FilterState {
|
||||||
severity: string;
|
severity: string;
|
||||||
@@ -101,13 +102,7 @@ export default function NotificationsPage() {
|
|||||||
const days = Math.floor(hours / 24);
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
if (days > 7) {
|
if (days > 7) {
|
||||||
return date.toLocaleDateString('en-US', {
|
return formatDateTime(date);
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
} else if (days > 0) {
|
} else if (days > 0) {
|
||||||
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
||||||
} else if (hours > 0) {
|
} 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