base con autenticacion, registro, modulo encuestas

This commit is contained in:
2025-06-16 12:02:22 -04:00
commit 475e0754df
411 changed files with 26265 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
'use server';
import { safeFetchApi } from '@/lib/fetch.api';
import { SurveyStatisticsData } from '../schemas/statistics';
import { SurveyStatisticsSchema } from '../schemas/statistics-schema';
export const getSurveysStatistics = async (): Promise<SurveyStatisticsData> => {
const [error, data] = await safeFetchApi(
SurveyStatisticsSchema,
`surveys/statistics`,
'GET',
);
if (error) {
console.log(error);
// console.log(error.details);
throw new Error('Ocurrio un error');
}
if (!data) {
throw new Error('No statistics data available');
}
return data?.data;
};

View File

@@ -0,0 +1,127 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@repo/shadcn/select';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
import { SurveyStatisticsData } from '../schemas/statistics';
interface SurveyDetailsProps {
data: SurveyStatisticsData | undefined;
}
export function SurveyDetails({ data }: SurveyDetailsProps) {
const [selectedSurvey, setSelectedSurvey] = useState<string>('');
if (!data || !data.surveyDetails || data.surveyDetails.length === 0) {
return (
<Card>
<CardContent className="pt-6">
<p className="text-center text-muted-foreground">No hay datos detallados disponibles</p>
</CardContent>
</Card>
);
}
// Set default selected survey if none is selected
if (!selectedSurvey && data.surveyDetails.length > 0) {
setSelectedSurvey(data.surveyDetails?.[0]?.id.toString() ?? '');
}
const currentSurvey = data.surveyDetails.find(
(survey) => survey.id.toString() === selectedSurvey
);
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d', '#ffc658'];
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Detalles por Encuesta</CardTitle>
<CardDescription>Análisis detallado de respuestas por encuesta</CardDescription>
</CardHeader>
<CardContent>
<Select value={selectedSurvey} onValueChange={setSelectedSurvey}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Selecciona una encuesta" />
</SelectTrigger>
<SelectContent>
{data.surveyDetails.map((survey) => (
<SelectItem key={survey.id} value={survey.id.toString()}>
{survey.title}
</SelectItem>
))}
</SelectContent>
</Select>
</CardContent>
</Card>
{currentSurvey && (
<>
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>{currentSurvey.title}</CardTitle>
<CardDescription>{currentSurvey.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span className="font-medium">Total de respuestas:</span>
<span>{currentSurvey.totalResponses}</span>
</div>
<div className="flex justify-between">
<span className="font-medium">Audiencia objetivo:</span>
<span>{currentSurvey.targetAudience}</span>
</div>
<div className="flex justify-between">
<span className="font-medium">Fecha de creación:</span>
<span>{new Date(currentSurvey.createdAt).toLocaleDateString()}</span>
</div>
{currentSurvey.closingDate && (
<div className="flex justify-between">
<span className="font-medium">Fecha de cierre:</span>
<span>{new Date(currentSurvey.closingDate).toLocaleDateString()}</span>
</div>
)}
</div>
</CardContent>
</Card>
{currentSurvey.questionStats && currentSurvey.questionStats.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Distribución de Respuestas</CardTitle>
</CardHeader>
<CardContent className="h-80">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={currentSurvey.questionStats}
cx="50%"
cy="50%"
labelLine={true}
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="count"
nameKey="label"
>
{currentSurvey.questionStats.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip formatter={(value, name) => [`${value}`, name]} />
{/* <Legend /> */}
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
)}
</div>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,82 @@
'use client';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
import { SurveyStatisticsData } from '../schemas/statistics';
interface SurveyOverviewProps {
data: SurveyStatisticsData | undefined;
}
export function SurveyOverview({ data }: SurveyOverviewProps) {
if (!data) return null;
const { totalSurveys, totalResponses, completionRate, surveysByMonth } = data;
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8'];
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total de Encuestas</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalSurveys}</div>
<p className="text-xs text-muted-foreground">
Encuestas creadas en la plataforma
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total de Respuestas</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalResponses}</div>
<p className="text-xs text-muted-foreground">
Respuestas recibidas en todas las encuestas
</p>
</CardContent>
</Card>
<Card>
{/* <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Tasa de Completado</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalSurveys/totalResponses}</div>
<p className="text-xs text-muted-foreground">
Porcentaje de encuestas completadas
</p>
</CardContent> */}
</Card>
<Card className="col-span-full">
<CardHeader>
<CardTitle>Encuestas por Mes</CardTitle>
<CardDescription>Distribución de encuestas creadas por mes</CardDescription>
</CardHeader>
<CardContent className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={surveysByMonth}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip wrapperStyle={{color: '#000', fontWeight: 'bold' }}/>
<Legend />
<Bar dataKey="count" fill="#8884d8" name="Encuestas" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,78 @@
'use client';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
import { SurveyStatisticsData } from '../schemas/statistics';
interface SurveyResponsesProps {
data: SurveyStatisticsData | undefined;
}
export function SurveyResponses({ data }: SurveyResponsesProps) {
if (!data) return null;
const { responsesByAudience, responseDistribution } = data;
const COLORS = ['#0088FE', '#8884d8', '#00C49F', '#FFBB28', '#FF8042'];
return (
<div className="grid gap-4 md:grid-cols-2">
<Card className="col-span-1">
<CardHeader>
<CardTitle>Respuestas por Audiencia</CardTitle>
<CardDescription>Distribución de respuestas según el tipo de audiencia</CardDescription>
</CardHeader>
<CardContent className="h-80">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={responsesByAudience}
cx="50%"
cy="50%"
labelLine={true}
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{responsesByAudience.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip formatter={(value, name) => [`${value} respuestas`, name]} />
<Legend />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card className="col-span-1">
<CardHeader>
<CardTitle>Distribución de Respuestas</CardTitle>
<CardDescription>Cantidad de respuestas por encuesta</CardDescription>
</CardHeader>
<CardContent className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={responseDistribution}
layout="vertical"
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis dataKey="title" type="category" width={150} tick={{ fontSize: 12 }} />
<Tooltip wrapperStyle={{color: '#000', fontWeight: 'bold' }}/>
<Legend />
<Bar dataKey="responses" fill="#8884d8" name="Respuestas" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,35 @@
'use client';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@repo/shadcn/tabs';
import { useSurveysStatsQuery } from '../hooks/use-query-statistics';
import { SurveyOverview } from './survey-overview';
import { SurveyResponses } from './survey-responses';
import { SurveyDetails } from './survey-details';
export function SurveyStatistics() {
const { data, isLoading } = useSurveysStatsQuery();
if (isLoading) {
return <div className="flex justify-center p-8">Cargando estadísticas...</div>;
}
return (
<Tabs defaultValue="overview" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview">Resumen General</TabsTrigger>
<TabsTrigger value="responses">Respuestas</TabsTrigger>
<TabsTrigger value="details">Detalles por Encuesta</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<SurveyOverview data={data} />
</TabsContent>
<TabsContent value="responses">
<SurveyResponses data={data} />
</TabsContent>
<TabsContent value="details">
<SurveyDetails data={data} />
</TabsContent>
</Tabs>
);
}

View File

@@ -0,0 +1,8 @@
import { useSafeQuery } from '@/hooks/use-safe-query';
import { getSurveysStatistics } from '../actions/surveys-statistics-actions';
// Hook for all survesys
export function useSurveysStatsQuery() {
return useSafeQuery(['surveys-statistics'], () => getSurveysStatistics())
}

View File

@@ -0,0 +1,59 @@
import { z } from 'zod';
// Esquema para QuestionStat
export const QuestionStatSchema = z.object({
questionId: z.string(),
label: z.string(),
count: z.number(),
});
// Esquema para SurveyDetail
export const SurveyDetailSchema = z.object({
id: z.number(),
title: z.string(),
description: z.string(),
totalResponses: z.number(),
targetAudience: z.string(),
createdAt: z.string(),
closingDate: z.string().optional(),
questionStats: z.array(QuestionStatSchema),
});
// Esquema para SurveyStatisticsData
export const SurveyStatisticsDataSchema = z.object({
totalSurveys: z.number(),
totalResponses: z.number(),
completionRate: z.number(),
surveysByMonth: z.array(
z.object({
month: z.string(),
count: z.number(),
})
),
responsesByAudience: z.array(
z.object({
name: z.string(),
value: z.number(),
})
),
responseDistribution: z.array(
z.object({
title: z.string(),
responses: z.number(),
})
),
surveyDetails: z.array(SurveyDetailSchema),
// surveyDetails: z.array(z.any()),
});
// Response schemas for the API create, update
export const SurveyStatisticsSchema = z.object({
message: z.string(),
data: SurveyStatisticsDataSchema,
});
// Tipos inferidos de Zod
export type SurveyStatisticsType = z.infer<typeof SurveyStatisticsSchema>;
export type SurveyDetailType = z.infer<typeof SurveyDetailSchema>;
export type QuestionStatType = z.infer<typeof QuestionStatSchema>;

View File

@@ -0,0 +1,35 @@
export interface SurveyStatisticsData {
totalSurveys: number;
totalResponses: number;
completionRate: number;
surveysByMonth: {
month: string;
count: number;
}[];
responsesByAudience: {
name: string;
value: number;
}[];
responseDistribution: {
title: string;
responses: number;
}[];
surveyDetails: SurveyDetail[];
}
export interface SurveyDetail {
id: number;
title: string;
description: string;
totalResponses: number;
targetAudience: string;
createdAt: string;
closingDate?: string;
questionStats: QuestionStat[];
}
export interface QuestionStat {
questionId: string;
label: string;
count: number;
}