mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
feat: date range in download report
This commit is contained in:
parent
1c333d4a64
commit
0b5fd107fa
6 changed files with 166 additions and 19 deletions
|
|
@ -365,12 +365,29 @@ class CampaignClient(BaseDBClient):
|
|||
result = await session.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_completed_runs_for_report(self, campaign_id: int) -> list:
|
||||
async def get_completed_runs_for_report(
|
||||
self,
|
||||
campaign_id: int,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
) -> list:
|
||||
"""Get completed workflow runs for campaign report CSV.
|
||||
|
||||
Returns rows with only the columns needed for report generation.
|
||||
"""
|
||||
async with self.async_session() as session:
|
||||
conditions = [
|
||||
WorkflowRunModel.campaign_id == campaign_id,
|
||||
WorkflowRunModel.is_completed.is_(True),
|
||||
WorkflowRunModel.cost_info["call_duration_seconds"]
|
||||
.as_string()
|
||||
.isnot(None),
|
||||
]
|
||||
if start_date is not None:
|
||||
conditions.append(WorkflowRunModel.created_at >= start_date)
|
||||
if end_date is not None:
|
||||
conditions.append(WorkflowRunModel.created_at <= end_date)
|
||||
|
||||
query = (
|
||||
select(
|
||||
WorkflowRunModel.id,
|
||||
|
|
@ -381,13 +398,7 @@ class CampaignClient(BaseDBClient):
|
|||
WorkflowRunModel.logs,
|
||||
WorkflowRunModel.public_access_token,
|
||||
)
|
||||
.where(
|
||||
WorkflowRunModel.campaign_id == campaign_id,
|
||||
WorkflowRunModel.is_completed.is_(True),
|
||||
WorkflowRunModel.cost_info["call_duration_seconds"]
|
||||
.as_string()
|
||||
.isnot(None),
|
||||
)
|
||||
.where(*conditions)
|
||||
.order_by(WorkflowRunModel.created_at.desc())
|
||||
)
|
||||
result = await session.execute(query)
|
||||
|
|
|
|||
|
|
@ -706,13 +706,21 @@ async def get_campaign_source_download_url(
|
|||
async def download_campaign_report(
|
||||
campaign_id: int,
|
||||
user: UserModel = Depends(get_user),
|
||||
start_date: Optional[datetime] = Query(
|
||||
None, description="Filter runs created on or after this datetime (ISO 8601)"
|
||||
),
|
||||
end_date: Optional[datetime] = Query(
|
||||
None, description="Filter runs created on or before this datetime (ISO 8601)"
|
||||
),
|
||||
) -> StreamingResponse:
|
||||
"""Download a CSV report of completed campaign runs."""
|
||||
campaign = await db_client.get_campaign(campaign_id, user.selected_organization_id)
|
||||
if not campaign:
|
||||
raise HTTPException(status_code=404, detail="Campaign not found")
|
||||
|
||||
output, filename = await generate_campaign_report_csv(campaign_id)
|
||||
output, filename = await generate_campaign_report_csv(
|
||||
campaign_id, start_date=start_date, end_date=end_date
|
||||
)
|
||||
|
||||
return StreamingResponse(
|
||||
output,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import csv
|
||||
import io
|
||||
from typing import Any, List
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from api.constants import BACKEND_API_ENDPOINT
|
||||
from api.db import db_client
|
||||
|
|
@ -27,12 +28,18 @@ def _collect_extracted_variable_keys(runs: List[Any]) -> list[str]:
|
|||
return list(keys)
|
||||
|
||||
|
||||
async def generate_campaign_report_csv(campaign_id: int) -> tuple[io.StringIO, str]:
|
||||
async def generate_campaign_report_csv(
|
||||
campaign_id: int,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
) -> tuple[io.StringIO, str]:
|
||||
"""Generate a CSV report for a campaign.
|
||||
|
||||
Returns a tuple of (csv_output, filename).
|
||||
"""
|
||||
runs = await db_client.get_completed_runs_for_report(campaign_id)
|
||||
runs = await db_client.get_completed_runs_for_report(
|
||||
campaign_id, start_date=start_date, end_date=end_date
|
||||
)
|
||||
|
||||
# Collect dynamic extracted variable columns
|
||||
extracted_var_keys = _collect_extracted_variable_keys(runs)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { ArrowLeft, Check, Clock, Download, Pause, Pencil, Play, RefreshCw, X } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { ArrowLeft, CalendarIcon, Check, Clock, Download, Pause, Pencil, Play, RefreshCw, X } from 'lucide-react';
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
|
@ -16,7 +17,11 @@ import {
|
|||
import type { CampaignResponse } from '@/client/types.gen';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
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 { useAuth } from '@/lib/auth';
|
||||
|
|
@ -43,6 +48,13 @@ export default function CampaignDetailPage() {
|
|||
const [isExecutingAction, setIsExecutingAction] = useState(false);
|
||||
const [isDownloadingReport, setIsDownloadingReport] = useState(false);
|
||||
|
||||
// Report date range state
|
||||
const [reportStartDate, setReportStartDate] = useState<Date | undefined>(undefined);
|
||||
const [reportStartTime, setReportStartTime] = useState('00:00');
|
||||
const [reportEndDate, setReportEndDate] = useState<Date | undefined>(undefined);
|
||||
const [reportEndTime, setReportEndTime] = useState('23:59');
|
||||
const [isReportPopoverOpen, setIsReportPopoverOpen] = useState(false);
|
||||
|
||||
// Fetch campaign details
|
||||
const fetchCampaign = useCallback(async () => {
|
||||
if (!user) return;
|
||||
|
|
@ -113,16 +125,33 @@ export default function CampaignDetailPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// Build ISO datetime string from date + time
|
||||
const buildDateTime = (date: Date | undefined, time: string): string | undefined => {
|
||||
if (!date) return undefined;
|
||||
const [hours, minutes] = time.split(':').map(Number);
|
||||
const combined = new Date(date);
|
||||
combined.setHours(hours, minutes, 0, 0);
|
||||
return combined.toISOString();
|
||||
};
|
||||
|
||||
// Handle download report
|
||||
const handleDownloadReport = async () => {
|
||||
if (!user) return;
|
||||
setIsDownloadingReport(true);
|
||||
setIsReportPopoverOpen(false);
|
||||
try {
|
||||
const accessToken = await getAccessToken();
|
||||
const startDate = buildDateTime(reportStartDate, reportStartTime);
|
||||
const endDate = buildDateTime(reportEndDate, reportEndTime);
|
||||
|
||||
const response = await downloadCampaignReportApiV1CampaignCampaignIdReportGet({
|
||||
path: {
|
||||
campaign_id: campaignId,
|
||||
},
|
||||
query: {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
},
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
|
|
@ -150,6 +179,13 @@ export default function CampaignDetailPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleClearDateRange = () => {
|
||||
setReportStartDate(undefined);
|
||||
setReportStartTime('00:00');
|
||||
setReportEndDate(undefined);
|
||||
setReportEndTime('23:59');
|
||||
};
|
||||
|
||||
// Handle start campaign
|
||||
const handleStart = async () => {
|
||||
if (!user) return;
|
||||
|
|
@ -368,10 +404,85 @@ export default function CampaignDetailPage() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={handleDownloadReport} disabled={isDownloadingReport}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Download Report
|
||||
</Button>
|
||||
<Popover open={isReportPopoverOpen} onOpenChange={setIsReportPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" disabled={isDownloadingReport}>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Download Report
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-4" align="end">
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-medium">Filter by date range</div>
|
||||
<div className="grid gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">From</Label>
|
||||
<div className="flex gap-2">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="w-[140px] justify-start text-left font-normal">
|
||||
<CalendarIcon className="mr-2 h-3.5 w-3.5" />
|
||||
{reportStartDate ? format(reportStartDate, 'MMM dd, yyyy') : 'Start date'}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={reportStartDate}
|
||||
onSelect={setReportStartDate}
|
||||
disabled={(date) => reportEndDate ? date > reportEndDate : false}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Input
|
||||
type="time"
|
||||
value={reportStartTime}
|
||||
onChange={(e) => setReportStartTime(e.target.value)}
|
||||
className="w-[100px] h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">To</Label>
|
||||
<div className="flex gap-2">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="w-[140px] justify-start text-left font-normal">
|
||||
<CalendarIcon className="mr-2 h-3.5 w-3.5" />
|
||||
{reportEndDate ? format(reportEndDate, 'MMM dd, yyyy') : 'End date'}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={reportEndDate}
|
||||
onSelect={setReportEndDate}
|
||||
disabled={(date) => reportStartDate ? date < reportStartDate : false}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Input
|
||||
type="time"
|
||||
value={reportEndTime}
|
||||
onChange={(e) => setReportEndTime(e.target.value)}
|
||||
className="w-[100px] h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between">
|
||||
<Button variant="ghost" size="sm" onClick={handleClearDateRange}>
|
||||
Clear
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleDownloadReport} disabled={isDownloadingReport}>
|
||||
<Download className="h-3.5 w-3.5 mr-1.5" />
|
||||
{reportStartDate || reportEndDate ? 'Download Filtered' : 'Download All'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{renderActionButton()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -3274,7 +3274,16 @@ export type DownloadCampaignReportApiV1CampaignCampaignIdReportGetData = {
|
|||
path: {
|
||||
campaign_id: number;
|
||||
};
|
||||
query?: never;
|
||||
query?: {
|
||||
/**
|
||||
* Filter runs created on or after this datetime (ISO 8601)
|
||||
*/
|
||||
start_date?: string | null;
|
||||
/**
|
||||
* Filter runs created on or before this datetime (ISO 8601)
|
||||
*/
|
||||
end_date?: string | null;
|
||||
};
|
||||
url: '/api/v1/campaign/{campaign_id}/report';
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue