fix: number pool initialization in multi telephony setup

If there are multiple telephony configurations, the form number should be initialized from the campaigns given telephonic configuration rather than the organization default telephonic configuration.
This commit is contained in:
Abhishek Kumar 2026-05-08 14:48:53 +05:30
parent 81a363b06e
commit 6d93be3ef6
31 changed files with 1105 additions and 238 deletions

View file

@ -1,11 +1,12 @@
"use client";
import { ChevronLeft, ChevronRight, Globe } from 'lucide-react';
import { ChevronLeft, ChevronRight, Download, Globe } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useId, useState } from 'react';
import TimezoneSelect, { type ITimezoneOption } from 'react-timezone-select';
import { toast } from 'sonner';
import { getDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGet, getMpsCreditsApiV1OrganizationsUsageMpsCreditsGet, getUsageHistoryApiV1OrganizationsUsageRunsGet } from '@/client/sdk.gen';
import { downloadUsageRunsReportApiV1OrganizationsUsageRunsReportGet, getDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGet, getMpsCreditsApiV1OrganizationsUsageMpsCreditsGet, getUsageHistoryApiV1OrganizationsUsageRunsGet } from '@/client/sdk.gen';
import type { DailyUsageBreakdownResponse, MpsCreditsResponse, UsageHistoryResponse, WorkflowRunUsageResponse } from '@/client/types.gen';
import { DailyUsageTable } from '@/components/DailyUsageTable';
import { FilterBuilder } from '@/components/filters/FilterBuilder';
@ -49,15 +50,21 @@ export default function UsagePage() {
return pageParam ? parseInt(pageParam, 10) : 1;
});
const [isExecutingFilters, setIsExecutingFilters] = useState(false);
const [isDownloadingReport, setIsDownloadingReport] = useState(false);
// Daily usage breakdown state (only for paid orgs)
const [dailyUsage, setDailyUsage] = useState<DailyUsageBreakdownResponse | null>(null);
const [isLoadingDaily, setIsLoadingDaily] = useState(false);
// Initialize filters from URL
// Initialize filters from URL. `activeFilters` tracks the in-progress
// edits in the FilterBuilder; `appliedFilters` is what's actually been
// committed via Apply (and what drives fetching + the download button).
const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>(() => {
return decodeFiltersFromURL(searchParams, usageFilterAttributes);
});
const [appliedFilters, setAppliedFilters] = useState<ActiveFilter[]>(() => {
return decodeFiltersFromURL(searchParams, usageFilterAttributes);
});
// Media preview dialog
const mediaPreview = MediaPreviewDialog();
@ -83,51 +90,50 @@ export default function UsagePage() {
}
}, [auth.isAuthenticated]);
// Translate the FilterBuilder state into the query-param shape the
// backend expects. Shared between the listing fetch and the CSV export
// so they stay in lockstep.
const buildUsageQueryParams = (filters?: ActiveFilter[]) => {
let filterParam: string | undefined;
let startDate = '';
let endDate = '';
if (filters && filters.length > 0) {
const dateRangeFilter = filters.find(f => f.attribute.id === 'dateRange');
if (dateRangeFilter && dateRangeFilter.value) {
const dateValue = dateRangeFilter.value as DateRangeValue;
if (dateValue.from) startDate = dateValue.from.toISOString();
if (dateValue.to) endDate = dateValue.to.toISOString();
}
const otherFilters = filters.filter(f => f.attribute.id !== 'dateRange');
if (otherFilters.length > 0) {
const filterData = otherFilters.map(filter => ({
attribute: filter.attribute.id,
type: filter.attribute.type,
value: filter.value,
}));
filterParam = JSON.stringify(filterData);
}
}
return {
...(startDate && { start_date: startDate }),
...(endDate && { end_date: endDate }),
...(filterParam && { filters: filterParam }),
};
};
// Fetch usage history
const fetchUsageHistory = useCallback(async (page: number, filters?: ActiveFilter[]) => {
if (!auth.isAuthenticated) return;
setIsLoadingHistory(true);
try {
let filterParam = undefined;
let startDate = '';
let endDate = '';
if (filters && filters.length > 0) {
// Extract date range filter if present
const dateRangeFilter = filters.find(f => f.attribute.id === 'dateRange');
if (dateRangeFilter && dateRangeFilter.value) {
const dateValue = dateRangeFilter.value as DateRangeValue;
if (dateValue.from) {
// The dates are already in the user's local timezone
// Convert to UTC ISO string for the backend
startDate = dateValue.from.toISOString();
}
if (dateValue.to) {
// Convert to UTC ISO string for the backend
endDate = dateValue.to.toISOString();
}
}
// Process other filters (excluding dateRange)
const otherFilters = filters.filter(f => f.attribute.id !== 'dateRange');
if (otherFilters.length > 0) {
const filterData = otherFilters.map(filter => ({
attribute: filter.attribute.id,
type: filter.attribute.type,
value: filter.value,
}));
filterParam = JSON.stringify(filterData);
}
}
const response = await getUsageHistoryApiV1OrganizationsUsageRunsGet({
query: {
page,
limit: 50,
...(startDate && { start_date: startDate }),
...(endDate && { end_date: endDate }),
...(filterParam && { filters: filterParam })
...buildUsageQueryParams(filters),
},
});
@ -161,6 +167,37 @@ export default function UsagePage() {
}
}, [auth.isAuthenticated, organizationPricing]);
// Download a CSV of all runs matching the current filters.
const handleDownloadReport = async () => {
if (!auth.isAuthenticated) return;
setIsDownloadingReport(true);
try {
const response = await downloadUsageRunsReportApiV1OrganizationsUsageRunsReportGet({
query: buildUsageQueryParams(appliedFilters),
parseAs: 'blob',
});
if (response.data) {
const blob = response.data as Blob;
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'usage_runs_report.csv';
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} else {
toast.error('Failed to download report');
}
} catch (error) {
console.error('Failed to download usage report:', error);
toast.error('Failed to download report');
} finally {
setIsDownloadingReport(false);
}
};
// Handle timezone change
const handleTimezoneChange = async (timezone: ITimezoneOption | string) => {
setSelectedTimezone(timezone);
@ -195,9 +232,9 @@ export default function UsagePage() {
useEffect(() => {
if (auth.isAuthenticated) {
fetchMpsCredits();
fetchUsageHistory(currentPage, activeFilters);
fetchUsageHistory(currentPage, appliedFilters);
}
}, [auth.isAuthenticated, currentPage, activeFilters, fetchUsageHistory, fetchMpsCredits]);
}, [auth.isAuthenticated, currentPage, appliedFilters, fetchUsageHistory, fetchMpsCredits]);
// Fetch daily usage when organizationPricing becomes available
useEffect(() => {
@ -229,6 +266,7 @@ export default function UsagePage() {
const handleApplyFilters = useCallback(async () => {
setIsExecutingFilters(true);
setCurrentPage(1); // Reset to first page when applying filters
setAppliedFilters(activeFilters);
updateUrlParams({ page: 1, filters: activeFilters });
await fetchUsageHistory(1, activeFilters);
setIsExecutingFilters(false);
@ -241,6 +279,8 @@ export default function UsagePage() {
const handleClearFilters = useCallback(async () => {
setIsExecutingFilters(true);
setCurrentPage(1);
setActiveFilters([]);
setAppliedFilters([]);
updateUrlParams({ page: 1, filters: [] }); // Clear filters from URL
await fetchUsageHistory(1, []); // Fetch all runs without filters
setIsExecutingFilters(false);
@ -249,8 +289,8 @@ export default function UsagePage() {
// Handle page change
const handlePageChange = (newPage: number) => {
setCurrentPage(newPage);
updateUrlParams({ page: newPage, filters: activeFilters });
fetchUsageHistory(newPage, activeFilters);
updateUrlParams({ page: newPage, filters: appliedFilters });
fetchUsageHistory(newPage, appliedFilters);
};
// Handle row click to navigate to workflow run
@ -289,8 +329,8 @@ export default function UsagePage() {
<div>
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold mb-2">Usage Dashboard</h1>
<p className="text-muted-foreground">Monitor your Dograh Token usage and quota</p>
<h1 className="text-3xl font-bold mb-2">Agent Runs</h1>
<p className="text-muted-foreground">See all your Agent Runs across all Voice Agents. You can use filters to filter out required Agent Runs.</p>
</div>
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
@ -419,7 +459,7 @@ export default function UsagePage() {
)}
{/* Filter Builder */}
<div className="mb-6">
<div className="mb-6 space-y-3">
<FilterBuilder
availableAttributes={usageFilterAttributes}
activeFilters={activeFilters}
@ -428,6 +468,19 @@ export default function UsagePage() {
onClearFilters={handleClearFilters}
isExecuting={isExecutingFilters}
/>
{appliedFilters.length > 0 && (
<div className="flex justify-end">
<Button
variant="outline"
size="sm"
onClick={handleDownloadReport}
disabled={isDownloadingReport}
>
<Download className="h-4 w-4 mr-2" />
{isDownloadingReport ? 'Preparing...' : 'Download Filtered Results'}
</Button>
</div>
)}
</div>
{/* Usage History */}
@ -435,9 +488,9 @@ export default function UsagePage() {
<CardHeader>
<div className="flex justify-between items-start">
<div className="space-y-1.5">
<CardTitle>Usage History</CardTitle>
<CardTitle>All Runs</CardTitle>
<CardDescription>
View detailed usage by workflow run
Every agent run across your organization, with usage details
</CardDescription>
</div>
</div>
@ -463,7 +516,7 @@ export default function UsagePage() {
<TableHead className="font-semibold">Date</TableHead>
<TableHead className="font-semibold text-right">Duration</TableHead>
<TableHead className="font-semibold text-right">
{organizationPricing?.price_per_second_usd ? 'Cost (USD)' : 'Dograh Tokens'}
{organizationPricing?.price_per_second_usd ? 'Cost (USD)' : 'Tokens'}
</TableHead>
<TableHead className="font-semibold">Actions</TableHead>
</TableRow>
@ -490,7 +543,9 @@ export default function UsagePage() {
)}
</TableCell>
<TableCell className="text-sm">
{run.phone_number || '-'}
{(run.call_type === 'inbound'
? run.caller_number
: run.called_number) || '-'}
</TableCell>
<TableCell>
{run.disposition ? (
@ -526,7 +581,7 @@ export default function UsagePage() {
</div>
{/* Summary */}
{activeFilters.length > 0 && (
{appliedFilters.length > 0 && (
<div className="mt-4 p-3 bg-muted rounded-md">
<p className="text-sm text-muted-foreground">
Total for filtered period: <span className="font-semibold text-foreground">
@ -570,7 +625,7 @@ export default function UsagePage() {
)}
</>
) : (
<p className="text-center py-8 text-muted-foreground">No usage history found</p>
<p className="text-center py-8 text-muted-foreground">No runs found</p>
)}
</CardContent>
</Card>

View file

@ -9,9 +9,10 @@ import { PhoneInput } from 'react-international-phone';
import {
initiateCallApiV1TelephonyInitiateCallPost,
listPhoneNumbersApiV1OrganizationsTelephonyConfigsConfigIdPhoneNumbersGet,
listTelephonyConfigurationsApiV1OrganizationsTelephonyConfigsGet
} from '@/client/sdk.gen';
import type { TelephonyConfigurationListItem } from '@/client/types.gen';
import type { PhoneNumberResponse, TelephonyConfigurationListItem } from '@/client/types.gen';
import { Button } from "@/components/ui/button";
import {
Dialog,
@ -58,6 +59,9 @@ export const PhoneCallDialog = ({
const [sipMode, setSipMode] = useState(() => /^(PJSIP|SIP)\//i.test(userConfig?.test_phone_number || ""));
const [telephonyConfigs, setTelephonyConfigs] = useState<TelephonyConfigurationListItem[]>([]);
const [selectedConfigId, setSelectedConfigId] = useState<string>("");
const [fromPhoneNumbers, setFromPhoneNumbers] = useState<PhoneNumberResponse[]>([]);
const [selectedFromPhoneNumberId, setSelectedFromPhoneNumberId] = useState<string>("");
const [loadingPhoneNumbers, setLoadingPhoneNumbers] = useState(false);
// Check telephony configuration when dialog opens
useEffect(() => {
@ -102,9 +106,49 @@ export const PhoneCallDialog = ({
setNeedsConfiguration(null);
setTelephonyConfigs([]);
setSelectedConfigId("");
setFromPhoneNumbers([]);
setSelectedFromPhoneNumberId("");
}
}, [open]);
// Fetch phone numbers whenever the selected telephony configuration changes.
useEffect(() => {
if (!open || !selectedConfigId) {
setFromPhoneNumbers([]);
setSelectedFromPhoneNumberId("");
return;
}
let cancelled = false;
const fetchPhoneNumbers = async () => {
setLoadingPhoneNumbers(true);
try {
const response = await listPhoneNumbersApiV1OrganizationsTelephonyConfigsConfigIdPhoneNumbersGet({
path: { config_id: Number(selectedConfigId) },
});
if (cancelled) return;
const all = response.data?.phone_numbers ?? [];
const active = all.filter((p) => p.is_active);
setFromPhoneNumbers(active);
const defaultPhone = active.find((p) => p.is_default_caller_id) ?? active[0];
setSelectedFromPhoneNumberId(defaultPhone ? String(defaultPhone.id) : "");
} catch (err) {
if (cancelled) return;
console.error("Failed to load phone numbers for config:", err);
setFromPhoneNumbers([]);
setSelectedFromPhoneNumberId("");
} finally {
if (!cancelled) setLoadingPhoneNumbers(false);
}
};
fetchPhoneNumbers();
return () => {
cancelled = true;
};
}, [open, selectedConfigId]);
// Keep phoneNumber in sync with userConfig when dialog opens
useEffect(() => {
if (open) {
@ -148,6 +192,7 @@ export const PhoneCallDialog = ({
workflow_id: workflowId,
phone_number: phoneNumber,
telephony_configuration_id: selectedConfigId ? Number(selectedConfigId) : null,
from_phone_number_id: selectedFromPhoneNumberId ? Number(selectedFromPhoneNumberId) : null,
},
});
@ -230,6 +275,38 @@ export const PhoneCallDialog = ({
</Select>
</div>
)}
{selectedConfigId && (
<div className="flex flex-col gap-1.5">
<Label htmlFor="from-phone-number">Caller ID (from)</Label>
{loadingPhoneNumbers ? (
<div className="flex items-center text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
Loading phone numbers...
</div>
) : fromPhoneNumbers.length > 0 ? (
<Select
value={selectedFromPhoneNumberId}
onValueChange={setSelectedFromPhoneNumberId}
>
<SelectTrigger id="from-phone-number" className="w-full">
<SelectValue placeholder="Select a phone number" />
</SelectTrigger>
<SelectContent>
{fromPhoneNumbers.map((phone) => (
<SelectItem key={phone.id} value={String(phone.id)}>
{phone.label ? `${phone.label}${phone.address}` : phone.address}
{phone.is_default_caller_id ? " — default" : ""}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<div className="text-xs text-muted-foreground">
No phone numbers in this configuration. The provider will pick one automatically.
</div>
)}
</div>
)}
{sipMode ? (
<Input
value={phoneNumber}