mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-25 12:01:04 +02:00
chore: refactor format date in UI
This commit is contained in:
parent
eae30b3b21
commit
9045dbca72
19 changed files with 155 additions and 98 deletions
|
|
@ -21,12 +21,15 @@ import { Input } from '@/components/ui/input';
|
|||
import { Label } from '@/components/ui/label';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useAppConfig } from '@/context/AppConfigContext';
|
||||
import { useOrganizationTimezone } from '@/hooks/useOrganizationTimezone';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { formatDateTime } from '@/lib/dateTime';
|
||||
import logger from '@/lib/logger';
|
||||
|
||||
export default function APIKeysPage() {
|
||||
const { user, getAccessToken, redirectToLogin, loading } = useAuth();
|
||||
const { config } = useAppConfig();
|
||||
const organizationTimezone = useOrganizationTimezone();
|
||||
const isOSS = config?.deploymentMode === 'oss';
|
||||
|
||||
logger.debug('[APIKeysPage] Component render', {
|
||||
|
|
@ -292,13 +295,7 @@ export default function APIKeysPage() {
|
|||
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return 'Never';
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
return formatDateTime(dateString, organizationTimezone);
|
||||
};
|
||||
|
||||
// Don't render content until auth is loaded
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ import {
|
|||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useAppConfig } from "@/context/AppConfigContext";
|
||||
import { useOrganizationTimezone } from "@/hooks/useOrganizationTimezone";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { formatDateTime } from "@/lib/dateTime";
|
||||
|
||||
const LEDGER_PAGE_SIZE = 50;
|
||||
|
||||
|
|
@ -52,16 +54,6 @@ const formatAmount = (amountMinor?: number | null, currency?: string | null) =>
|
|||
}).format(amountMinor / 100);
|
||||
};
|
||||
|
||||
const formatDate = (value: string) => (
|
||||
new Date(value).toLocaleString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
|
||||
const metricLabels: Record<string, string> = {
|
||||
voice_minutes: "Voice usage",
|
||||
platform_usage: "Platform usage",
|
||||
|
|
@ -117,6 +109,7 @@ export default function BillingPage() {
|
|||
const searchParams = useSearchParams();
|
||||
const auth = useAuth();
|
||||
const { config, loading: configLoading } = useAppConfig();
|
||||
const organizationTimezone = useOrganizationTimezone();
|
||||
const [credits, setCredits] = useState<MpsBillingCreditsResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
|
@ -353,7 +346,9 @@ export default function BillingPage() {
|
|||
const billableQuantity = formatBillableQuantity(entry);
|
||||
return (
|
||||
<TableRow key={entry.id}>
|
||||
<TableCell>{formatDate(entry.created_at)}</TableCell>
|
||||
<TableCell>
|
||||
{formatDateTime(entry.created_at, organizationTimezone)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">{getLedgerEntryLabel(entry)}</span>
|
||||
|
|
|
|||
|
|
@ -27,10 +27,13 @@ import { Label } from '@/components/ui/label';
|
|||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { CampaignRuns } from '@/components/workflow-runs';
|
||||
import { useOrganizationTimezone } from '@/hooks/useOrganizationTimezone';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { formatDate, formatDateTime } from '@/lib/dateTime';
|
||||
|
||||
export default function CampaignDetailPage() {
|
||||
const { user, getAccessToken, redirectToLogin, loading } = useAuth();
|
||||
const organizationTimezone = useOrganizationTimezone();
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
|
|
@ -352,15 +355,6 @@ export default function CampaignDetailPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// Format date for display
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
// Get badge variant for state
|
||||
const getStateBadgeVariant = (state: string) => {
|
||||
switch (state) {
|
||||
|
|
@ -410,7 +404,7 @@ export default function CampaignDetailPage() {
|
|||
const formatLogTimestamp = (ts: string) => {
|
||||
const d = new Date(ts);
|
||||
if (isNaN(d.getTime())) return ts;
|
||||
return d.toLocaleString();
|
||||
return formatDateTime(d, organizationTimezone);
|
||||
};
|
||||
|
||||
// Render action button based on state
|
||||
|
|
@ -508,7 +502,7 @@ export default function CampaignDetailPage() {
|
|||
{campaign.state}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">
|
||||
Created {formatDate(campaign.created_at)}
|
||||
Created {formatDate(campaign.created_at, organizationTimezone)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -700,13 +694,17 @@ export default function CampaignDetailPage() {
|
|||
{campaign.started_at && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium">Started At</dt>
|
||||
<dd className="mt-1">{formatDateTime(campaign.started_at)}</dd>
|
||||
<dd className="mt-1">
|
||||
{formatDateTime(campaign.started_at, organizationTimezone)}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{campaign.completed_at && (
|
||||
<div>
|
||||
<dt className="text-sm font-medium">Completed At</dt>
|
||||
<dd className="mt-1">{formatDateTime(campaign.completed_at)}</dd>
|
||||
<dd className="mt-1">
|
||||
{formatDateTime(campaign.completed_at, organizationTimezone)}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
|
|
|
|||
|
|
@ -17,10 +17,13 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useOrganizationTimezone } from '@/hooks/useOrganizationTimezone';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { formatDate } from '@/lib/dateTime';
|
||||
|
||||
export default function CampaignsPage() {
|
||||
const { user, getAccessToken, redirectToLogin, loading } = useAuth();
|
||||
const organizationTimezone = useOrganizationTimezone();
|
||||
const router = useRouter();
|
||||
|
||||
const [campaignsData, setCampaignsData] = useState<CampaignsResponse | null>(null);
|
||||
|
|
@ -72,10 +75,6 @@ export default function CampaignsPage() {
|
|||
router.push('/campaigns/new');
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
};
|
||||
|
||||
const getStateBadgeVariant = (state: string) => {
|
||||
switch (state) {
|
||||
case 'created':
|
||||
|
|
@ -152,7 +151,9 @@ export default function CampaignsPage() {
|
|||
<TableCell>
|
||||
{campaign.executed_count} / {campaign.total_queued_count}
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(campaign.created_at)}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate(campaign.created_at, organizationTimezone)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { Badge } from '@/components/ui/badge';
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useOrganizationTimezone } from '@/hooks/useOrganizationTimezone';
|
||||
import { formatDateTime } from '@/lib/dateTime';
|
||||
import logger from '@/lib/logger';
|
||||
|
||||
interface DocumentListProps {
|
||||
|
|
@ -20,6 +22,7 @@ interface DocumentListProps {
|
|||
}
|
||||
|
||||
export default function DocumentList({ refreshTrigger }: DocumentListProps) {
|
||||
const organizationTimezone = useOrganizationTimezone();
|
||||
const [documents, setDocuments] = useState<DocumentResponseSchema[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
|
@ -120,11 +123,6 @@ export default function DocumentList({ refreshTrigger }: DocumentListProps) {
|
|||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
||||
};
|
||||
|
||||
const filteredDocuments = documents.filter((doc) =>
|
||||
doc.filename.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
|
@ -212,7 +210,7 @@ export default function DocumentList({ refreshTrigger }: DocumentListProps) {
|
|||
{doc.processing_status === 'completed' && doc.retrieval_mode !== 'full_document' && (
|
||||
<span>{doc.total_chunks} chunks</span>
|
||||
)}
|
||||
<span>{formatDate(doc.created_at)}</span>
|
||||
<span>{formatDateTime(doc.created_at, organizationTimezone)}</span>
|
||||
</div>
|
||||
{doc.processing_error && (
|
||||
<p className="text-xs text-destructive mt-1">
|
||||
|
|
|
|||
|
|
@ -14,9 +14,12 @@ import { Button } from "@/components/ui/button";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useAudioPlayback } from "@/hooks/useAudioPlayback";
|
||||
import { useOrganizationTimezone } from "@/hooks/useOrganizationTimezone";
|
||||
import { formatDateTime } from "@/lib/dateTime";
|
||||
import logger from "@/lib/logger";
|
||||
|
||||
export default function RecordingsList({ refreshKey }: { refreshKey?: number }) {
|
||||
const organizationTimezone = useOrganizationTimezone();
|
||||
const [recordings, setRecordings] = useState<RecordingResponseSchema[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
|
@ -130,11 +133,6 @@ export default function RecordingsList({ refreshKey }: { refreshKey?: number })
|
|||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
|
||||
};
|
||||
|
||||
const filteredRecordings = recordings.filter((rec) => {
|
||||
if (!searchQuery) return true;
|
||||
const q = searchQuery.toLowerCase();
|
||||
|
|
@ -288,7 +286,7 @@ export default function RecordingsList({ refreshKey }: { refreshKey?: number })
|
|||
{rec.transcript}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground flex-wrap">
|
||||
<span>{formatDate(rec.created_at)}</span>
|
||||
<span>{formatDateTime(rec.created_at, organizationTimezone)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from "@/components/ui/table";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { formatDateTime } from '@/lib/dateTime';
|
||||
import{ superadminFilterAttributes } from "@/lib/filterAttributes";
|
||||
import { decodeFiltersFromURL, encodeFiltersToURL } from '@/lib/filters';
|
||||
import { impersonateAsSuperadmin } from '@/lib/utils';
|
||||
|
|
@ -248,8 +249,6 @@ export default function RunsPage() {
|
|||
* ----------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const formatDate = (dateString: string) => new Date(dateString).toLocaleString();
|
||||
|
||||
const calculateDuration = (isCompleted: boolean, usageInfo?: Record<string, unknown>) => {
|
||||
if (isCompleted && typeof usageInfo?.call_duration_seconds === 'number') {
|
||||
return `${Number(usageInfo.call_duration_seconds).toFixed(2)}s`;
|
||||
|
|
@ -472,7 +471,7 @@ export default function RunsPage() {
|
|||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{formatDate(run.created_at)}
|
||||
{formatDateTime(run.created_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex space-x-2">
|
||||
|
|
|
|||
|
|
@ -57,8 +57,10 @@ import {
|
|||
} from "@/components/ui/table";
|
||||
import { useAppConfig } from "@/context/AppConfigContext";
|
||||
import { useOrgConfig } from "@/context/OrgConfigContext";
|
||||
import { useOrganizationTimezone } from "@/hooks/useOrganizationTimezone";
|
||||
import { detailFromError } from "@/lib/apiError";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { formatDateTime } from "@/lib/dateTime";
|
||||
import { resolveWebhookBaseUrl } from "@/lib/webhookUrl";
|
||||
|
||||
const INBOUND_WEBHOOK_PATH = "/api/v1/telephony/inbound/run";
|
||||
|
|
@ -71,6 +73,7 @@ export default function TelephonyConfigurationDetailPage() {
|
|||
const { user, getAccessToken, loading: authLoading } = useAuth();
|
||||
const { config: appConfig } = useAppConfig();
|
||||
const { externalPbxIntegrationsEnabled } = useOrgConfig();
|
||||
const organizationTimezone = useOrganizationTimezone();
|
||||
const inboundWebhookUrl = `${resolveWebhookBaseUrl(appConfig?.tunnelUrl)}${INBOUND_WEBHOOK_PATH}`;
|
||||
const [config, setConfig] = useState<TelephonyConfigurationDetail | null>(null);
|
||||
const [phoneNumbers, setPhoneNumbers] = useState<PhoneNumberResponse[]>([]);
|
||||
|
|
@ -220,7 +223,7 @@ export default function TelephonyConfigurationDetailPage() {
|
|||
)}
|
||||
</div>
|
||||
<CardDescription>
|
||||
Updated {new Date(config.updated_at).toLocaleString()}
|
||||
Updated {formatDateTime(config.updated_at, organizationTimezone)}
|
||||
</CardDescription>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -26,13 +26,11 @@ import {
|
|||
import { useUserConfig } from '@/context/UserConfigContext';
|
||||
import { detailFromError } from '@/lib/apiError';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { formatDateTime, getLocalTimezone } from '@/lib/dateTime';
|
||||
import { usageFilterAttributes } from '@/lib/filterAttributes';
|
||||
import { decodeFiltersFromURL, encodeFiltersToURL } from '@/lib/filters';
|
||||
import type { ActiveFilter, DateRangeValue, FilterAttribute, NumberFilterOption } from '@/types/filters';
|
||||
|
||||
// Get local timezone
|
||||
const getLocalTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
const buildAgentFilterAttributes = (
|
||||
agentOptions: NumberFilterOption[] | null,
|
||||
isLoadingAgentOptions: boolean
|
||||
|
|
@ -390,22 +388,8 @@ export default function UsagePage() {
|
|||
router.push(`/workflow/${run.workflow_id}/run/${run.id}`);
|
||||
};
|
||||
|
||||
// Format datetime for display with timezone support
|
||||
const formatDateTime = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const tzValue = typeof selectedTimezone === 'string' ? selectedTimezone : selectedTimezone.value;
|
||||
// Use local timezone if none selected (during loading)
|
||||
const effectiveTz = tzValue || localTimezone;
|
||||
return date.toLocaleString('en-US', {
|
||||
timeZone: effectiveTz,
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
};
|
||||
const timezoneValue = typeof selectedTimezone === 'string' ? selectedTimezone : selectedTimezone.value;
|
||||
const effectiveTimezone = timezoneValue || localTimezone;
|
||||
|
||||
// Format duration for display
|
||||
const formatDuration = (seconds: number) => {
|
||||
|
|
@ -602,7 +586,7 @@ export default function UsagePage() {
|
|||
<span className="text-sm text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{formatDateTime(run.created_at)}</TableCell>
|
||||
<TableCell>{formatDateTime(run.created_at, effectiveTimezone)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatDuration(run.call_duration_seconds)}
|
||||
</TableCell>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import {
|
||||
Bot,
|
||||
Check,
|
||||
Clock,
|
||||
Copy,
|
||||
Download,
|
||||
ExternalLink,
|
||||
|
|
@ -31,12 +32,15 @@ import { Skeleton } from '@/components/ui/skeleton';
|
|||
import { ConversationRailFrame, RealtimeFeedback, WorkflowRunLogs } from '@/components/workflow/conversation';
|
||||
import { PostHogEvent } from '@/constants/posthog-events';
|
||||
import { WORKFLOW_RUN_MODES } from '@/constants/workflowRunModes';
|
||||
import { useOrganizationTimezone } from '@/hooks/useOrganizationTimezone';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { formatDateTime } from '@/lib/dateTime';
|
||||
import { downloadFile, getSignedUrl } from '@/lib/files';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface WorkflowRunResponse {
|
||||
mode: string;
|
||||
created_at: string | null;
|
||||
is_completed: boolean;
|
||||
transcript_url: string | null;
|
||||
recording_url: string | null;
|
||||
|
|
@ -599,6 +603,7 @@ export default function WorkflowRunPage() {
|
|||
const params = useParams();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const auth = useAuth();
|
||||
const organizationTimezone = useOrganizationTimezone();
|
||||
const [workflowRun, setWorkflowRun] = useState<WorkflowRunResponse | null>(null);
|
||||
const [workflowName, setWorkflowName] = useState<string | null>(null);
|
||||
const customizeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
|
@ -639,6 +644,7 @@ export default function WorkflowRunPage() {
|
|||
setWorkflowName(workflowResponse.data?.name ?? null);
|
||||
const runData = {
|
||||
mode: runResponse.data?.mode ?? '',
|
||||
created_at: runResponse.data?.created_at ?? null,
|
||||
is_completed: runResponse.data?.is_completed ?? false,
|
||||
transcript_url: runResponse.data?.transcript_url ?? null,
|
||||
recording_url: runResponse.data?.recording_url ?? null,
|
||||
|
|
@ -739,6 +745,12 @@ export default function WorkflowRunPage() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{workflowRun?.created_at && (
|
||||
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Clock className="h-4 w-4" />
|
||||
Call time: {formatDateTime(workflowRun.created_at, organizationTimezone)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Link href={`/workflow/${params.workflowId}`}>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { formatCalendarDate } from '@/lib/dateTime';
|
||||
|
||||
interface DailyUsageTableProps {
|
||||
data: DailyUsageBreakdownResponse | null;
|
||||
|
|
@ -16,16 +17,6 @@ interface DailyUsageTableProps {
|
|||
}
|
||||
|
||||
export function DailyUsageTable({ data, isLoading }: DailyUsageTableProps) {
|
||||
// Format date for display
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
|
|
@ -79,7 +70,7 @@ export function DailyUsageTable({ data, isLoading }: DailyUsageTableProps) {
|
|||
{data.breakdown.map((day) => (
|
||||
<TableRow key={day.date}>
|
||||
<TableCell className="font-medium">
|
||||
{formatDate(day.date)}
|
||||
{formatCalendarDate(day.date)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{day.minutes.toFixed(1)}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
|||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { formatLocalDateTime } from "@/lib/dateTime";
|
||||
import { getDatePresetValue } from "@/lib/filters";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DateRangeValue } from "@/types/filters";
|
||||
|
|
@ -40,7 +41,7 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|||
|
||||
const formatDate = (date: Date | null) => {
|
||||
if (!date) return "Select date";
|
||||
return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
return formatLocalDateTime(date);
|
||||
};
|
||||
|
||||
const handlePresetClick = (preset: string) => {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useOrganizationTimezone } from "@/hooks/useOrganizationTimezone";
|
||||
import { formatDateTime } from "@/lib/dateTime";
|
||||
import { ActiveFilter, FilterAttribute } from "@/types/filters";
|
||||
|
||||
export interface WorkflowRunsTableProps {
|
||||
|
|
@ -85,12 +87,11 @@ export function WorkflowRunsTable({
|
|||
emptyMessage = "No workflow runs found",
|
||||
}: WorkflowRunsTableProps) {
|
||||
const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
|
||||
const organizationTimezone = useOrganizationTimezone();
|
||||
|
||||
// Media preview dialog
|
||||
const mediaPreview = MediaPreviewDialog();
|
||||
|
||||
const formatDate = (dateString: string) => new Date(dateString).toLocaleString();
|
||||
|
||||
const handleRowClick = (runId: number) => {
|
||||
window.open(`/workflow/${workflowId}/run/${runId}`, '_blank');
|
||||
};
|
||||
|
|
@ -188,7 +189,9 @@ export function WorkflowRunsTable({
|
|||
{run.is_completed ? "Completed" : "In Progress"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{formatDate(run.created_at)}</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{formatDateTime(run.created_at, organizationTimezone)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<CallTypeCell mode={run.mode} callType={run.call_type} />
|
||||
</TableCell>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { useOrganizationTimezone } from '@/hooks/useOrganizationTimezone';
|
||||
import { formatDate } from '@/lib/dateTime';
|
||||
|
||||
interface WorkflowCardProps {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -10,6 +13,7 @@ interface WorkflowCardProps {
|
|||
|
||||
export function WorkflowCard({ id, name, createdAt }: WorkflowCardProps) {
|
||||
const router = useRouter();
|
||||
const organizationTimezone = useOrganizationTimezone();
|
||||
|
||||
const handleClick = () => {
|
||||
router.push(`/workflow/${id}`);
|
||||
|
|
@ -23,7 +27,7 @@ export function WorkflowCard({ id, name, createdAt }: WorkflowCardProps) {
|
|||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">{name}</h3>
|
||||
<p className="text-gray-600 mb-2">
|
||||
Created: {new Date(createdAt).toLocaleDateString()}
|
||||
Created: {formatDate(createdAt, organizationTimezone)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useOrganizationTimezone } from '@/hooks/useOrganizationTimezone';
|
||||
import { formatDate } from '@/lib/dateTime';
|
||||
|
||||
interface Workflow {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -64,6 +67,7 @@ export function WorkflowTable({
|
|||
currentFolderId = null,
|
||||
}: WorkflowTableProps) {
|
||||
const router = useRouter();
|
||||
const organizationTimezone = useOrganizationTimezone();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [loadingWorkflowId, setLoadingWorkflowId] = useState<number | null>(null);
|
||||
const [movingWorkflowId, setMovingWorkflowId] = useState<number | null>(null);
|
||||
|
|
@ -152,11 +156,7 @@ export function WorkflowTable({
|
|||
{workflow.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(workflow.created_at).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})}
|
||||
{formatDate(workflow.created_at, organizationTimezone)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className="inline-flex items-center justify-center min-w-[2rem] px-2 py-1 text-sm font-semibold bg-muted rounded-full">
|
||||
|
|
|
|||
9
ui/src/hooks/useOrganizationTimezone.ts
Normal file
9
ui/src/hooks/useOrganizationTimezone.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import { useOrgConfig } from '@/context/OrgConfigContext';
|
||||
import { getLocalTimezone } from '@/lib/dateTime';
|
||||
|
||||
export function useOrganizationTimezone() {
|
||||
const { organizationPreferences } = useOrgConfig();
|
||||
return organizationPreferences?.timezone || getLocalTimezone();
|
||||
}
|
||||
21
ui/src/lib/dateTime.test.ts
Normal file
21
ui/src/lib/dateTime.test.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { formatCalendarDate, formatDate, formatDateTime } from '@/lib/dateTime';
|
||||
|
||||
describe('dateTime', () => {
|
||||
it('formats an instant in the requested timezone', () => {
|
||||
expect(formatDateTime('2026-07-23T10:00:00Z', 'Asia/Kolkata')).toBe(
|
||||
'Jul 23, 2026, 03:30 PM',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats the date represented in the requested timezone', () => {
|
||||
expect(formatDate('2026-07-23T01:00:00Z', 'America/Los_Angeles')).toBe(
|
||||
'Jul 22, 2026',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not shift calendar-only dates across timezones', () => {
|
||||
expect(formatCalendarDate('2026-07-23')).toBe('Jul 23, 2026');
|
||||
});
|
||||
});
|
||||
46
ui/src/lib/dateTime.ts
Normal file
46
ui/src/lib/dateTime.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
export function getLocalTimezone() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
|
||||
type DateInput = Date | number | string;
|
||||
|
||||
export function formatDateTime(value: DateInput, timezone?: string | null) {
|
||||
return new Date(value).toLocaleString('en-US', {
|
||||
timeZone: timezone || getLocalTimezone(),
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatDate(value: DateInput, timezone?: string | null) {
|
||||
return new Date(value).toLocaleDateString('en-US', {
|
||||
timeZone: timezone || getLocalTimezone(),
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
export function formatCalendarDate(value: string) {
|
||||
const calendarDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
||||
if (!calendarDate) {
|
||||
return formatDate(value, 'UTC');
|
||||
}
|
||||
|
||||
const [, year, month, day] = calendarDate;
|
||||
return formatDate(
|
||||
Date.UTC(Number(year), Number(month) - 1, Number(day)),
|
||||
'UTC',
|
||||
);
|
||||
}
|
||||
|
||||
export function formatLocalDateTime(value: Date) {
|
||||
return `${value.toLocaleDateString()} ${value.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}`;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { formatLocalDateTime } from "@/lib/dateTime";
|
||||
import { ActiveFilter, DateRangeValue, FilterAttribute, FilterValue, MultiSelectValue, NumberRangeValue, NumberValue, RadioValue, TextValue } from "@/types/filters";
|
||||
|
||||
// Get default value based on attribute type
|
||||
|
|
@ -187,11 +188,7 @@ export const decodeFiltersFromURL = (
|
|||
export const formatDateRange = (value: DateRangeValue): string => {
|
||||
if (!value.from || !value.to) return "No date range selected";
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
return `${formatDate(value.from)} to ${formatDate(value.to)}`;
|
||||
return `${formatLocalDateTime(value.from)} to ${formatLocalDateTime(value.to)}`;
|
||||
};
|
||||
|
||||
// Format number range for display
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue