Skip to content

Commit a37bbae

Browse files
committed
Add CSV download functionality for challenges with stats by dimension and subdivision
1 parent 9dc71af commit a37bbae

File tree

3 files changed

+233
-23
lines changed

3 files changed

+233
-23
lines changed

controllers/challengeController.js

+169-22
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const UtilsHelper = require('../helpers/utilsHelper');
44
const ChallengeHelper = require('../helpers/challengeHelper');
55
const RecaptchaHelper = require('../helpers/recaptchaHelper');
66
const { Op } = require('sequelize');
7+
const dayjs = require('dayjs');
8+
// json2csv
9+
const { Parser } = require('json2csv');
710

811
exports.fetch = async (req, res) => {
912
try {
@@ -356,6 +359,9 @@ exports.statsChartCountByDimension = async (req, res) => {
356359
for (const city of cities) {
357360
const cityData = {
358361
name: city.name,
362+
label: {
363+
show: true,
364+
},
359365
value: dimensions.map(
360366
(dimension) => challengeCounts[city.id]?.[dimension.id] || 0
361367
),
@@ -370,44 +376,185 @@ exports.statsChartCountByDimension = async (req, res) => {
370376
}
371377
};
372378

379+
380+
exports.statsCountByDimensionBar = async (req, res) => {
381+
try {
382+
const cityId = req.params.cityId || null;
383+
// Initialize radar data structure
384+
385+
const results = await ChallengeHelper.getChallengesStatsByDimensionBar();
386+
387+
const outputJson = JSON.parse(JSON.stringify(results));
388+
389+
// convert percentage to float
390+
for (let i = 0; i < outputJson.length; i++) {
391+
outputJson[i].percentage = parseFloat(outputJson[i].percentage);
392+
}
393+
394+
return res.status(200).json(outputJson);
395+
} catch (error) {
396+
console.error(error);
397+
return res.status(500).json({ message: msg.error.default });
398+
}
399+
};
400+
373401
exports.statsChartCountBySubdivision = async (req, res) => {
374402
try {
375403
const cityId = req.params.cityId || null;
376404
const countBySubdivisions = [];
377405

378406
// count how many challenges per subdivision per city
379407
// challenge.subdivision.id , challenge.subdivision.name, challenge.subdivision.city.id, count
380-
const countOfChallengesPerSubdivision = await models.Challenge.findAll({
381-
attributes: [
382-
[models.sequelize.col("subdivision.id"), "subdivisionId"],
383-
[models.sequelize.col("subdivision.name"), "subdivisionName"],
384-
[models.sequelize.col("subdivision.type"), "subdivisionType"],
385-
[models.sequelize.col("subdivision.city.id"), "cityId"],
386-
[models.sequelize.col("subdivision.city.name"), "cityName"],
387-
[models.sequelize.fn("COUNT", "subdivisionId"), "count"],
388-
],
389-
group: ["subdivisionId"],
408+
// const countOfChallengesPerSubdivision = await models.Challenge.findAll({
409+
// attributes: [
410+
// [models.sequelize.col("subdivision.id"), "subdivisionId"],
411+
// [models.sequelize.col("subdivision.name"), "subdivisionName"],
412+
// [models.sequelize.col("subdivision.type"), "subdivisionType"],
413+
// [models.sequelize.col("subdivision.city.id"), "cityId"],
414+
// [models.sequelize.col("subdivision.city.name"), "cityName"],
415+
// [models.sequelize.fn("COUNT", "subdivisionId"), "count"],
416+
// ],
417+
// group: ["subdivisionId"],
418+
// include: [
419+
// {
420+
// model: models.Subdivision,
421+
// as: "subdivision",
422+
// where: cityId ? { cityId } : {},
423+
// attributes: [],
424+
// include: [
425+
// {
426+
// model: models.City,
427+
// as: "city",
428+
// where: cityId ? { id: cityId } : {},
429+
// attributes: [],
430+
// },
431+
// ],
432+
// },
433+
// ],
434+
// });
435+
436+
const countOfChallengesPerSubdivision = await ChallengeHelper.getChallengesCountBySubdivision(cityId);
437+
438+
return res.status(200).json(countOfChallengesPerSubdivision);
439+
} catch (error) {
440+
console.error(error);
441+
return res.status(500).json({ message: msg.error.default });
442+
}
443+
};
444+
445+
exports.downloadChallengesCsv = async (req, res) => {
446+
try {
447+
448+
const recaptchaResponse = req.body.recaptchaResponse;
449+
450+
// if the user is an admin, we don't need to validate the recaptcha
451+
if(RecaptchaHelper.requiresRecaptcha(req.user)) {
452+
// validate the recaptcha
453+
const recaptchaValidation = await RecaptchaHelper.verifyRecaptcha(recaptchaResponse);
454+
if(!recaptchaValidation) {
455+
return res.status(400).json({ message: 'Error en la validación del recaptcha' });
456+
}
457+
}
458+
459+
const challenges = await models.Challenge.findAll({
390460
include: [
391461
{
392462
model: models.Subdivision,
393-
as: "subdivision",
394-
where: cityId ? { cityId } : {},
395-
attributes: [],
463+
as: 'subdivision',
464+
attributes: ['id', 'type', 'name'],
396465
include: [
397466
{
398467
model: models.City,
399-
as: "city",
400-
where: cityId ? { id: cityId } : {},
401-
attributes: [],
402-
},
403-
],
468+
as: 'city',
469+
attributes: ['id','name'],
470+
}
471+
]
472+
},
473+
{
474+
model: models.Dimension,
475+
as: 'dimension',
476+
attributes: ['id', 'name'],
404477
},
405478
],
406479
});
407480

408-
return res.status(200).json(countOfChallengesPerSubdivision);
481+
const fields = [
482+
{
483+
label: 'reporteId',
484+
value: 'id',
485+
},
486+
{
487+
label: 'fuente',
488+
value: 'source',
489+
},
490+
{
491+
label: 'fechaCreacion',
492+
value: (row) => dayjs(row.createdAt).toISOString(),
493+
},
494+
{
495+
label: 'ciudadId',
496+
value: 'subdivision.city.id',
497+
},
498+
{
499+
label: 'ciudadNombre',
500+
value: 'subdivision.city.name',
501+
},
502+
{
503+
label: 'subdivisionId',
504+
value: 'subdivision.id'
505+
},
506+
{
507+
label: 'subdivisionNombre',
508+
value: 'subdivision.name',
509+
},
510+
{
511+
label: 'subdivisionTipo',
512+
value: 'subdivision.type',
513+
},
514+
{
515+
label: 'ejeTematicoId',
516+
value: 'dimension.id',
517+
},
518+
{
519+
label: 'ejeTematicoNombre',
520+
value: 'dimension.name',
521+
},
522+
{
523+
label: 'latitude',
524+
value: (row) => row.latitude ? row.latitude.toString() : null
525+
},
526+
{
527+
label: 'longitude',
528+
value: (row) => row.longitude ? row.longitude.toString() : null
529+
},
530+
{
531+
label: 'necesidadesYDesafios',
532+
value: 'needsAndChallenges',
533+
},
534+
{
535+
label: 'propuesta',
536+
value: 'proposal'
537+
},
538+
{
539+
label: 'enPalabras',
540+
value: 'inWords',
541+
},
542+
]
543+
544+
const opts = { fields, defaultValue: 'NULL' };
545+
546+
const parser = new Parser(opts);
547+
548+
const csv = parser.parse(challenges);
549+
550+
const timestamp = dayjs().format('YYYYMMDD_HHmmss');
551+
const filename = `${timestamp}_desafios_export.csv`;
552+
553+
res.setHeader('Content-disposition', `attachment; filename=${filename}`);
554+
res.set('Content-Type', 'text/csv');
555+
res.status(200).send(csv);
409556
} catch (error) {
410-
console.error(error);
411-
return res.status(500).json({ message: msg.error.default });
557+
console.error(error)
558+
return res.status(500).json({ message: msg.error.default })
412559
}
413-
};
560+
}

helpers/challengeHelper.js

+51-1
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,54 @@ exports.getIdsWithoutFilteringByDimensions = async (challengeName = null, cityId
9292
} catch (error) {
9393
throw error
9494
}
95-
}
95+
}
96+
97+
exports.getChallengesStatsByDimensionBar = async () => {
98+
try {
99+
let sqlQuery = `
100+
SELECT
101+
c.dimensionId,
102+
c2.id as 'cityId',
103+
c2.name as 'cityName',
104+
d.name as 'dimensionName',
105+
COUNT(c.dimensionId) as 'value',
106+
(COUNT(c.dimensionId) * 100.0 / SUM(COUNT(c.dimensionId)) OVER (PARTITION BY c2.id)) AS 'percentage'
107+
FROM Challenges AS c
108+
LEFT JOIN Dimensions AS d ON c.dimensionId = d.id
109+
LEFT JOIN Subdivisions AS s ON c.subdivisionId = s.id
110+
LEFT JOIN Cities AS c2 ON s.cityId = c2.id
111+
GROUP BY c.dimensionId, c2.id
112+
ORDER BY c2.id ASC, c.dimensionId ASC
113+
`;
114+
const results = await models.sequelize.query(sqlQuery, {
115+
replacements: {
116+
},
117+
type: QueryTypes.SELECT,
118+
});
119+
120+
return results
121+
} catch (error) {
122+
throw error
123+
}
124+
}
125+
126+
exports.getChallengesCountBySubdivision = async (cityId = null) => {
127+
try {
128+
let sqlQuery = `
129+
SELECT s.id AS "subdivisionId", s.name AS "subdivisionName", s.type as "subdivisionType", c.id AS "cityId", c.name AS "cityName", COUNT(c2.id) AS "count"
130+
FROM Subdivisions s
131+
LEFT JOIN Challenges c2 ON c2.subdivisionId = s.id
132+
LEFT JOIN Cities c ON s.cityId = c.id
133+
WHERE c.id = :cityId
134+
GROUP BY s.id
135+
`;
136+
const results = await models.sequelize.query(sqlQuery, {
137+
replacements: { cityId },
138+
type: QueryTypes.SELECT,
139+
});
140+
141+
return results
142+
} catch (error) {
143+
throw error
144+
}
145+
}

routes/challenge.js

+13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const router = express.Router();
1616
// -----------------------------------------------
1717
// GET /
1818
// POST /
19+
// PÓST /csv
1920
// GET /:id
2021
// DELETE /:id
2122
// GET /stats/chart/count-by-subdivision/:cityId?
@@ -48,6 +49,14 @@ router.post('/',
4849
ChallengeController.create
4950
);
5051

52+
router.post('/csv',
53+
[
54+
check('recaptchaResponse').isString().withMessage(msg.validationError.string),
55+
],
56+
validate,
57+
ChallengeController.downloadChallengesCsv
58+
);
59+
5160
router.get('/list/geolocalized',
5261
ChallengeController.fetchAllGeolocalized
5362
);
@@ -96,6 +105,10 @@ router.get('/stats/chart/count-by-dimension',
96105
ChallengeController.statsChartCountByDimension
97106
);
98107

108+
router.get('/stats/bar/count-by-dimension',
109+
ChallengeController.statsCountByDimensionBar
110+
);
111+
99112
// -----------------------------------------------
100113

101114
module.exports = router;

0 commit comments

Comments
 (0)