Skip to content

Commit 6ee92a1

Browse files
committedMay 20, 2025·
feature: Global Context vars
1 parent a903a56 commit 6ee92a1

20 files changed

+561
-185
lines changed
 

‎.github/workflows/pr-validate-branch.yaml

Whitespace-only changes.

‎src/components/common/SquareButton.jsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const SquareButton = ({
55
children,
66
helperLabel,
77
colorType = "white", // "accent" o "white"
8+
iconColor,
9+
hoverColor,
810
...props
911
}) => {
1012
const [showHelper, setShowHelper] = useState(false);
@@ -17,18 +19,18 @@ const SquareButton = ({
1719

1820
// Definición de estilos según colorType y hover
1921
let buttonBgClass = "";
20-
let iconColor = "";
22+
let iconColorClass = iconColor || "";
2123
if (colorType === "accent") {
2224
buttonBgClass = hover ? "bg-teal-300" : "bg-teal-400";
23-
iconColor = "text-white";
25+
iconColorClass = iconColor || "text-white";
2426
} else { // white
2527
buttonBgClass = hover ? "bg-white" : "bg-white";
26-
iconColor = hover ? "text-teal-300" : "text-teal-400";
28+
iconColorClass = iconColor || (hover ? "text-teal-300" : "text-teal-400");
2729
}
2830

2931
// Si el hijo es un icono, pásale la clase de color
3032
const childWithColor = React.isValidElement(children)
31-
? React.cloneElement(children, { className: `w-6 h-6 ${iconColor}` })
33+
? React.cloneElement(children, { className: `w-6 h-6 ${iconColorClass}` })
3234
: children;
3335

3436
return (
@@ -38,7 +40,7 @@ const SquareButton = ({
3840
onMouseEnter={helperLabel ? handleMouseEnter : undefined}
3941
onMouseLeave={helperLabel ? handleMouseLeave : undefined}
4042
onMouseMove={helperLabel ? handleMouseMove : undefined}
41-
className={`rounded-md ${buttonBgClass} p-2 shadow transition ${props.className || ""}`}
43+
className={`rounded-md ${buttonBgClass} p-2 shadow transition ${hoverColor || ""} ${props.className || ""}`}
4244
>
4345
{childWithColor}
4446
</button>

‎src/components/form/Form.jsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ function Form({ formFields, onSubmit, title, submitLabel, initialValues }) {
66
formFields.reduce(
77
(acc, field) => ({
88
...acc,
9-
[field.name]:
10-
(initialValues && initialValues[field.name] !== undefined)
11-
? initialValues[field.name]
9+
[field.field]:
10+
(initialValues && initialValues[field.field] !== undefined)
11+
? initialValues[field.field]
1212
: field.default_option || "",
1313
}),
1414
{}
@@ -31,13 +31,13 @@ function Form({ formFields, onSubmit, title, submitLabel, initialValues }) {
3131
const validate = () => {
3232
const newErrors = {};
3333
formFields.forEach((field) => {
34-
const value = formData[field.name];
34+
const value = formData[field.field];
3535
if (field.mandatory && !value) {
36-
newErrors[field.name] = `${field.label} es obligatorio.`;
36+
newErrors[field.field] = `${field.label} es obligatorio.`;
3737
} else if (field.type === "numeric" && isNaN(value)) {
38-
newErrors[field.name] = `${field.label} debe ser un número.`;
38+
newErrors[field.field] = `${field.label} debe ser un número.`;
3939
} else if (field.type === "string" && field.maxLength && value.length > field.maxLength) {
40-
newErrors[field.name] = `${field.label} no puede tener más de ${field.maxLength} caracteres.`;
40+
newErrors[field.field] = `${field.label} no puede tener más de ${field.maxLength} caracteres.`;
4141
}
4242
});
4343
return newErrors;
@@ -60,11 +60,11 @@ function Form({ formFields, onSubmit, title, submitLabel, initialValues }) {
6060
<div className="flex flex-wrap -mx-2">
6161
{formFields.map((field) => (
6262
<FormField
63-
key={field.name}
63+
key={field.field}
6464
field={field}
65-
value={formData[field.name]}
65+
value={formData[field.field]}
6666
onChange={handleChange}
67-
error={errors[field.name]}
67+
error={errors[field.field]}
6868
/>
6969
))}
7070
</div>

‎src/components/form/FormField.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22

33
function FormField({ field, value, onChange, error }) {
4-
const { name, label, type, options, maxLength, placeholder, width, default_option } = field;
4+
const { field: fieldName, label, type, options, maxLength, placeholder, width, default_option } = field;
55

66
// Determina la clase de ancho según el valor de `width`
77
const widthClass = width === "half" ? "w-1/2" : "w-full";
@@ -12,7 +12,7 @@ function FormField({ field, value, onChange, error }) {
1212
{label}
1313
{type === "select" ? (
1414
<select
15-
name={name}
15+
name={fieldName}
1616
value={value}
1717
onChange={onChange}
1818
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-teal-500 focus:border-teal-500 sm:text-sm font-normal placeholder-gray-400"
@@ -29,7 +29,7 @@ function FormField({ field, value, onChange, error }) {
2929
) : (
3030
<input
3131
type={type === "numeric" ? "number" : "text"}
32-
name={name}
32+
name={fieldName}
3333
value={value}
3434
onChange={onChange}
3535
maxLength={type === "string" ? maxLength : undefined}

‎src/components/layout/EditModalCard.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React from "react";
22
import WindowOverlay from "../common/WindowOverlay";
33
import Form from "../form/Form";
4+
import { useAppConfig } from '../../config/AppConfigContext';
45

5-
function EditModalCard({ isOpen, onClose, onSubmit, formFields, rowData }) {
6+
function EditModalCard({ isOpen, onClose, onSubmit, rowData }) {
7+
const { formFields } = useAppConfig();
68
if (!isOpen) return null;
79

810
const handleFormSubmit = (formData) => {

‎src/components/layout/MainContent.jsx

Lines changed: 6 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -6,112 +6,20 @@ import PrimaryButton from '../common/PrimaryButton';
66
import RoundedButton from '../common/RoundedButton';
77
import SquareButton from '../common/SquareButton';
88
import CallToAction from './CallToAction';
9-
import DataTable from '../table/Table';
9+
import DataTable from '../table/DataTable';
1010
import ModalCard from './ModalCard';
1111
import EditModalCard from './EditModalCard';
1212
import ColumnsWeight from '../ColumnsWeight/ColumnsWeight';
1313

14+
import { useAppConfig } from '../../config/AppConfigContext';
1415
import IconAddRow from '../../assets/IconAddRow';
1516
import IconEnableColumns from '../../assets/IconEnableColumns';
1617
import { usePersistentState } from "../../hooks/usePersistentState";
17-
18-
const columns = [
19-
{ field: "id", weight: 0, visible:false, label: "ID", width: 50, highlight: false, sortable: false, align: "left" },
20-
{ field: "link", weight: 1, label: "Link", width: 200, highlight: false, sortable: false, align: "center" },
21-
{ field: "emoji", weight: 1, label: "❤️", width: 60, highlight: false, sortable: false, align: "center" },
22-
{ field: "kpi", weight: 1, label: "K-Pick", width: 100, highlight: true, sortable: true, align: "center" },
23-
{ field: "precio", weight: 1, label: "Precio", width: 100, highlight: false, sortable: true, align: "center" },
24-
{ field: "superficie", weight: 1, label: "Superficie", width: 80, highlight: false, sortable: true, align: "center" },
25-
{ field: "eurom2", weight: 1, label: "€/m²", width: 90, highlight: false, sortable: true, align: "center" },
26-
{ field: "habitaciones", weight: 1, label: "Habitaciones", width: 90, highlight: false, sortable: false, align: "center" },
27-
{ field: "baños", weight: 1, label: "Baños", width: 70, highlight: false, sortable: false, align: "center" },
28-
{ field: "planta", weight: 1, label: "Planta", width: 70, highlight: false, sortable: false, align: "center" },
29-
{ field: "ascensor", weight: 1, label: "Ascensor", width: 80, highlight: false, sortable: false, align: "center" },
30-
{ field: "calefaccion", weight: 1, label: "Calefacción", width: 100, highlight: false, sortable: false, align: "center" },
31-
{ field: "fachada", weight: 1, label: "Fachada", width: 120, highlight: false, sortable: false, align: "center" },
32-
{ field: "garaje", weight: 1, label: "Garaje", width: 120, highlight: false, sortable: false, align: "center" },
33-
{ field: "terraza", weight: 1, label: "Terraza", width: 120, highlight: false, sortable: false, align: "center" },
34-
{ field: "trastero", weight: 0, label: "Trastero", width: 100, highlight: false, sortable: false, align: "center" },
35-
{ field: "ac", weight: 0, label: "Aire acond.", width: 120, highlight: false, sortable: false, align: "center" },
36-
{ field: "año", weight: 0, label: "Año de const.", width: 120, highlight: false, sortable: true, align: "center" },
37-
{ field: "estado", weight: 0, label: "Estado", width: 150, highlight: false, sortable: false, align: "center" },
38-
];
39-
40-
const formFields = [
41-
{ name: "link", label: "Link", type: "string", maxLength: 255, mandatory: false, placeholder: "Añade el link al anuncio de tu vivienda", width: "full" },
42-
{ name: "precio", label: "Precio", type: "numeric", mandatory: true, placeholder: "Introduce el precio", width: "half" },
43-
{ name: "superficie", label: "Superficie", type: "numeric", mandatory: true, placeholder: "Introduce la superficie", width: "half" },
44-
{ name: "habitaciones", label: "Habitaciones", type: "numeric", mandatory: false, placeholder: "Número de habitaciones", width: "half" },
45-
{ name: "baños", label: "Baños", type: "numeric", mandatory: false, placeholder: "Número de baños", width: "half" },
46-
{ name: "planta", label: "Planta", type: "select", options: ["Semisótano", "Baja", "Intermedia", "Ático"], default_option: "Intermedia", mandatory: false, placeholder: "Introduce la planta", width: "half" },
47-
{ name: "ascensor", label: "Ascensor", type: "select", options: ["Sí", "No"], default_option: "Sí", mandatory: false, placeholder: "Seleccione una opción", width: "half" },
48-
{ name: "calefaccion", label: "Calefacción", type: "select", options: ["Sí", "No"], default_option: "Sí", mandatory: false, placeholder: "Seleccione calefacción", width: "half" },
49-
{ name: "fachada", label: "Fachada", type: "select", options: ["Exterior", "Interior"], default_option: "Exterior", mandatory: false, width: "half" },
50-
{ name: "terraza", label: "Terraza", type: "select", options: ["Sí", "No"], default_option: "No", mandatory: false, width: "half" },
51-
{ name: "garaje", label: "Garaje", type: "select", options: ["Sí", "No"], default_option: "No", mandatory: false, width: "half" },
52-
{ name: "trastero", label: "Trastero", type: "select", options: ["Sí", "No"], default_option: "No", mandatory: false, width: "half" },
53-
{ name: "ac", label: "Aire acond.", type: "select", options: ["Sí", "No"], default_option: "No", mandatory: false, width: "half" },
54-
{ name: "año", label: "Año de construcción", type: "numeric", mandatory: false, placeholder: "Año de construcción", width: "half" },
55-
{ name: "estado", label: "Estado", type: "select", options: ["Nueva/reformada", "Buen estado", "Necesita reforma", "Reforma integral"], default_option: "Buen estado", mandatory: false, width: "half" },
56-
];
57-
58-
const initialRows = [
59-
{
60-
id: 1,
61-
link: "https://www.example.com/property/1",
62-
emoji: "",
63-
kpi: 0,
64-
precio: 250000,
65-
superficie: 120,
66-
eurom2: 2083,
67-
habitaciones: 3,
68-
baños: 2,
69-
planta: "Intermedia",
70-
ascensor: "Sí",
71-
calefaccion: "Sí",
72-
fachada: "Exterior",
73-
garaje: "No",
74-
terraza: "Sí",
75-
},
76-
{
77-
id: 2,
78-
link: "https://www.example.com/property/2",
79-
emoji: "",
80-
kpi: 0,
81-
precio: 210000,
82-
superficie: 90,
83-
eurom2: 2333,
84-
habitaciones: 2,
85-
baños: 1,
86-
planta: "Baja",
87-
ascensor: "No",
88-
calefaccion: "No",
89-
fachada: "Interior",
90-
garaje: "Sí",
91-
terraza: "No",
92-
},
93-
{
94-
id: 3,
95-
link: "https://www.example.com/property/3",
96-
emoji: "",
97-
kpi: 0,
98-
precio: 320000,
99-
superficie: 150,
100-
eurom2: 2133,
101-
habitaciones: 3,
102-
baños: 2,
103-
planta: "Ático",
104-
ascensor: "Sí",
105-
calefaccion: "Sí",
106-
fachada: "Exterior",
107-
garaje: "Sí",
108-
terraza: "Sí",
109-
},
110-
];
111-
112-
const API_URL = "https://o9qh2kvujg.execute-api.eu-west-3.amazonaws.com/generate-kpick";
18+
import { initialRows } from '../../config/initialRows';
19+
import { API_URL } from '../../config/api';
11320

11421
function MainContent() {
22+
const { tableColumns, formFields, features } = useAppConfig();
11523
const [isModalOpen, setIsModalOpen] = useState(false);
11624
const [rows, setRows] = usePersistentState("viviendas", initialRows);
11725
const [loading, setLoading] = useState(false);
@@ -124,7 +32,7 @@ function MainContent() {
12432
const [isColumnsWeightOpen, setIsColumnsWeightOpen] = useState(false);
12533

12634
// Nuevo estado para columns
127-
const [columnsState, setColumnsState] = usePersistentState("columnsState", columns);
35+
const [columnsState, setColumnsState] = usePersistentState("columnsState", tableColumns);
12836

12937
// Estado para ordenación
13038
const [sortConfig, setSortConfig] = useState({ field: null, direction: null });
@@ -325,14 +233,12 @@ function MainContent() {
325233
isOpen={isModalOpen}
326234
onClose={() => setIsModalOpen(false)}
327235
onSubmit={handleAddRow}
328-
formFields={formFields}
329236
/>
330237

331238
<EditModalCard
332239
isOpen={editModalOpen}
333240
onClose={() => setEditModalOpen(false)}
334241
onSubmit={handleUpdateRow}
335-
formFields={formFields}
336242
rowData={rowToEdit}
337243
/>
338244
</main>

‎src/components/layout/ModalCard.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React from "react";
22
import WindowOverlay from "../common/WindowOverlay";
33
import Form from "../form/Form";
4+
import { useAppConfig } from '../../config/AppConfigContext';
45

5-
function ModalCard({ isOpen, onClose, onSubmit, formFields }) {
6+
function ModalCard({ isOpen, onClose, onSubmit }) {
7+
const { formFields } = useAppConfig();
68
return (
79
<WindowOverlay isOpen={isOpen} onClose={onClose}>
810
<Form

‎src/components/table/DataGrid.jsx

Lines changed: 0 additions & 20 deletions
This file was deleted.

‎src/components/table/DataTable.jsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from "react";
2+
import TableHeader from "./TableHeader";
3+
import TableBody from "./TableBody";
4+
5+
export default function DataTable({ columns, rows, onRowDelete, onEditRow, onSort, sortConfig, onEmojiChange }) {
6+
return (
7+
<div className="mt-8 mb-8 relative max-w-[96%] w-min overflow-x-auto max-h-[28rem] flex flex-col h-full text-gray-700 bg-white shadow-md rounded-lg bg-clip-border">
8+
<table className="w-full text-left table-auto min-w-max">
9+
<TableHeader columns={columns} onSort={onSort} sortConfig={sortConfig} />
10+
<TableBody
11+
rows={rows}
12+
columns={columns}
13+
onRowDelete={onRowDelete}
14+
onEditRow={onEditRow}
15+
onEmojiChange={onEmojiChange}
16+
/>
17+
</table>
18+
</div>
19+
);
20+
}

‎src/components/table/Table.jsx

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +0,0 @@
1-
import React from "react";
2-
import DataGrid from "./DataGrid";
3-
4-
export default function Table({ columns, rows, onRowDelete, onEditRow, onSort, sortConfig, onEmojiChange }) {
5-
return (
6-
<div className="mt-8 mb-8 relative max-w-[96%] w-min overflow-x-auto max-h-[28rem] flex flex-col h-full text-gray-700 bg-white shadow-md rounded-lg bg-clip-border">
7-
<DataGrid
8-
columns={columns}
9-
rows={rows}
10-
onRowDelete={onRowDelete}
11-
onEditRow={onEditRow}
12-
onSort={onSort}
13-
sortConfig={sortConfig}
14-
onEmojiChange={onEmojiChange}
15-
/>
16-
</div>
17-
);
18-
}

‎src/components/table/TableHeader.jsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,12 @@ function TableHeader({ columns, onSort, sortConfig }) {
1616
<tr>
1717
{columns
1818
.filter(col => col.weight !== 0)
19-
.map(({ field, label, sortable, align }) => (
19+
.map(({ field, label, sortable }) => (
2020
<th
2121
key={field}
22-
className={`p-2 py-4 text-sm font-medium leading-none text-slate-500 bg-slate-100 min-w-max whitespace-nowrap ${
23-
align === "center" ? "text-center" : align === "right" ? "text-right" : "text-left"
24-
}`}
22+
className="p-2 py-4 text-sm font-medium leading-none text-slate-500 bg-slate-100 min-w-max whitespace-nowrap text-center"
2523
>
26-
<div
27-
className={`inline-flex items-center ${
28-
align === "center" ? "justify-center" : align === "right" ? "justify-end" : "justify-start"
29-
}`}
30-
>
24+
<div className="inline-flex items-center justify-center">
3125
<span>
3226
{label ?? field}
3327
</span>

‎src/components/table/TableRow.jsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ function TableRow({ row, columns, onRowDelete, onEditRow, onEmojiChange }) {
1111
<tr className="group hover:bg-teal-50 transition-colors">
1212
{columns
1313
.filter(({ weight }) => weight !== 0)
14-
.map(({ field, highlight, align }) => (
14+
.map(({ field, highlight }) => (
1515
<td
1616
key={field}
17-
className={`p-2 text-sm text-slate-700 min-h-[2.5rem]
18-
${highlight ? "bg-teal-50 text-teal-400 font-bold" : "bg-white group-hover:bg-teal-50"}
19-
${align === "center" ? "text-center" : align === "right" ? "text-right" : "text-left"}`}
17+
className={`p-2 text-sm text-slate-700 min-h-[2.5rem] text-center
18+
${highlight ? "bg-teal-50 text-teal-400 font-bold" : "bg-white group-hover:bg-teal-50"}`}
2019
>
2120
{field === "link" ? (
2221
<IconLinkButton link={row[field]} labelHelper="Abrir link del anuncio">
@@ -28,19 +27,19 @@ function TableRow({ row, columns, onRowDelete, onEditRow, onEmojiChange }) {
2827
onChange={emoji => onEmojiChange(row.id, emoji)}
2928
/>
3029
) : field === "precio" ? (
31-
<span className="font-semibold text-slate-500">
30+
<span className="font-semibold text-slate-500 text-center block">
3231
{Number(row[field]).toLocaleString("es-ES")}
3332
</span>
3433
) : field === "superficie" ? (
35-
<span className="font-semibold text-slate-500">
34+
<span className="font-semibold text-slate-500 text-center block">
3635
{Number(row[field]).toLocaleString("es-ES")}
3736
</span>
3837
) : field === "eurom2" ? (
39-
<span className="font-semibold text-slate-500">
38+
<span className="font-semibold text-slate-500 text-center block">
4039
{row[field] ? `${Number(row[field]).toLocaleString("es-ES")} €/m²` : ""}
4140
</span>
4241
) : (
43-
row[field]
42+
<span className="text-center block">{row[field]}</span>
4443
)}
4544
</td>
4645
))}

‎src/config/AppConfigContext.jsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// src/config/AppConfigContext.jsx
2+
import React, { createContext, useContext } from 'react';
3+
import { tableColumns } from './configTable';
4+
import { formFields } from './configForm';
5+
import { features } from './appData';
6+
7+
export const AppConfigContext = createContext({
8+
tableColumns,
9+
formFields,
10+
features,
11+
});
12+
13+
export const AppConfigProvider = ({ children }) => (
14+
<AppConfigContext.Provider value={{ tableColumns, formFields, features }}>
15+
{children}
16+
</AppConfigContext.Provider>
17+
);
18+
19+
export const useAppConfig = () => useContext(AppConfigContext);

‎src/config/api.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// src/config/api.js
2+
export const API_URL = "https://o9qh2kvujg.execute-api.eu-west-3.amazonaws.com/generate-kpick";

‎src/config/appData.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// src/config/appData.js
2+
export const features = [
3+
{
4+
field: "id",
5+
isKpick: false,
6+
},
7+
{
8+
field: "link",
9+
isKpick: false,
10+
weight: 1,
11+
label: "Link",
12+
dataType: "numeric",
13+
ascending: true
14+
},
15+
{
16+
field: "precio",
17+
isKpick: true,
18+
weight: 1,
19+
label: "Precio",
20+
dataType: "numeric",
21+
ascending: true
22+
},
23+
{
24+
field: "superficie",
25+
isKpick: true,
26+
weight: 1,
27+
label: "Superficie",
28+
dataType: "numeric",
29+
ascending: true
30+
},
31+
{
32+
field: "eurom2",
33+
isKpick: true,
34+
weight: 1,
35+
label: "€/m²",
36+
width: 90,
37+
highlight: false,
38+
sortable: true,
39+
align: "center"
40+
},
41+
{
42+
field: "habitaciones",
43+
isKpick: true,
44+
weight: 1,
45+
label: "Habitaciones",
46+
dataType: "numeric",
47+
ascending: true
48+
},
49+
{
50+
field: "baños",
51+
isKpick: true,
52+
weight: 1,
53+
label: "Baños",
54+
dataType: "numeric",
55+
ascending: true
56+
},
57+
{
58+
field: "planta",
59+
isKpick: true,
60+
weight: 1,
61+
label: "Planta",
62+
dataType: "category",
63+
options: ["Ático", "Intermedia", "Baja", "Semisótano"],
64+
ascending: true
65+
},
66+
{
67+
field: "ascensor",
68+
isKpick: true,
69+
weight: 1,
70+
label: "Ascensor",
71+
dataType: "category",
72+
options: ["Sí", "No"],
73+
ascending: true
74+
},
75+
{
76+
field: "calefaccion",
77+
isKpick: true,
78+
weight: 1,
79+
label: "Calefacción",
80+
dataType: "category",
81+
options: ["Sí", "No"],
82+
ascending: true
83+
},
84+
{
85+
field: "fachada",
86+
isKpick: true,
87+
weight: 1,
88+
label: "Fachada",
89+
dataType: "category",
90+
options: ["Exterior", "Interior"],
91+
ascending: true
92+
},
93+
{
94+
field: "terraza",
95+
isKpick: true,
96+
weight: 1,
97+
label: "Terraza",
98+
dataType: "category",
99+
options: ["Sí", "No"],
100+
ascending: true
101+
},
102+
{
103+
field: "garaje",
104+
isKpick: true,
105+
weight: 1,
106+
label: "Garaje",
107+
dataType: "category",
108+
options: ["Sí", "No"],
109+
ascending: true
110+
},
111+
{
112+
field: "trastero",
113+
isKpick: true,
114+
weight: 0,
115+
label: "Trastero",
116+
dataType: "category",
117+
options: ["Sí", "No"],
118+
ascending: true
119+
},
120+
{
121+
field: "ac",
122+
isKpick: true,
123+
weight: 0,
124+
label: "Aire acond.",
125+
dataType: "category",
126+
options: ["Sí", "No"],
127+
ascending: true
128+
},
129+
{
130+
field: "año",
131+
isKpick: true,
132+
weight: 0,
133+
label: "Año de construcción",
134+
dataType: "numeric",
135+
options: [],
136+
ascending: true
137+
},
138+
{
139+
field: "estado",
140+
isKpick: true,
141+
weight: 0,
142+
label: "Estado",
143+
dataType: "category",
144+
options: ["Nueva/Reformada", "Buen estado", "Necesita reforma", "Reforma integral"],
145+
ascending: true
146+
}
147+
];

‎src/config/configForm.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { features } from './appData';
2+
3+
// Campos del formulario sin label, type ni options
4+
const baseFormFields = [
5+
{
6+
field: "link",
7+
type: "string",
8+
maxLength: 255,
9+
mandatory: false,
10+
placeholder: "Añade el link al anuncio de tu vivienda",
11+
width: "full"
12+
},
13+
{
14+
field: "precio",
15+
type: "numeric",
16+
mandatory: true,
17+
placeholder: "Introduce el precio",
18+
width: "half"
19+
},
20+
{
21+
field: "superficie",
22+
type: "numeric",
23+
mandatory: true,
24+
placeholder: "Introduce la superficie",
25+
width: "half"
26+
},
27+
{
28+
field: "habitaciones",
29+
type: "numeric",
30+
mandatory: false,
31+
placeholder: "Número de habitaciones",
32+
width: "half"
33+
},
34+
{
35+
field: "baños",
36+
type: "numeric",
37+
mandatory: false,
38+
placeholder: "Número de baños",
39+
width: "half"
40+
},
41+
{
42+
field: "planta",
43+
type: "select",
44+
default_option: "Intermedia",
45+
mandatory: false,
46+
placeholder: "Introduce la planta",
47+
width: "half"
48+
},
49+
{
50+
field: "ascensor",
51+
type: "select",
52+
default_option: "Sí",
53+
mandatory: false,
54+
placeholder: "Seleccione una opción",
55+
width: "half"
56+
},
57+
{
58+
field: "calefaccion",
59+
type: "select",
60+
default_option: "Sí",
61+
mandatory: false,
62+
placeholder: "Seleccione calefacción",
63+
width: "half"
64+
},
65+
{
66+
field: "fachada",
67+
type: "select",
68+
default_option: "Exterior",
69+
mandatory: false,
70+
width: "half"
71+
},
72+
{
73+
field: "terraza",
74+
type: "select",
75+
default_option: "No",
76+
mandatory: false,
77+
width: "half"
78+
},
79+
{
80+
field: "garaje",
81+
type: "select",
82+
default_option: "No",
83+
mandatory: false,
84+
width: "half"
85+
}
86+
];
87+
88+
// Mergea label, type y options desde features si existen
89+
function mergeWithFeatures(field) {
90+
const feature = features.find(f => f.field === field.field);
91+
if (feature) {
92+
return {
93+
...field,
94+
label: feature.label,
95+
type: feature.type,
96+
options: feature.options
97+
};
98+
}
99+
return field;
100+
}
101+
102+
export const formFields = baseFormFields.map(mergeWithFeatures);

‎src/config/configTable.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// src/config/configTable.js
2+
import { features } from './appData';
3+
4+
// Columnas base sin label, type ni options
5+
const baseTableColumns = [
6+
{
7+
field: "id",
8+
weight: 0,
9+
visible: false,
10+
},
11+
{
12+
field: "link",
13+
weight: 1,
14+
visible: true,
15+
highlight: false,
16+
sortable: false
17+
},
18+
{
19+
field: "emoji",
20+
weight: 1,
21+
visible: true,
22+
highlight: false,
23+
sortable: false
24+
},
25+
{
26+
field: "kpi",
27+
weight: 1,
28+
visible: true,
29+
highlight: true,
30+
sortable: true
31+
},
32+
{
33+
field: "precio",
34+
weight: 1,
35+
visible: true,
36+
highlight: false,
37+
sortable: true
38+
},
39+
{
40+
field: "superficie",
41+
weight: 1,
42+
visible: true,
43+
highlight: false,
44+
sortable: true
45+
},
46+
{
47+
field: "eurom2",
48+
weight: 1,
49+
visible: true,
50+
highlight: false,
51+
sortable: true
52+
},
53+
{
54+
field: "habitaciones",
55+
weight: 1,
56+
visible: true,
57+
highlight: false,
58+
sortable: false
59+
},
60+
{
61+
field: "baños",
62+
weight: 1,
63+
visible: true,
64+
highlight: false,
65+
sortable: false
66+
},
67+
{
68+
field: "planta",
69+
weight: 1,
70+
visible: true,
71+
highlight: false,
72+
sortable: false
73+
},
74+
{
75+
field: "ascensor",
76+
weight: 1,
77+
visible: true,
78+
highlight: false,
79+
sortable: false
80+
},
81+
{
82+
field: "calefaccion",
83+
weight: 1,
84+
visible: true,
85+
highlight: false,
86+
sortable: false
87+
},
88+
{
89+
field: "fachada",
90+
weight: 1,
91+
visible: true,
92+
highlight: false,
93+
sortable: false
94+
},
95+
{
96+
field: "garaje",
97+
weight: 1,
98+
visible: true,
99+
highlight: false,
100+
sortable: false
101+
},
102+
{
103+
field: "terraza",
104+
weight: 1,
105+
visible: true,
106+
highlight: false,
107+
sortable: false
108+
},
109+
{
110+
field: "trastero",
111+
weight: 0,
112+
visible: true,
113+
highlight: false,
114+
sortable: false
115+
},
116+
{
117+
field: "ac",
118+
weight: 0,
119+
visible: true,
120+
highlight: false,
121+
sortable: false
122+
},
123+
{
124+
field: "año",
125+
weight: 0,
126+
visible: true,
127+
highlight: false,
128+
sortable: true
129+
},
130+
{
131+
field: "estado",
132+
weight: 0,
133+
visible: true,
134+
highlight: false,
135+
sortable: false
136+
}
137+
];
138+
139+
function mergeWithFeatures(col) {
140+
const feature = features.find(f => f.field === col.field);
141+
if (feature) {
142+
return {
143+
...col,
144+
label: feature.label
145+
};
146+
}
147+
return col;
148+
}
149+
150+
export const tableColumns = baseTableColumns.map(mergeWithFeatures);

‎src/config/initialRows.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// src/config/initialRows.js
2+
// Datos de ejemplo iniciales para la tabla de viviendas
3+
4+
export const initialRows = [
5+
{
6+
id: 1,
7+
link: "https://www.example.com/property/1",
8+
emoji: "",
9+
kpi: 0,
10+
precio: 250000,
11+
superficie: 120,
12+
eurom2: 2083,
13+
habitaciones: 3,
14+
baños: 2,
15+
planta: "Intermedia",
16+
ascensor: "Sí",
17+
calefaccion: "Sí",
18+
fachada: "Exterior",
19+
garaje: "No",
20+
terraza: "Sí",
21+
},
22+
{
23+
id: 2,
24+
link: "https://www.example.com/property/2",
25+
emoji: "",
26+
kpi: 0,
27+
precio: 210000,
28+
superficie: 90,
29+
eurom2: 2333,
30+
habitaciones: 2,
31+
baños: 1,
32+
planta: "Baja",
33+
ascensor: "No",
34+
calefaccion: "No",
35+
fachada: "Interior",
36+
garaje: "Sí",
37+
terraza: "No",
38+
},
39+
{
40+
id: 3,
41+
link: "https://www.example.com/property/3",
42+
emoji: "",
43+
kpi: 0,
44+
precio: 320000,
45+
superficie: 150,
46+
eurom2: 2133,
47+
habitaciones: 3,
48+
baños: 2,
49+
planta: "Ático",
50+
ascensor: "Sí",
51+
calefaccion: "Sí",
52+
fachada: "Exterior",
53+
garaje: "Sí",
54+
terraza: "Sí",
55+
},
56+
];

‎src/main.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
22
import { createRoot } from 'react-dom/client'
33
import './input.css'
44
import App from './App.jsx'
5+
import { AppConfigProvider } from './config/AppConfigContext.jsx'
56

67
createRoot(document.getElementById('root')).render(
78
<StrictMode>
8-
<App />
9+
<AppConfigProvider>
10+
<App />
11+
</AppConfigProvider>
912
</StrictMode>,
1013
)

‎src_lambda/kpick_generator.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@ def lambda_handler(event, context):
2222

2323
rows = data.get("rows", [])
2424
columns = data.get("columns", [])
25+
logger.debug(f"Datos recibidos: {rows}, {columns}")
2526

26-
# Determina qué campos están activos
27-
active_fields = set(col["field"] for col in columns if col.get("active", False))
27+
# Determina qué campos están activos (ahora solo los que tienen weight > 0)
28+
active_fields = set(col["field"] for col in columns if col.get("weight", 0) > 0)
29+
30+
# Crear mapa de pesos por campo
31+
weight_map = {col["field"]: col.get("weight", 1) for col in columns}
2832

2933
# Convertir campos numéricos para evitar errores
3034
for row in rows:
@@ -65,13 +69,19 @@ def lambda_handler(event, context):
6569
]
6670

6771
for row in rows:
68-
# Solo incluye factores cuyo campo esté activo
69-
factors = [
70-
func(row)
71-
for field, func in factor_definitions
72-
if field in active_fields
73-
]
74-
kpi = sum(factors) / len(factors) * 100 if factors else 0
72+
weighted_factors = []
73+
total_weight = 0
74+
for field, func in factor_definitions:
75+
if field in active_fields:
76+
weight = weight_map.get(field, 1)
77+
if weight > 0:
78+
factor_value = func(row)
79+
weighted_factors.append(factor_value * weight)
80+
total_weight += weight
81+
if total_weight > 0:
82+
kpi = sum(weighted_factors) / total_weight * 100
83+
else:
84+
kpi = 0
7585
row["kpi"] = int(kpi)
7686
logger.info(row)
7787

0 commit comments

Comments
 (0)
Please sign in to comment.