medoficaciones de front end y base de datoss
Daniel Silva committed
Aug 27, 2026 at 19:31 UTC
aedd43b14237280719e33b0ba5fb79d361fce360
24 files changed
+1754
-233
frontend/README.md
+3
-3
@@ -29,11 +29,11 @@ frontend/
29
|----------|------|-----|
30
| Inicio | `pages/inicio.html` | `js/pages/inicio.js` |
31
| Perfil | `pages/perfil.html` | `js/pages/perfil.js` |
32
-| Foro | `pages/foro.html` | `js/pages/foro.js` |
33
-| Tips | `pages/tips.html` | `js/pages/tips.js` |
34
-| Apuntes | `pages/recursos.html` | `js/pages/recursos.js` |
32
+| Comunidad y Recursos | `pages/comunidad.html` | `js/pages/comunidad.js` |
33
| Mensajes | `pages/mensajes.html` | `js/pages/mensajes.js` |
34
| Empresarial | `pages/empresarial.html` | `js/pages/empresarial.js` |
35
| Círculos | `pages/circulos.html` | `js/pages/circulos.js` |
36
37
+> "Comunidad y Recursos" integra en pestañas Foro de Dudas, Repositorio de Apuntes y Tips Académicos. Cada pestaña solo renderiza contenido cuando el backend (`/foro`, `/recursos`, `/tips`) devuelve datos.
38
+
39
El contenido de cada pantalla va en `#main-content`.
frontend/css/comunidad.css
new
+323
@@ -0,0 +1,323 @@
1
+/* MAPS Connect - Comunidad y Recursos (Foro + Apuntes + Tips) */
2
+
3
+.academic-hub-container {
4
+ --hub-accent: #005a2b;
5
+ --hub-accent-dark: #004420;
6
+ --hub-accent-soft: #e8f5e9;
7
+ --hub-border: #dcdfe6;
8
+ --hub-bg: #f4f6f8;
9
+ --hub-text: #1a1a1a;
10
+ --hub-muted: #666;
11
+
12
+ max-width: 1050px;
13
+ margin: 0 auto;
14
+ width: 100%;
15
+}
16
+
17
+/* Bordes rectos institucionales (scopeado a la página) */
18
+.academic-hub-container .card,
19
+.academic-hub-container .input-search,
20
+.academic-hub-container .select-materia,
21
+.academic-hub-container .btn-solid,
22
+.academic-hub-container .btn-clean,
23
+.academic-hub-container .badge,
24
+.academic-hub-container .file-icon,
25
+.academic-hub-container .solved-box {
26
+ border-radius: 0;
27
+}
28
+
29
+.academic-hub-container .card {
30
+ background: #ffffff;
31
+ border: 1px solid var(--hub-border);
32
+ padding: 20px;
33
+ margin-bottom: 16px;
34
+}
35
+
36
+/* --- 1. BARRA DE BÚSQUEDA Y FILTRO SUPERIOR --- */
37
+.hub-filter-bar {
38
+ display: grid;
39
+ grid-template-columns: 2fr 1.2fr auto;
40
+ gap: 12px;
41
+ margin-bottom: 20px;
42
+}
43
+
44
+@media (max-width: 768px) {
45
+ .hub-filter-bar {
46
+ grid-template-columns: 1fr;
47
+ }
48
+}
49
+
50
+.input-search,
51
+.select-materia {
52
+ padding: 10px 14px;
53
+ border: 1px solid var(--hub-border);
54
+ background: #ffffff;
55
+ font-size: 0.9rem;
56
+ outline: none;
57
+ color: var(--hub-text);
58
+}
59
+
60
+.input-search:focus,
61
+.select-materia:focus {
62
+ border-color: var(--hub-accent);
63
+}
64
+
65
+.btn-solid {
66
+ background-color: var(--hub-accent);
67
+ color: #ffffff;
68
+ border: none;
69
+ padding: 10px 18px;
70
+ font-size: 0.9rem;
71
+ font-weight: bold;
72
+ cursor: pointer;
73
+ white-space: nowrap;
74
+}
75
+
76
+.btn-solid:hover {
77
+ background-color: var(--hub-accent-dark);
78
+}
79
+
80
+.btn-clean {
81
+ background-color: transparent;
82
+ color: var(--hub-accent);
83
+ border: 1px solid var(--hub-accent);
84
+ padding: 6px 14px;
85
+ font-size: 0.85rem;
86
+ font-weight: 600;
87
+ cursor: pointer;
88
+}
89
+
90
+.btn-clean:hover {
91
+ background-color: var(--hub-accent-soft);
92
+}
93
+
94
+/* --- 2. PESTAÑAS DE SUB-NAVEGACIÓN --- */
95
+.hub-tabs {
96
+ display: flex;
97
+ border-bottom: 2px solid var(--hub-border);
98
+ margin-bottom: 20px;
99
+ background: #ffffff;
100
+}
101
+
102
+.tab-button {
103
+ flex: 1;
104
+ padding: 14px;
105
+ background: none;
106
+ border: none;
107
+ border-bottom: 3px solid transparent;
108
+ font-size: 0.95rem;
109
+ font-weight: bold;
110
+ color: var(--hub-muted);
111
+ cursor: pointer;
112
+ text-align: center;
113
+}
114
+
115
+.tab-button:hover {
116
+ color: var(--hub-accent);
117
+}
118
+
119
+.tab-button.active {
120
+ color: var(--hub-accent);
121
+ border-bottom-color: var(--hub-accent);
122
+ background-color: #fafafa;
123
+}
124
+
125
+/* Paneles de contenido */
126
+.tab-panel {
127
+ display: none;
128
+}
129
+
130
+.tab-panel.active {
131
+ display: block;
132
+}
133
+
134
+/* Badges */
135
+.hub-badge {
136
+ padding: 3px 8px;
137
+ font-size: 0.75rem;
138
+ font-weight: bold;
139
+ display: inline-block;
140
+}
141
+
142
+.badge-materia {
143
+ background-color: var(--hub-accent-soft);
144
+ color: var(--hub-accent);
145
+ border: 1px solid #c8e6c9;
146
+}
147
+
148
+.badge-solved {
149
+ background-color: #dcfce7;
150
+ color: #166534;
151
+ border: 1px solid #bbf7d0;
152
+}
153
+
154
+.badge-verified {
155
+ background-color: #e0f2fe;
156
+ color: #0284c7;
157
+ border: 1px solid #bae6fd;
158
+}
159
+
160
+/* --- ESTILOS: FORO DE DUDAS --- */
161
+.post-header {
162
+ display: flex;
163
+ justify-content: space-between;
164
+ align-items: flex-start;
165
+ margin-bottom: 8px;
166
+ gap: 12px;
167
+ flex-wrap: wrap;
168
+}
169
+
170
+.post-meta {
171
+ font-size: 0.8rem;
172
+ color: #888;
173
+}
174
+
175
+.post-title {
176
+ font-size: 1.1rem;
177
+ color: #111;
178
+ margin-bottom: 6px;
179
+}
180
+
181
+.post-desc {
182
+ font-size: 0.9rem;
183
+ color: #444;
184
+ line-height: 1.4;
185
+ margin-bottom: 12px;
186
+}
187
+
188
+.post-desc code {
189
+ background: #f3f4f6;
190
+ padding: 1px 5px;
191
+ font-size: 0.85em;
192
+}
193
+
194
+.solved-box {
195
+ background-color: #f9fafb;
196
+ border-left: 3px solid var(--hub-accent);
197
+ padding: 10px 12px;
198
+ margin: 10px 0;
199
+ font-size: 0.85rem;
200
+}
201
+
202
+.post-footer {
203
+ display: flex;
204
+ justify-content: space-between;
205
+ align-items: center;
206
+ border-top: 1px solid #f0f0f0;
207
+ padding-top: 10px;
208
+ margin-top: 10px;
209
+ font-size: 0.82rem;
210
+ color: var(--hub-muted);
211
+}
212
+
213
+.post-actions {
214
+ display: flex;
215
+ gap: 12px;
216
+ flex-wrap: wrap;
217
+}
218
+
219
+.btn-link {
220
+ background: none;
221
+ border: none;
222
+ color: #555;
223
+ font-weight: 600;
224
+ font-size: 0.82rem;
225
+ cursor: pointer;
226
+}
227
+
228
+.btn-link:hover {
229
+ color: var(--hub-accent);
230
+}
231
+
232
+/* --- ESTILOS: APUNTES Y REPOSITORIO --- */
233
+.apunte-card {
234
+ display: flex;
235
+ justify-content: space-between;
236
+ align-items: center;
237
+ flex-wrap: wrap;
238
+ gap: 16px;
239
+}
240
+
241
+.apunte-info {
242
+ display: flex;
243
+ align-items: flex-start;
244
+ gap: 14px;
245
+}
246
+
247
+.file-icon {
248
+ width: 44px;
249
+ height: 44px;
250
+ background-color: var(--hub-accent-soft);
251
+ color: var(--hub-accent);
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: center;
255
+ font-weight: bold;
256
+ font-size: 0.85rem;
257
+ border: 1px solid #c8e6c9;
258
+ flex-shrink: 0;
259
+}
260
+
261
+.apunte-details h4 {
262
+ font-size: 1rem;
263
+ color: #111;
264
+ margin-bottom: 4px;
265
+}
266
+
267
+.apunte-details p {
268
+ font-size: 0.82rem;
269
+ color: var(--hub-muted);
270
+}
271
+
272
+/* --- ESTILOS: TIPS ACADÉMICOS --- */
273
+.tip-grid {
274
+ display: grid;
275
+ grid-template-columns: 1fr 1fr;
276
+ gap: 16px;
277
+}
278
+
279
+@media (max-width: 768px) {
280
+ .tip-grid {
281
+ grid-template-columns: 1fr;
282
+ }
283
+}
284
+
285
+.tip-card {
286
+ border-top: 3px solid var(--hub-accent);
287
+ display: flex;
288
+ flex-direction: column;
289
+ justify-content: space-between;
290
+}
291
+
292
+.tip-card .tip-head {
293
+ display: flex;
294
+ justify-content: space-between;
295
+ align-items: center;
296
+ gap: 8px;
297
+ flex-wrap: wrap;
298
+}
299
+
300
+.tip-title {
301
+ font-size: 1.05rem;
302
+ color: #111;
303
+ margin: 8px 0;
304
+}
305
+
306
+.tip-text {
307
+ font-size: 0.88rem;
308
+ color: #444;
309
+ line-height: 1.4;
310
+ margin-bottom: 12px;
311
+}
312
+
313
+.tip-text code {
314
+ background: #f3f4f6;
315
+ padding: 1px 5px;
316
+ font-size: 0.85em;
317
+}
318
+
319
+.vote-badge {
320
+ font-size: 0.82rem;
321
+ font-weight: bold;
322
+ color: var(--hub-accent);
323
+}
frontend/css/mensajes.css
new
+271
@@ -0,0 +1,271 @@
1
+/* MAPS Connect - Mensajes */
2
+
3
+.messages-container {
4
+ --hub-accent: #005a2b;
5
+ --hub-accent-dark: #004420;
6
+ --hub-accent-soft: #e8f5e9;
7
+ --hub-border: #dcdfe6;
8
+ --hub-muted: #666;
9
+
10
+ max-width: 1050px;
11
+ margin: 0 auto;
12
+ width: 100%;
13
+ height: calc(100vh - 110px);
14
+ min-height: 420px;
15
+ background: #ffffff;
16
+ border: 1px solid var(--hub-border);
17
+ display: grid;
18
+ grid-template-columns: 320px 1fr;
19
+}
20
+
21
+@media (max-width: 768px) {
22
+ .messages-container {
23
+ grid-template-columns: 1fr;
24
+ height: auto;
25
+ }
26
+}
27
+
28
+/* Bordes rectos institucionales (scopeado a la página) */
29
+.messages-container .avatar,
30
+.messages-container .btn-solid,
31
+.messages-container .btn-clean,
32
+.messages-container .chat-search-box input,
33
+.messages-container .chat-input-bar input,
34
+.messages-container .message {
35
+ border-radius: 0;
36
+}
37
+
38
+/* --- BANDEJA LATERAL (IZQUIERDA) --- */
39
+.chat-sidebar {
40
+ border-right: 1px solid var(--hub-border);
41
+ display: flex;
42
+ flex-direction: column;
43
+ background: #fafafa;
44
+}
45
+
46
+.chat-search-box {
47
+ padding: 14px;
48
+ border-bottom: 1px solid #e0e0e0;
49
+}
50
+
51
+.chat-search-box input {
52
+ width: 100%;
53
+ padding: 8px 12px;
54
+ border: 1px solid var(--hub-border);
55
+ outline: none;
56
+ background: #ffffff;
57
+ font-size: 0.88rem;
58
+ color: #1a1a1a;
59
+}
60
+
61
+.chat-search-box input:focus {
62
+ border-color: var(--hub-accent);
63
+}
64
+
65
+.conversation-list {
66
+ flex: 1;
67
+ overflow-y: auto;
68
+}
69
+
70
+.chat-item {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 12px;
74
+ padding: 14px;
75
+ border-bottom: 1px solid #f0f0f0;
76
+ cursor: pointer;
77
+ background: #ffffff;
78
+ transition: background 0.15s;
79
+}
80
+
81
+.chat-item:hover {
82
+ background: #f4f6f8;
83
+}
84
+
85
+.chat-item.active {
86
+ background: var(--hub-accent-soft);
87
+ border-left: 4px solid var(--hub-accent);
88
+}
89
+
90
+.avatar {
91
+ width: 40px;
92
+ height: 40px;
93
+ background-color: var(--hub-accent);
94
+ color: #ffffff;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ font-weight: bold;
99
+ font-size: 0.85rem;
100
+ flex-shrink: 0;
101
+}
102
+
103
+.avatar.prof {
104
+ background-color: #0284c7;
105
+}
106
+
107
+.chat-item-info {
108
+ flex: 1;
109
+ min-width: 0;
110
+}
111
+
112
+.chat-item-header {
113
+ display: flex;
114
+ justify-content: space-between;
115
+ margin-bottom: 2px;
116
+ gap: 8px;
117
+}
118
+
119
+.chat-item-header strong {
120
+ font-size: 0.9rem;
121
+ color: #222;
122
+ white-space: nowrap;
123
+ overflow: hidden;
124
+ text-overflow: ellipsis;
125
+}
126
+
127
+.chat-item-header span {
128
+ font-size: 0.75rem;
129
+ color: #888;
130
+ flex-shrink: 0;
131
+}
132
+
133
+.chat-item-preview {
134
+ font-size: 0.8rem;
135
+ color: var(--hub-muted);
136
+ white-space: nowrap;
137
+ overflow: hidden;
138
+ text-overflow: ellipsis;
139
+}
140
+
141
+/* --- VENTANA PRINCIPAL DE CHAT (DERECHA) --- */
142
+.chat-window {
143
+ display: flex;
144
+ flex-direction: column;
145
+ background: #ffffff;
146
+}
147
+
148
+.chat-header {
149
+ padding: 14px 20px;
150
+ border-bottom: 1px solid var(--hub-border);
151
+ display: flex;
152
+ justify-content: space-between;
153
+ align-items: center;
154
+ background: #ffffff;
155
+ gap: 12px;
156
+ flex-wrap: wrap;
157
+}
158
+
159
+.chat-header-user {
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 12px;
163
+}
164
+
165
+.chat-header-user h3 {
166
+ font-size: 1rem;
167
+ color: #111;
168
+}
169
+
170
+.chat-header-user span {
171
+ font-size: 0.8rem;
172
+ color: var(--hub-accent);
173
+ font-weight: 600;
174
+}
175
+
176
+/* Cuerpo con los mensajes */
177
+.chat-body {
178
+ flex: 1;
179
+ padding: 20px;
180
+ overflow-y: auto;
181
+ display: flex;
182
+ flex-direction: column;
183
+ gap: 12px;
184
+ background-color: #f9fafb;
185
+}
186
+
187
+.message {
188
+ max-width: 70%;
189
+ padding: 10px 14px;
190
+ font-size: 0.9rem;
191
+ line-height: 1.4;
192
+}
193
+
194
+.message.received {
195
+ align-self: flex-start;
196
+ background-color: #ffffff;
197
+ border: 1px solid #e0e0e0;
198
+ color: #222;
199
+}
200
+
201
+.message.sent {
202
+ align-self: flex-end;
203
+ background-color: var(--hub-accent);
204
+ color: #ffffff;
205
+}
206
+
207
+.message-time {
208
+ display: block;
209
+ font-size: 0.7rem;
210
+ margin-top: 4px;
211
+ text-align: right;
212
+ opacity: 0.8;
213
+}
214
+
215
+/* Estado vacío del chat */
216
+.chat-empty {
217
+ flex: 1;
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ color: #999;
222
+ font-size: 0.9rem;
223
+ background-color: #f9fafb;
224
+ padding: 24px;
225
+ text-align: center;
226
+}
227
+
228
+/* Barra de entrada inferior */
229
+.chat-input-bar {
230
+ padding: 14px 20px;
231
+ border-top: 1px solid var(--hub-border);
232
+ display: flex;
233
+ gap: 10px;
234
+ background: #ffffff;
235
+}
236
+
237
+.chat-input-bar input {
238
+ flex: 1;
239
+ padding: 10px 14px;
240
+ border: 1px solid var(--hub-border);
241
+ outline: none;
242
+ font-size: 0.9rem;
243
+ color: #1a1a1a;
244
+}
245
+
246
+.chat-input-bar input:focus {
247
+ border-color: var(--hub-accent);
248
+}
249
+
250
+.messages-container .btn-solid {
251
+ background-color: var(--hub-accent);
252
+ color: #ffffff;
253
+ border: none;
254
+ padding: 10px 20px;
255
+ font-size: 0.9rem;
256
+ font-weight: bold;
257
+ cursor: pointer;
258
+}
259
+
260
+.messages-container .btn-solid:hover {
261
+ background-color: var(--hub-accent-dark);
262
+}
263
+
264
+.messages-container .btn-clean {
265
+ background-color: transparent;
266
+ color: #555;
267
+ border: 1px solid var(--hub-border);
268
+ padding: 8px 14px;
269
+ cursor: pointer;
270
+ font-weight: 600;
271
+}
frontend/css/perfil.css
new
+254
@@ -0,0 +1,254 @@
1
+/* MAPS Connect - Mi Perfil */
2
+
3
+.perfil-container {
4
+ --hub-accent: #005a2b;
5
+ --hub-accent-dark: #004420;
6
+ --hub-accent-soft: #e8f5e9;
7
+ --hub-border: #dcdfe6;
8
+ --hub-muted: #666;
9
+
10
+ max-width: 1050px;
11
+ margin: 0 auto;
12
+ width: 100%;
13
+}
14
+
15
+/* Bordes rectos institucionales (scopeado a la página) */
16
+.perfil-container .card,
17
+.perfil-container .profile-avatar,
18
+.perfil-container .btn-solid,
19
+.perfil-container .btn-clean,
20
+.perfil-container .badge,
21
+.perfil-container .certificate-item,
22
+.perfil-container .stat-box {
23
+ border-radius: 0;
24
+}
25
+
26
+.perfil-container .card {
27
+ background: #ffffff;
28
+ border: 1px solid var(--hub-border);
29
+ padding: 20px;
30
+ margin-bottom: 20px;
31
+}
32
+
33
+/* --- 1. CABECERA DE PERFIL --- */
34
+.profile-header-card {
35
+ display: flex;
36
+ justify-content: space-between;
37
+ align-items: center;
38
+ flex-wrap: wrap;
39
+ gap: 20px;
40
+ border-left: 4px solid var(--hub-accent);
41
+}
42
+
43
+.profile-main-info {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 18px;
47
+}
48
+
49
+.profile-avatar {
50
+ width: 70px;
51
+ height: 70px;
52
+ background-color: var(--hub-accent);
53
+ color: #ffffff;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ font-weight: bold;
58
+ font-size: 1.6rem;
59
+ flex-shrink: 0;
60
+}
61
+
62
+.profile-name {
63
+ font-size: 1.4rem;
64
+ color: #111111;
65
+ margin-bottom: 4px;
66
+}
67
+
68
+.profile-sub {
69
+ font-size: 0.9rem;
70
+ color: #555555;
71
+}
72
+
73
+/* Botones */
74
+.perfil-container .btn-solid {
75
+ background-color: var(--hub-accent);
76
+ color: #ffffff;
77
+ border: none;
78
+ padding: 9px 18px;
79
+ font-size: 0.9rem;
80
+ font-weight: bold;
81
+ cursor: pointer;
82
+}
83
+
84
+.perfil-container .btn-solid:hover {
85
+ background-color: var(--hub-accent-dark);
86
+}
87
+
88
+.perfil-container .btn-clean {
89
+ background-color: transparent;
90
+ color: var(--hub-accent);
91
+ border: 1px solid var(--hub-accent);
92
+ padding: 8px 16px;
93
+ font-size: 0.85rem;
94
+ font-weight: 600;
95
+ cursor: pointer;
96
+}
97
+
98
+.perfil-container .btn-clean:hover {
99
+ background-color: var(--hub-accent-soft);
100
+}
101
+
102
+/* --- 2. LAYOUT A DOS COLUMNAS --- */
103
+.profile-grid {
104
+ display: grid;
105
+ grid-template-columns: 1.8fr 1.2fr;
106
+ gap: 20px;
107
+}
108
+
109
+@media (max-width: 850px) {
110
+ .profile-grid {
111
+ grid-template-columns: 1fr;
112
+ }
113
+}
114
+
115
+.section-heading {
116
+ font-size: 1.1rem;
117
+ color: #222222;
118
+ margin-bottom: 14px;
119
+ border-bottom: 1px solid #e0e0e0;
120
+ padding-bottom: 6px;
121
+}
122
+
123
+.sub-heading {
124
+ font-size: 0.95rem;
125
+ color: #333;
126
+ margin: 16px 0 10px 0;
127
+}
128
+
129
+/* Badges */
130
+.perfil-container .badge {
131
+ padding: 3px 8px;
132
+ font-size: 0.75rem;
133
+ font-weight: bold;
134
+ display: inline-block;
135
+}
136
+
137
+.badge-green {
138
+ background-color: var(--hub-accent-soft);
139
+ color: var(--hub-accent);
140
+ border: 1px solid #c8e6c9;
141
+}
142
+
143
+.badge-gray {
144
+ background-color: #f1f5f9;
145
+ color: #64748b;
146
+ border: 1px solid #e2e8f0;
147
+}
148
+
149
+/* --- COLUMNA IZQUIERDA: TRAYECTORIA MAPS --- */
150
+.purpose-box {
151
+ background-color: #f9fafb;
152
+ border-left: 3px solid var(--hub-accent);
153
+ padding: 12px 16px;
154
+ margin-bottom: 18px;
155
+}
156
+
157
+.purpose-box strong {
158
+ font-size: 0.85rem;
159
+ color: var(--hub-accent);
160
+ display: block;
161
+ text-transform: uppercase;
162
+ margin-bottom: 4px;
163
+}
164
+
165
+.purpose-box p {
166
+ font-size: 0.92rem;
167
+ color: #333333;
168
+ font-style: italic;
169
+}
170
+
171
+.certificate-item {
172
+ border: 1px solid var(--hub-border);
173
+ padding: 14px;
174
+ margin-bottom: 12px;
175
+ background: #ffffff;
176
+}
177
+
178
+.certificate-item.active {
179
+ border-left: 4px solid var(--hub-accent);
180
+}
181
+
182
+.cert-header {
183
+ display: flex;
184
+ justify-content: space-between;
185
+ align-items: center;
186
+ margin-bottom: 6px;
187
+ gap: 8px;
188
+ flex-wrap: wrap;
189
+}
190
+
191
+.cert-title {
192
+ font-size: 0.95rem;
193
+ color: #111111;
194
+ font-weight: bold;
195
+}
196
+
197
+.cert-desc {
198
+ font-size: 0.85rem;
199
+ color: #555555;
200
+ line-height: 1.4;
201
+}
202
+
203
+/* --- COLUMNA DERECHA: APORTACIONES Y ESTADÍSTICAS --- */
204
+.stats-summary {
205
+ display: grid;
206
+ grid-template-columns: 1fr 1fr;
207
+ gap: 10px;
208
+ margin-bottom: 18px;
209
+}
210
+
211
+.stat-box {
212
+ background: #fafafa;
213
+ border: 1px solid #e0e0e0;
214
+ padding: 12px;
215
+ text-align: center;
216
+}
217
+
218
+.stat-box .number {
219
+ font-size: 1.3rem;
220
+ font-weight: bold;
221
+ color: var(--hub-accent);
222
+ display: block;
223
+}
224
+
225
+.stat-box .label {
226
+ font-size: 0.75rem;
227
+ color: var(--hub-muted);
228
+ text-transform: uppercase;
229
+}
230
+
231
+.activity-list {
232
+ list-style: none;
233
+}
234
+
235
+.activity-item {
236
+ padding: 10px 0;
237
+ border-bottom: 1px solid #f0f0f0;
238
+ font-size: 0.85rem;
239
+}
240
+
241
+.activity-item:last-child {
242
+ border-bottom: none;
243
+}
244
+
245
+.activity-item strong {
246
+ color: #222222;
247
+ display: block;
248
+ margin-bottom: 2px;
249
+}
250
+
251
+.activity-item span {
252
+ color: #888888;
253
+ font-size: 0.75rem;
254
+}
frontend/js/layout.js
+1
-3
@@ -5,9 +5,7 @@
5
const NAV_ITEMS = [
6
{ href: 'inicio.html', label: 'Inicio' },
7
{ href: 'perfil.html', label: 'Mi Perfil' },
8
- { href: 'foro.html', label: 'Foro de Dudas' },
9
- { href: 'tips.html', label: 'Tips Académicos' },
10
- { href: 'recursos.html', label: 'Apuntes' },
8
+ { href: 'comunidad.html', label: 'Comunidad y Recursos' },
9
{ href: 'mensajes.html', label: 'Mensajes' },
10
{ href: 'empresarial.html', label: 'Semestre Empresarial' },
11
{ href: 'circulos.html', label: 'Círculos de Estudio' }
frontend/js/pages/comunidad.js
new
+299
@@ -0,0 +1,299 @@
1
+/**
2
+ * MAPS Connect - Comunidad y Recursos (Foro + Apuntes + Tips)
3
+ * Las tarjetas solo se renderizan cuando el backend devuelve datos.
4
+ */
5
+
6
+const Comunidad = {
7
+ state: {
8
+ foro: [],
9
+ apuntes: [],
10
+ tips: [],
11
+ loaded: new Set(),
12
+ activeTab: 'foro-dudas'
13
+ },
14
+
15
+ init() {
16
+ Layout.init('comunidad.html');
17
+ Layout.setPageTitle('Comunidad y Recursos');
18
+
19
+ this.bindTabs();
20
+ this.bindFilters();
21
+
22
+ // Permite enlazar directo a una pestaña: comunidad.html#foro-dudas
23
+ const hash = window.location.hash.replace('#', '');
24
+ const startTab = ['foro-dudas', 'apuntes', 'tips'].includes(hash) ? hash : 'foro-dudas';
25
+ const btn = document.querySelector(`.tab-button[data-tab="${startTab}"]`);
26
+ this.switchTab(startTab, btn ? btn.dataset.action : '+ Preguntar Duda');
27
+ },
28
+
29
+ bindTabs() {
30
+ const tabs = document.querySelectorAll('.tab-button');
31
+ tabs.forEach(btn => {
32
+ btn.addEventListener('click', () => {
33
+ this.switchTab(btn.dataset.tab, btn.dataset.action);
34
+ });
35
+ });
36
+ },
37
+
38
+ bindFilters() {
39
+ const search = document.getElementById('hub-search');
40
+ const materia = document.getElementById('hub-materia');
41
+ search?.addEventListener('input', () => this.renderActive());
42
+ materia?.addEventListener('change', () => this.renderActive());
43
+ },
44
+
45
+ switchTab(tabId, actionText) {
46
+ this.state.activeTab = tabId;
47
+
48
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
49
+ document.querySelectorAll('.tab-button').forEach(b => b.classList.remove('active'));
50
+
51
+ document.getElementById(tabId)?.classList.add('active');
52
+ document.querySelector(`.tab-button[data-tab="${tabId}"]`)?.classList.add('active');
53
+
54
+ const actionBtn = document.getElementById('context-action-btn');
55
+ if (actionBtn) actionBtn.textContent = actionText;
56
+
57
+ this.loadTab(tabId);
58
+ },
59
+
60
+ async loadTab(tabId) {
61
+ const map = {
62
+ 'foro-dudas': { key: 'foro', loader: () => this.loadForo() },
63
+ 'apuntes': { key: 'apuntes', loader: () => this.loadApuntes() },
64
+ 'tips': { key: 'tips', loader: () => this.loadTips() }
65
+ };
66
+ const conf = map[tabId];
67
+ if (!conf) return;
68
+ if (this.state.loaded.has(conf.key)) {
69
+ this.renderActive();
70
+ return;
71
+ }
72
+ await conf.loader();
73
+ },
74
+
75
+ async loadForo() {
76
+ try {
77
+ const res = await API.request('/foro');
78
+ this.state.foro = this.toArray(res);
79
+ this.state.loaded.add('foro');
80
+ } catch (e) {
81
+ this.state.foro = [];
82
+ }
83
+ this.renderActive();
84
+ },
85
+
86
+ async loadApuntes() {
87
+ try {
88
+ const res = await API.request('/recursos');
89
+ this.state.apuntes = this.toArray(res);
90
+ this.state.loaded.add('apuntes');
91
+ } catch (e) {
92
+ this.state.apuntes = [];
93
+ }
94
+ this.renderActive();
95
+ },
96
+
97
+ async loadTips() {
98
+ try {
99
+ const res = await API.request('/tips');
100
+ this.state.tips = this.toArray(res);
101
+ this.state.loaded.add('tips');
102
+ } catch (e) {
103
+ this.state.tips = [];
104
+ }
105
+ this.renderActive();
106
+ },
107
+
108
+ renderActive() {
109
+ const q = (document.getElementById('hub-search')?.value || '').trim().toLowerCase();
110
+ const m = document.getElementById('hub-materia')?.value || '';
111
+
112
+ const match = item => {
113
+ const text = `${this.field(item, 'titulo')} ${this.field(item, 'descripcion')} ${this.field(item, 'texto')}`.toLowerCase();
114
+ const materia = this.materiaNombre(item).toLowerCase();
115
+ return (!q || text.includes(q)) && (!m || materia === m);
116
+ };
117
+
118
+ switch (this.state.activeTab) {
119
+ case 'foro-dudas':
120
+ this.renderForo(this.state.foro.filter(match), 'foro-dudas');
121
+ break;
122
+ case 'apuntes':
123
+ this.renderApuntes(this.state.apuntes.filter(match), 'apuntes');
124
+ break;
125
+ case 'tips':
126
+ this.renderTips(this.state.tips.filter(match), 'tips');
127
+ break;
128
+ }
129
+ },
130
+
131
+ renderForo(items, panelId) {
132
+ const panel = document.getElementById(panelId);
133
+ if (!panel) return;
134
+ panel.innerHTML = '';
135
+ if (!items.length) return;
136
+ panel.innerHTML = items.map(item => this.foroCard(item)).join('');
137
+ },
138
+
139
+ renderApuntes(items, panelId) {
140
+ const panel = document.getElementById(panelId);
141
+ if (!panel) return;
142
+ panel.innerHTML = '';
143
+ if (!items.length) return;
144
+ panel.innerHTML = items.map(item => this.apunteCard(item)).join('');
145
+ },
146
+
147
+ renderTips(items, panelId) {
148
+ const panel = document.getElementById(panelId);
149
+ if (!panel) return;
150
+ panel.innerHTML = '';
151
+ if (!items.length) return;
152
+ panel.innerHTML = items.map(item => this.tipCard(item)).join('');
153
+ },
154
+
155
+ foroCard(item) {
156
+ const titulo = this.field(item, 'titulo');
157
+ const desc = this.field(item, 'descripcion');
158
+ const materia = this.materiaNombre(item);
159
+ const autor = this.autorNombre(item);
160
+ const tiempo = this.field(item, 'tiempo') || this.field(item, 'haceTiempo') || '';
161
+ const votos = this.num(item, 'votos');
162
+ const respuestas = this.num(item, 'respuestas') || this.num(item, 'numRespuestas');
163
+ const resuelto = item.resuelto === true || item.resuelto === 'true';
164
+ const solucion = this.field(item, 'solucion');
165
+
166
+ const badges = [
167
+ materia ? `<span class="hub-badge badge-materia">${this.esc(materia)}</span>` : '',
168
+ resuelto ? `<span class="hub-badge badge-solved">✓ Solución Aceptada</span>` : ''
169
+ ].join('');
170
+
171
+ const solucionBox = (resuelto && solucion)
172
+ ? `<div class="solved-box"><strong>Solución validada por el autor:</strong><p>${this.esc(solucion)}</p></div>`
173
+ : '';
174
+
175
+ const metaTiempo = tiempo ? `Por: ${this.esc(autor)} • ${this.esc(tiempo)}` : `Por: ${this.esc(autor)}`;
176
+ const accionRespuesta = resuelto
177
+ ? `<button class="btn-link">💬 ${respuestas} Respuestas</button>`
178
+ : `<button class="btn-link" style="color:#005a2b;font-weight:bold;">✍ Aportar Respuesta</button>`;
179
+
180
+ return `
181
+ <article class="card">
182
+ <header class="post-header">
183
+ <div style="display:flex;gap:6px;flex-wrap:wrap;">${badges}</div>
184
+ <span class="post-meta">${this.esc(metaTiempo)}</span>
185
+ </header>
186
+ <h3 class="post-title">${this.esc(titulo)}</h3>
187
+ <p class="post-desc">${this.esc(desc)}</p>
188
+ ${solucionBox}
189
+ <footer class="post-footer">
190
+ <div class="post-actions">
191
+ <button class="btn-link">▲ ${votos} Votos</button>
192
+ ${accionRespuesta}
193
+ <button class="btn-link">🔖 Guardar</button>
194
+ </div>
195
+ <span>${resuelto ? 'Última aportación reciente' : 'Esperando solución'}</span>
196
+ </footer>
197
+ </article>`;
198
+ },
199
+
200
+ apunteCard(item) {
201
+ const titulo = this.field(item, 'titulo');
202
+ const materia = this.materiaNombre(item);
203
+ const autor = this.autorNombre(item);
204
+ const tipo = this.field(item, 'tipo') || 'PDF';
205
+ const tamano = this.field(item, 'tamano') || this.field(item, 'tamanio') || '';
206
+ const descargas = this.num(item, 'descargas');
207
+ const rating = this.field(item, 'rating') || '';
208
+ const url = this.field(item, 'url') || this.field(item, 'archivoUrl') || '#';
209
+
210
+ return `
211
+ <article class="card apunte-card">
212
+ <div class="apunte-info">
213
+ <div class="file-icon">${this.esc(tipo)}</div>
214
+ <div class="apunte-details">
215
+ <h4>${this.esc(titulo)}</h4>
216
+ <p>Materia: <strong>${this.esc(materia)}</strong> • Subido por: <strong>${this.esc(autor)}</strong></p>
217
+ <p style="margin-top:4px;color:#888;">${tamano ? `Tamaño: ${this.esc(tamano)} • ` : ''}${descargas} descargas${rating ? ` • ${this.esc(rating)} ★` : ''}</p>
218
+ </div>
219
+ </div>
220
+ <button class="btn-solid" type="button" data-href="${this.esc(url)}">⬇ Descargar Documento</button>
221
+ </article>`;
222
+ },
223
+
224
+ tipCard(item) {
225
+ const titulo = this.field(item, 'titulo');
226
+ const texto = this.field(item, 'texto') || this.field(item, 'descripcion');
227
+ const materia = this.materiaNombre(item);
228
+ const autor = this.autorNombre(item);
229
+ const votos = this.num(item, 'votos');
230
+ const verificado = item.verificado === true || item.verificado === 'true';
231
+
232
+ const verified = verificado
233
+ ? `<span class="hub-badge badge-verified">✓ Verificado por Docente</span>`
234
+ : '';
235
+
236
+ return `
237
+ <article class="card tip-card">
238
+ <div>
239
+ <div class="tip-head">
240
+ <span class="hub-badge badge-materia">${this.esc(materia)}</span>
241
+ ${verified}
242
+ </div>
243
+ <h4 class="tip-title">${this.esc(titulo)}</h4>
244
+ <p class="tip-text">${this.esc(texto)}</p>
245
+ </div>
246
+ <div class="post-footer">
247
+ <span class="vote-badge">▲ ${votos} votos de utilidad</span>
248
+ <span style="font-size:0.78rem;color:#888;">Por: ${this.esc(autor)}</span>
249
+ </div>
250
+ </article>`;
251
+ },
252
+
253
+ // ---- helpers ----
254
+ field(obj, key) {
255
+ if (!obj) return '';
256
+ return obj[key] != null ? String(obj[key]) : '';
257
+ },
258
+
259
+ num(obj, key) {
260
+ const v = obj && obj[key];
261
+ const n = Number(v);
262
+ return Number.isFinite(n) ? n : 0;
263
+ },
264
+
265
+ materiaNombre(item) {
266
+ if (!item) return '';
267
+ if (typeof item.materia === 'string') return item.materia;
268
+ if (item.materia && typeof item.materia === 'object') return item.materia.nombre || item.materia.nom || '';
269
+ if (item.materiaNombre) return item.materiaNombre;
270
+ return 'General';
271
+ },
272
+
273
+ autorNombre(item) {
274
+ if (!item) return 'Anónimo';
275
+ if (typeof item.autor === 'string') return item.autor;
276
+ if (item.autor && typeof item.autor === 'object') return item.autor.nombre || item.autor.nom || 'Anónimo';
277
+ if (item.autorNombre) return item.autorNombre;
278
+ return 'Anónimo';
279
+ },
280
+
281
+ toArray(res) {
282
+ if (!res) return [];
283
+ if (Array.isArray(res)) return res;
284
+ if (Array.isArray(res.data)) return res.data;
285
+ if (Array.isArray(res.contenido)) return res.contenido;
286
+ return [];
287
+ },
288
+
289
+ esc(str) {
290
+ return String(str == null ? '' : str)
291
+ .replace(/&/g, '&')
292
+ .replace(/</g, '<')
293
+ .replace(/>/g, '>')
294
+ .replace(/"/g, '"')
295
+ .replace(/'/g, ''');
296
+ }
297
+};
298
+
299
+document.addEventListener('DOMContentLoaded', () => Comunidad.init());
frontend/js/pages/foro.js
deleted
-4
@@ -1,4 +0,0 @@
1
-document.addEventListener('DOMContentLoaded', () => {
2
- Layout.init('foro.html');
3
- Layout.setPageTitle('Foro de Dudas');
4
-});
frontend/js/pages/inicio.js
+3
-3
@@ -43,7 +43,7 @@ function renderDashboard(content, data) {
43
44
document.getElementById('quick-question-form')?.addEventListener('submit', event => {
45
event.preventDefault();
46
- window.location.href = 'foro.html';
46
+ window.location.href = 'comunidad.html#foro-dudas';
47
});
48
}
49
@@ -87,7 +87,7 @@ function renderPost(post) {
87
<div class="post-header"><div class="user-block"><div class="avatar ${post.verificadoDocente ? 'prof' : ''}">${escapeHtml(initials)}</div><div class="user-data"><strong>${escapeHtml(author.nombre || 'Usuario')}</strong><span>${escapeHtml(post.fechaRelativa || '')}</span></div></div><div class="badges-wrap">${tags}</div></div>
88
<h2 class="post-title">${escapeHtml(post.titulo || '')}</h2>
89
${post.contenido ? `<p class="post-body">${escapeHtml(post.contenido)}</p>` : ''}
90
- <div class="post-footer"><div class="action-links"><a class="btn-action" href="foro.html">💬 ${Number(post.respuestas) || 0} respuestas</a></div><span class="post-status">${post.solucionAceptada ? 'Resuelto' : ''}</span></div>
90
+ <div class="post-footer"><div class="action-links"><a class="btn-action" href="comunidad.html#foro-dudas">💬 ${Number(post.respuestas) || 0} respuestas</a></div><span class="post-status">${post.solucionAceptada ? 'Resuelto' : ''}</span></div>
91
</article>
92
`;
93
}
@@ -106,7 +106,7 @@ function renderAccessCards(data) {
106
}
107
108
function renderEmptyFeed() {
109
- return '<section class="card empty-state"><h2>Aún no hay actividad</h2><p>Las dudas, avisos y recursos de tu comunidad aparecerán aquí cuando estén disponibles.</p><a class="side-link" href="foro.html">Ir al foro →</a></section>';
109
+ return '<section class="card empty-state"><h2>Aún no hay actividad</h2><p>Las dudas, avisos y recursos de tu comunidad aparecerán aquí cuando estén disponibles.</p><a class="side-link" href="comunidad.html#foro-dudas">Ir al foro →</a></section>';
110
}
111
112
function escapeHtml(value) {
frontend/js/pages/mensajes.js
+228
-4
@@ -1,4 +1,228 @@
1
-document.addEventListener('DOMContentLoaded', () => {
2
- Layout.init('mensajes.html');
3
- Layout.setPageTitle('Mensajes');
4
-});
1
+/**
2
+ * MAPS Connect - Mensajes
3
+ * Las conversaciones y los mensajes se renderizan solo cuando el backend
4
+ * (/mensajes y /mensajes/{id}) devuelve datos.
5
+ */
6
+
7
+const Mensajes = {
8
+ state: {
9
+ conversaciones: [],
10
+ seleccionada: null
11
+ },
12
+
13
+ async init() {
14
+ Layout.init('mensajes.html');
15
+ Layout.setPageTitle('Mensajes');
16
+
17
+ this.bindEventos();
18
+ await this.loadConversaciones();
19
+ },
20
+
21
+ bindEventos() {
22
+ const list = document.getElementById('conversacion-list');
23
+ list?.addEventListener('click', e => {
24
+ const item = e.target.closest('.chat-item');
25
+ if (item && item.dataset.id) this.seleccionar(item.dataset.id);
26
+ });
27
+
28
+ const search = document.getElementById('chat-search');
29
+ search?.addEventListener('input', () => this.renderConversaciones());
30
+
31
+ document.getElementById('enviar-mensaje')?.addEventListener('click', () => this.enviar());
32
+ document.getElementById('mensaje-input')?.addEventListener('keydown', e => {
33
+ if (e.key === 'Enter') this.enviar();
34
+ });
35
+ },
36
+
37
+ async loadConversaciones() {
38
+ try {
39
+ const res = await API.request('/mensajes');
40
+ this.state.conversaciones = this.toArray(res);
41
+ } catch (e) {
42
+ this.state.conversaciones = [];
43
+ }
44
+ this.renderConversaciones();
45
+ },
46
+
47
+ renderConversaciones() {
48
+ const host = document.getElementById('conversacion-list');
49
+ if (!host) return;
50
+
51
+ const q = (document.getElementById('chat-search')?.value || '').trim().toLowerCase();
52
+ const items = this.state.conversaciones.filter(c => {
53
+ const nombre = this.nombreConv(c).toLowerCase();
54
+ const preview = this.field(c, 'preview').toLowerCase();
55
+ return !q || nombre.includes(q) || preview.includes(q);
56
+ });
57
+
58
+ if (!items.length) { host.innerHTML = ''; return; }
59
+
60
+ host.innerHTML = items.map(c => {
61
+ const id = this.field(c, 'id');
62
+ const active = this.state.seleccionada && String(this.state.seleccionada) === String(id) ? ' active' : '';
63
+ const initials = this.initials(this.nombreConv(c));
64
+ const profCls = this.esProf(c) ? ' prof' : '';
65
+ const tiempo = this.field(c, 'tiempo') || this.field(c, 'fechaRelativa');
66
+ const preview = this.field(c, 'preview');
67
+ return `
68
+ <div class="chat-item${active}" data-id="${this.esc(id)}">
69
+ <div class="avatar${profCls}">${this.esc(initials)}</div>
70
+ <div class="chat-item-info">
71
+ <div class="chat-item-header">
72
+ <strong>${this.esc(this.nombreConv(c))}</strong>
73
+ ${tiempo ? `<span>${this.esc(tiempo)}</span>` : ''}
74
+ </div>
75
+ <p class="chat-item-preview">${this.esc(preview)}</p>
76
+ </div>
77
+ </div>`;
78
+ }).join('');
79
+ },
80
+
81
+ async seleccionar(id) {
82
+ this.state.seleccionada = id;
83
+ this.renderConversaciones();
84
+
85
+ // Cabecera provisional con el dato de la lista; el detalle viene del backend.
86
+ const conv = this.state.conversaciones.find(c => String(this.field(c, 'id')) === String(id));
87
+ if (conv) this.renderCabecera(conv, true);
88
+
89
+ this.setEntradaHabilitada(false);
90
+ this.renderMensajes([]);
91
+
92
+ let data;
93
+ try {
94
+ data = await API.request(`/mensajes/${encodeURIComponent(id)}`);
95
+ } catch (e) {
96
+ // El endpoint se implementará en el backend. La UI mantiene estados vacíos.
97
+ data = null;
98
+ }
99
+
100
+ if (!data) return;
101
+
102
+ // Refresca cabecera con el detalle completo si viene.
103
+ this.renderCabecera(data, false);
104
+ const mensajes = Array.isArray(data.mensajes) ? data.mensajes : (Array.isArray(data) ? data : []);
105
+ this.renderMensajes(mensajes);
106
+ this.setEntradaHabilitada(true);
107
+ },
108
+
109
+ renderCabecera(data, fromList) {
110
+ const host = document.getElementById('chat-header');
111
+ if (!host) return;
112
+ const nombre = this.nombreConv(data);
113
+ const initials = this.initials(nombre);
114
+ const profCls = this.esProf(data) ? ' prof' : '';
115
+ const sub = this.field(data, 'subtitulo') || (fromList ? '' : '');
116
+
117
+ host.innerHTML = `
118
+ <div class="chat-header-user">
119
+ <div class="avatar${profCls}">${this.esc(initials)}</div>
120
+ <div>
121
+ <h3>${this.esc(nombre)}</h3>
122
+ ${sub ? `<span>${this.esc(sub)}</span>` : ''}
123
+ </div>
124
+ </div>
125
+ <div>
126
+ <button class="btn-clean" type="button">Ver Perfil Académico</button>
127
+ </div>
128
+ `;
129
+ },
130
+
131
+ renderMensajes(mensajes) {
132
+ const host = document.getElementById('chat-body');
133
+ const empty = document.getElementById('chat-empty');
134
+ if (!host) return;
135
+
136
+ if (!mensajes.length) {
137
+ host.innerHTML = '';
138
+ if (empty) host.appendChild(empty);
139
+ return;
140
+ }
141
+
142
+ if (empty) empty.remove();
143
+ host.innerHTML = mensajes.map(m => this.messageEl(m)).join('');
144
+ host.scrollTop = host.scrollHeight;
145
+ },
146
+
147
+ messageEl(m) {
148
+ const texto = this.field(m, 'texto') || this.field(m, 'contenido');
149
+ const tiempo = this.field(m, 'tiempo') || this.field(m, 'fechaRelativa');
150
+ const enviado = m.enviado === true || m.enviado === 'true' || m.propio === true;
151
+ const cls = enviado ? 'sent' : 'received';
152
+ return `
153
+ <div class="message ${cls}">
154
+ ${this.esc(texto)}
155
+ ${tiempo ? `<span class="message-time">${this.esc(tiempo)}</span>` : ''}
156
+ </div>`;
157
+ },
158
+
159
+ async enviar() {
160
+ const input = document.getElementById('mensaje-input');
161
+ if (!input) return;
162
+ const texto = input.value.trim();
163
+ if (!texto || !this.state.seleccionada) return;
164
+
165
+ try {
166
+ await API.request(`/mensajes/${encodeURIComponent(this.state.seleccionada)}`, {
167
+ method: 'POST',
168
+ body: JSON.stringify({ texto })
169
+ });
170
+ input.value = '';
171
+ // Refrescar la conversación para obtener el mensaje persistido.
172
+ await this.seleccionar(this.state.seleccionada);
173
+ } catch (e) {
174
+ // El endpoint aún no existe en el backend. No se limpia el input.
175
+ }
176
+ },
177
+
178
+ setEntradaHabilitada(on) {
179
+ const input = document.getElementById('mensaje-input');
180
+ const btn = document.getElementById('enviar-mensaje');
181
+ if (input) input.disabled = !on;
182
+ if (btn) btn.disabled = !on;
183
+ },
184
+
185
+ // ---- helpers ----
186
+ nombreConv(c) {
187
+ if (!c) return '';
188
+ return this.field(c, 'nombre') || this.field(c, 'nombreCompleto') || (c.autor && (c.autor.nombre || c.autor.nombreCompleto)) || '';
189
+ },
190
+
191
+ esProf(c) {
192
+ if (!c) return false;
193
+ if (c.esProf === true || c.esProfesor === true || c.rol === 'DOCENTE' || c.rol === 'PROFESOR') return true;
194
+ return false;
195
+ },
196
+
197
+ field(obj, key) {
198
+ if (!obj) return '';
199
+ return obj[key] != null ? String(obj[key]) : '';
200
+ },
201
+
202
+ initials(name) {
203
+ if (!name) return '?';
204
+ const parts = String(name).trim().split(/\s+/);
205
+ const first = parts[0] ? parts[0][0] : '';
206
+ const second = parts[1] ? parts[1][0] : '';
207
+ return (first + second).toUpperCase() || '?';
208
+ },
209
+
210
+ toArray(res) {
211
+ if (!res) return [];
212
+ if (Array.isArray(res)) return res;
213
+ if (Array.isArray(res.data)) return res.data;
214
+ if (Array.isArray(res.contenido)) return res.contenido;
215
+ return [];
216
+ },
217
+
218
+ esc(str) {
219
+ return String(str == null ? '' : str)
220
+ .replace(/&/g, '&')
221
+ .replace(/</g, '<')
222
+ .replace(/>/g, '>')
223
+ .replace(/"/g, '"')
224
+ .replace(/'/g, ''');
225
+ }
226
+};
227
+
228
+document.addEventListener('DOMContentLoaded', () => Mensajes.init());
frontend/js/pages/perfil.js
+189
-4
@@ -1,4 +1,189 @@
1
-document.addEventListener('DOMContentLoaded', () => {
2
- Layout.init('perfil.html');
3
- Layout.setPageTitle('Mi Perfil');
4
-});
1
+/**
2
+ * MAPS Connect - Mi Perfil
3
+ * La cabecera usa la sesión (Auth) como dato real; el detalle académico
4
+ * (matrícula, carrera, certificados, stats, actividad) se renderiza solo
5
+ * cuando el backend (/estudiantes/perfil) devuelve datos.
6
+ */
7
+
8
+const Perfil = {
9
+ async init() {
10
+ Layout.init('perfil.html');
11
+ Layout.setPageTitle('Mi Perfil');
12
+
13
+ // La cabecera puede pintarse con la sesión real del usuario.
14
+ this.renderHeader(Auth.getUser());
15
+
16
+ try {
17
+ const data = await API.request('/estudiantes/perfil');
18
+ this.renderProfile(data || {});
19
+ } catch (e) {
20
+ // El endpoint se implementará en el backend. La UI mantiene estados vacíos.
21
+ }
22
+ },
23
+
24
+ renderHeader(user) {
25
+ const host = document.getElementById('profile-header-info');
26
+ if (!host) return;
27
+
28
+ const nombre = (user && user.nombre) || '';
29
+ const email = (user && user.email) || '';
30
+ if (!nombre && !email) {
31
+ host.innerHTML = '';
32
+ return;
33
+ }
34
+
35
+ const initials = this.initials(nombre || email);
36
+
37
+ host.innerHTML = `
38
+ <div class="profile-avatar">${this.esc(initials)}</div>
39
+ <div>
40
+ <h2 class="profile-name">${this.esc(nombre)}</h2>
41
+ ${email ? `<p class="profile-sub">${this.esc(email)}</p>` : ''}
42
+ </div>
43
+ `;
44
+ },
45
+
46
+ renderProfile(data) {
47
+ // Cabecera con datos completos si el backend los trae.
48
+ if (data.nombre || data.matricula || data.carrera) {
49
+ this.renderHeaderData(data);
50
+ }
51
+
52
+ // Propósito
53
+ const purposeBox = document.getElementById('profile-purpose');
54
+ const purposeText = document.getElementById('profile-purpose-text');
55
+ if (purposeBox && purposeText) {
56
+ if (data.proposito) {
57
+ purposeText.textContent = data.proposito;
58
+ purposeBox.hidden = false;
59
+ } else {
60
+ purposeBox.hidden = true;
61
+ }
62
+ }
63
+
64
+ // Certificados
65
+ const certs = Array.isArray(data.certificados) ? data.certificados : [];
66
+ this.renderCertificados(certs);
67
+
68
+ // Stats
69
+ this.renderStats(data.stats || {});
70
+
71
+ // Actividad
72
+ const acts = Array.isArray(data.actividad) ? data.actividad : [];
73
+ this.renderActividad(acts);
74
+ },
75
+
76
+ renderHeaderData(data) {
77
+ const host = document.getElementById('profile-header-info');
78
+ if (!host) return;
79
+
80
+ const nombre = data.nombre || (Auth.getUser() && Auth.getUser().nombre) || '';
81
+ const initials = this.initials(nombre);
82
+
83
+ const subParts = [];
84
+ if (data.matricula) subParts.push(`Matrícula: <strong>${this.esc(data.matricula)}</strong>`);
85
+ if (data.carrera) subParts.push(`<strong>${this.esc(data.carrera)}</strong>`);
86
+ const sub1 = subParts.length ? `<p class="profile-sub">${subParts.join(' • ')}</p>` : '';
87
+
88
+ const subParts2 = [];
89
+ if (data.semestre) subParts2.push(`Semestre Vigente: <strong>${this.esc(data.semestre)}° Semestre</strong>`);
90
+ if (data.campus) subParts2.push(this.esc(data.campus));
91
+ const sub2 = subParts2.length ? `<p class="profile-sub" style="margin-top:2px;">${subParts2.join(' • ')}</p>` : '';
92
+
93
+ host.innerHTML = `
94
+ <div class="profile-avatar">${this.esc(initials)}</div>
95
+ <div>
96
+ <h2 class="profile-name">${this.esc(nombre)}</h2>
97
+ ${sub1}
98
+ ${sub2}
99
+ </div>
100
+ `;
101
+ },
102
+
103
+ renderCertificados(certs) {
104
+ const host = document.getElementById('profile-certificados');
105
+ if (!host) return;
106
+ if (!certs.length) { host.innerHTML = ''; return; }
107
+ host.innerHTML = certs.map(c => this.certCard(c)).join('');
108
+ },
109
+
110
+ certCard(c) {
111
+ const titulo = c.titulo || '';
112
+ const desc = c.descripcion || '';
113
+ const estado = c.estado || '';
114
+ const semestre = c.semestre ? `${this.esc(c.semestre)}° Semestre` : '';
115
+
116
+ let badge = '';
117
+ if (estado === 'acreditado') {
118
+ badge = `<span class="badge badge-green">✓ Acreditado${semestre ? ' (' + semestre + ')' : ''}</span>`;
119
+ } else if (estado === 'encurso' || estado === 'en-curso') {
120
+ badge = `<span class="badge badge-green">En Curso${semestre ? ' (' + semestre + ')' : ''}</span>`;
121
+ } else if (estado === 'futuro' || estado === 'bloque') {
122
+ badge = `<span class="badge badge-gray">Bloque Futuro${semestre ? ' (' + semestre + ')' : ''}</span>`;
123
+ } else if (estado) {
124
+ badge = `<span class="badge badge-gray">${this.esc(estado)}</span>`;
125
+ }
126
+
127
+ const activeCls = (estado === 'encurso' || estado === 'en-curso') ? ' active' : '';
128
+
129
+ return `
130
+ <div class="certificate-item${activeCls}">
131
+ <div class="cert-header">
132
+ <span class="cert-title">${this.esc(titulo)}</span>
133
+ ${badge}
134
+ </div>
135
+ ${desc ? `<p class="cert-desc">${this.esc(desc)}</p>` : ''}
136
+ </div>`;
137
+ },
138
+
139
+ renderStats(stats) {
140
+ const host = document.getElementById('profile-stats');
141
+ if (!host) return;
142
+ const items = [
143
+ { n: stats.dudasResueltas, l: 'Dudas Resueltas' },
144
+ { n: stats.apuntesCompartidos, l: 'Apuntes Compartidos' },
145
+ { n: stats.votos, l: 'Votos de Utilidad' },
146
+ { n: stats.reputacion, l: 'Reputación Académica' }
147
+ ];
148
+ // Solo se pinta si al menos hay un valor con dato.
149
+ const hasData = items.some(it => it.n !== undefined && it.n !== null && it.n !== '');
150
+ if (!hasData) { host.innerHTML = ''; return; }
151
+ host.innerHTML = items.map(it => `
152
+ <div class="stat-box">
153
+ <span class="number">${this.esc(it.n !== undefined && it.n !== null && it.n !== '' ? it.n : '—')}</span>
154
+ <span class="label">${this.esc(it.l)}</span>
155
+ </div>
156
+ `).join('');
157
+ },
158
+
159
+ renderActividad(acts) {
160
+ const host = document.getElementById('profile-actividad');
161
+ if (!host) return;
162
+ if (!acts.length) { host.innerHTML = ''; return; }
163
+ host.innerHTML = acts.map(a => `
164
+ <li class="activity-item">
165
+ <strong>${this.esc(a.titulo || '')}</strong>
166
+ <span>${this.esc(a.detalle || a.descripcion || '')}</span>
167
+ </li>
168
+ `).join('');
169
+ },
170
+
171
+ initials(name) {
172
+ if (!name) return '?';
173
+ const parts = String(name).trim().split(/\s+/);
174
+ const first = parts[0] ? parts[0][0] : '';
175
+ const second = parts[1] ? parts[1][0] : '';
176
+ return (first + second).toUpperCase() || '?';
177
+ },
178
+
179
+ esc(str) {
180
+ return String(str == null ? '' : str)
181
+ .replace(/&/g, '&')
182
+ .replace(/</g, '<')
183
+ .replace(/>/g, '>')
184
+ .replace(/"/g, '"')
185
+ .replace(/'/g, ''');
186
+ }
187
+};
188
+
189
+document.addEventListener('DOMContentLoaded', () => Perfil.init());
frontend/js/pages/recursos.js
deleted
-4
@@ -1,4 +0,0 @@
1
-document.addEventListener('DOMContentLoaded', () => {
2
- Layout.init('recursos.html');
3
- Layout.setPageTitle('Apuntes');
4
-});
frontend/js/pages/tips.js
deleted
-4
@@ -1,4 +0,0 @@
1
-document.addEventListener('DOMContentLoaded', () => {
2
- Layout.init('tips.html');
3
- Layout.setPageTitle('Tips Académicos');
4
-});
frontend/pages/comunidad.html
new
+75
@@ -0,0 +1,75 @@
1
+<!DOCTYPE html>
2
+<html lang="es">
3
+<head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Comunidad y Recursos | MAPS Connect</title>
7
+ <link rel="stylesheet" href="../css/base.css">
8
+ <link rel="stylesheet" href="../css/layout.css">
9
+ <link rel="stylesheet" href="../css/components.css">
10
+ <link rel="stylesheet" href="../css/comunidad.css">
11
+</head>
12
+<body>
13
+ <div class="app-layout">
14
+ <aside class="sidebar" id="sidebar"></aside>
15
+ <div class="main-wrapper">
16
+ <header class="topbar" id="topbar"></header>
17
+ <main class="main-content" id="main-content">
18
+
19
+ <section class="academic-hub-container">
20
+
21
+ <!-- 1. BARRA SUPERIOR DE FILTROS Y ACCIÓN CONTEXTUAL -->
22
+ <div class="hub-filter-bar">
23
+ <input type="text" class="input-search" id="hub-search" placeholder="Buscar por tema, duda, apunte o concepto clave...">
24
+
25
+ <select class="select-materia" id="hub-materia">
26
+ <option value="">Todas las materias del semestre</option>
27
+ </select>
28
+
29
+ <!-- Este botón cambia según la pestaña activa mediante JS -->
30
+ <button class="btn-solid" id="context-action-btn" type="button">+ Preguntar Duda</button>
31
+ </div>
32
+
33
+ <!-- 2. PESTAÑAS DE NAVEGACIÓN INTERNA -->
34
+ <nav class="hub-tabs" role="tablist">
35
+ <button class="tab-button active" type="button" role="tab"
36
+ data-tab="foro-dudas" data-action="+ Preguntar Duda">
37
+ Foro de Dudas
38
+ </button>
39
+ <button class="tab-button" type="button" role="tab"
40
+ data-tab="apuntes" data-action="+ Subir Apunte (PDF)">
41
+ Repositorio de Apuntes
42
+ </button>
43
+ <button class="tab-button" type="button" role="tab"
44
+ data-tab="tips" data-action="+ Compartir Tip">
45
+ Tips Académicos
46
+ </button>
47
+ </nav>
48
+
49
+ <!-- ================= PESTAÑA 1: FORO DE DUDAS ================= -->
50
+ <section id="foro-dudas" class="tab-panel active" role="tabpanel">
51
+ <!-- Las dudas se renderizan aquí solo cuando el backend responde -->
52
+ </section>
53
+
54
+ <!-- ================= PESTAÑA 2: APUNTES Y REPOSITORIO ================= -->
55
+ <section id="apuntes" class="tab-panel" role="tabpanel">
56
+ <!-- Los apuntes se renderizan aquí solo cuando el backend responde -->
57
+ </section>
58
+
59
+ <!-- ================= PESTAÑA 3: TIPS ACADÉMICOS ================= -->
60
+ <section id="tips" class="tab-panel" role="tabpanel">
61
+ <!-- Los tips se renderizan aquí solo cuando el backend responde -->
62
+ </section>
63
+
64
+ </section>
65
+
66
+ </main>
67
+ </div>
68
+ </div>
69
+ <script src="../js/api.js"></script>
70
+ <script src="../js/auth.js"></script>
71
+ <script src="../js/utils.js"></script>
72
+ <script src="../js/layout.js"></script>
73
+ <script src="../js/pages/comunidad.js"></script>
74
+</body>
75
+</html>
frontend/pages/foro.html
deleted
-29
@@ -1,29 +0,0 @@
1
-<!DOCTYPE html>
2
-<html lang="es">
3
-<head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Foro | MAPS Connect</title>
7
- <link rel="stylesheet" href="../css/base.css">
8
- <link rel="stylesheet" href="../css/layout.css">
9
- <link rel="stylesheet" href="../css/components.css">
10
-</head>
11
-<body>
12
- <div class="app-layout">
13
- <aside class="sidebar" id="sidebar"></aside>
14
- <div class="main-wrapper">
15
- <header class="topbar" id="topbar"></header>
16
- <main class="main-content" id="main-content">
17
-
18
- DUDAS RESUELTAS, COMENTARIOS, INTERACCIONES "LIKES" AL COMENTARIO
19
-
20
- </main>
21
- </div>
22
- </div>
23
- <script src="../js/api.js"></script>
24
- <script src="../js/auth.js"></script>
25
- <script src="../js/utils.js"></script>
26
- <script src="../js/layout.js"></script>
27
- <script src="../js/pages/foro.js"></script>
28
-</body>
29
-</html>
frontend/pages/inicio.html
+1
-4
@@ -16,10 +16,7 @@
16
<div class="main-wrapper">
17
<header class="topbar" id="topbar"></header>
18
<main class="main-content" id="main-content">
19
-
20
- hola
21
-
22
-
19
+
20
</main>
21
</div>
22
</div>
frontend/pages/mensajes.html
+32
-60
@@ -7,6 +7,7 @@
7
<link rel="stylesheet" href="../css/base.css">
8
<link rel="stylesheet" href="../css/layout.css">
9
<link rel="stylesheet" href="../css/components.css">
10
+ <link rel="stylesheet" href="../css/mensajes.css">
11
</head>
12
<body>
13
<div class="app-layout">
@@ -15,66 +16,37 @@
16
<header class="topbar" id="topbar"></header>
17
<main class="main-content" id="main-content">
18
18
- <section class="mensaje">
19
-
20
- <div class="mensajes-encabezado">
21
- <h2>Mensajes</h2>
22
-
23
- <button class="notificaciones-btn">
24
- Notificaciones
25
- </button>
26
- </div>
27
-
28
- <div class="mensajes-contenedor">
29
-
30
- <aside class="conversaciones">
31
-
32
- <h3>Conversaciones</h3>
33
-
34
- <div class="conversaciom">
35
- <p class="conversacion-nombre">Profesor</p>
36
- <p class="conversacion-preview">
37
- Ultimo mensaje
38
- </p>
39
- </div>
40
-
41
- <div class="conversacion">
42
- <p class="conversacion-nombre">Compañero</p>
43
- <p class="conversacion-previw">
44
- Nos vemos manñana
45
- </p>
46
- </div>
47
-
48
- </aside>
49
-
50
- <section class="chat">
51
-
52
- <div class="chat-emcabezado">
53
- <h3>Selecciona una conversacion</h3>
54
- </div>
55
-
56
- <div class="chat-mensajes" id="chat-mensajes">
57
-
58
- <p>Aqui van a aparecer los mensajes</p>
59
-
60
- </div>
61
-
62
- <div class="chat-escribir">
63
-
64
- <input
65
- type="text"
66
- id="mensaje-input"
67
- placeholder="Escribe un mensaje">
68
-
69
- <button id="enviar-mensaje">
70
- Enviar
71
- </button>
72
-
73
- </div>
74
-
75
- </section>
76
-
77
- </div>
19
+ <section class="messages-container">
20
+
21
+ <!-- 1. BANDEJA LATERAL IZQUIERDA -->
22
+ <aside class="chat-sidebar">
23
+ <div class="chat-search-box">
24
+ <input type="text" id="chat-search" placeholder="Buscar compañero o profesor...">
25
+ </div>
26
+ <div class="conversation-list" id="conversacion-list">
27
+ <!-- Las conversaciones se renderizan aquí solo cuando el backend responde -->
28
+ </div>
29
+ </aside>
30
+
31
+ <!-- 2. VENTANA DE CHAT DERECHA -->
32
+ <section class="chat-window">
33
+
34
+ <header class="chat-header" id="chat-header">
35
+ <!-- Cabecera del chat: se renderiza al seleccionar una conversación -->
36
+ </header>
37
+
38
+ <div class="chat-body" id="chat-body">
39
+ <div class="chat-empty" id="chat-empty">Selecciona una conversación para ver los mensajes.</div>
40
+ <!-- Los mensajes se renderizan aquí solo cuando el backend responde -->
41
+ </div>
42
+
43
+ <footer class="chat-input-bar">
44
+ <button class="btn-clean" type="button" id="btn-adjuntar" title="Adjuntar archivo o apunte">📎</button>
45
+ <input type="text" id="mensaje-input" placeholder="Escribe un mensaje..." disabled>
46
+ <button class="btn-solid" type="button" id="enviar-mensaje" disabled>Enviar</button>
47
+ </footer>
48
+
49
+ </section>
50
51
</section>
52
frontend/pages/perfil.html
+36
-31
@@ -7,6 +7,7 @@
7
<link rel="stylesheet" href="../css/base.css">
8
<link rel="stylesheet" href="../css/layout.css">
9
<link rel="stylesheet" href="../css/components.css">
10
+ <link rel="stylesheet" href="../css/perfil.css">
11
</head>
12
<body>
13
<div class="app-layout">
@@ -15,45 +16,49 @@
16
<header class="topbar" id="topbar"></header>
17
<main class="main-content" id="main-content">
18
18
- <section class="perfil">
19
+ <section class="perfil-container" id="perfil-root">
20
21
+ <!-- 1. CABECERA DE PERFIL -->
22
+ <div class="card profile-header-card">
23
+ <div class="profile-main-info" id="profile-header-info">
24
+ <!-- Avatar, nombre y datos se renderizan desde el backend (o sesión) -->
25
+ </div>
26
+ <div>
27
+ <button class="btn-clean" type="button" id="btn-editar-proposito">Editar Propósito</button>
28
+ </div>
29
+ </div>
30
21
- <div class="perfil-encabezado">
22
-
23
- <img
24
- id="perfil-foto"
25
- class="perfil-foto"
26
- src="../assets/default-avatar.png"
27
- alt="Foto de perfil"
28
- >
29
-
30
- <h2 id="perfil-nombre">
31
- Nombre del estudiante
32
- </h2>
31
+ <!-- 2. LAYOUT A DOS COLUMNAS -->
32
+ <div class="profile-grid">
33
34
- </div>
34
+ <!-- COLUMNA IZQUIERDA: TRAYECTORIA ACADÉMICA MAPS -->
35
+ <section class="card">
36
+ <h3 class="section-heading">Identidad y Ruta MAPS</h3>
37
36
- <div class="perfil-info">
38
+ <div class="purpose-box" id="profile-purpose" hidden>
39
+ <strong>Propósito de Vida</strong>
40
+ <p id="profile-purpose-text"></p>
41
+ </div>
42
38
- <div class="perfil-dato">
39
- <span>Correo</span>
40
- <p id="perfil-correo">correo@tecmilenio.mx</p>
41
- </div>
43
+ <h4 class="sub-heading">Certificados del Plan de Estudios</h4>
44
+ <div id="profile-certificados">
45
+ <!-- Los certificados se renderizan aquí solo cuando el backend responde -->
46
+ </div>
47
+ </section>
48
43
- <div class="perfil-dato">
44
- <span>Matrícula</span>
45
- <p id="perfil-matricula">00000000</p>
46
- </div>
49
+ <!-- COLUMNA DERECHA: REPUTACIÓN Y ACTIVIDAD COMUNITARIA -->
50
+ <aside class="card">
51
+ <h3 class="section-heading">Aportaciones en la Comunidad</h3>
52
48
- <div class="perfil-dato">
49
- <span>Semestre</span>
50
- <p id="perfil-semestre">1</p>
51
- </div>
53
+ <div class="stats-summary" id="profile-stats">
54
+ <!-- Las estadísticas se renderizan aquí solo cuando el backend responde -->
55
+ </div>
56
53
- <div class="perfil-dato">
54
- <span>Carrera</span>
55
- <p id="perfil-carrera">Carrera</p>
56
- </div>
57
+ <h4 class="sub-heading">Actividad Reciente</h4>
58
+ <ul class="activity-list" id="profile-actividad">
59
+ <!-- La actividad se renderiza aquí solo cuando el backend responde -->
60
+ </ul>
61
+ </aside>
62
63
</div>
64
frontend/pages/recursos.html
deleted
-29
@@ -1,29 +0,0 @@
1
-<!DOCTYPE html>
2
-<html lang="es">
3
-<head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Apuntes | MAPS Connect</title>
7
- <link rel="stylesheet" href="../css/base.css">
8
- <link rel="stylesheet" href="../css/layout.css">
9
- <link rel="stylesheet" href="../css/components.css">
10
-</head>
11
-<body>
12
- <div class="app-layout">
13
- <aside class="sidebar" id="sidebar"></aside>
14
- <div class="main-wrapper">
15
- <header class="topbar" id="topbar"></header>
16
- <main class="main-content" id="main-content">
17
-
18
- BORRAR SE VA A PONER EN TIPS
19
-
20
- </main>
21
- </div>
22
- </div>
23
- <script src="../js/api.js"></script>
24
- <script src="../js/auth.js"></script>
25
- <script src="../js/utils.js"></script>
26
- <script src="../js/layout.js"></script>
27
- <script src="../js/pages/recursos.js"></script>
28
-</body>
29
-</html>
frontend/pages/tips.html
deleted
-31
@@ -1,31 +0,0 @@
1
-<!DOCTYPE html>
2
-<html lang="es">
3
-<head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Tips | MAPS Connect</title>
7
- <link rel="stylesheet" href="../css/base.css">
8
- <link rel="stylesheet" href="../css/layout.css">
9
- <link rel="stylesheet" href="../css/components.css">
10
-</head>
11
-<body>
12
- <div class="app-layout">
13
- <aside class="sidebar" id="sidebar"></aside>
14
- <div class="main-wrapper">
15
- <header class="topbar" id="topbar"></header>
16
- <main class="main-content" id="main-content">
17
-
18
- REDDIT (CIRCULOS DE ESTUDIO), (APUNES) ESOS VAN AQUI
19
-
20
-
21
-
22
- </main>
23
- </div>
24
- </div>
25
- <script src="../js/api.js"></script>
26
- <script src="../js/auth.js"></script>
27
- <script src="../js/utils.js"></script>
28
- <script src="../js/layout.js"></script>
29
- <script src="../js/pages/tips.js"></script>
30
-</body>
31
-</html>
src/main/java/com/tecmilenio/mapsconect/dto/UsuarioDTO.java
-1
@@ -14,7 +14,6 @@ public class UsuarioDTO {
14
private Integer id;
15
private String email;
16
private String nombre;
17
- private String apellido;
17
private String rol;
18
private Boolean activo;
19
src/main/java/com/tecmilenio/mapsconect/entity/RolConverter.java
new
+23
@@ -0,0 +1,23 @@
1
+package com.tecmilenio.mapsconect.entity;
2
+
3
+import jakarta.persistence.AttributeConverter;
4
+import jakarta.persistence.Converter;
5
+
6
+/**
7
+ * Convierte el enum Rol (mayúsculas en Java) a minúsculas en la BD,
8
+ * para coincidir con el enum de MySQL: ('estudiante','profesor','administrador').
9
+ */
10
+@Converter
11
+public class RolConverter implements AttributeConverter<Usuario.Rol, String> {
12
+
13
+ @Override
14
+ public String convertToDatabaseColumn(Usuario.Rol rol) {
15
+ return rol == null ? null : rol.name().toLowerCase();
16
+ }
17
+
18
+ @Override
19
+ public Usuario.Rol convertToEntityAttribute(String dbValue) {
20
+ if (dbValue == null) return null;
21
+ return Usuario.Rol.valueOf(dbValue.toUpperCase());
22
+ }
23
+}
src/main/java/com/tecmilenio/mapsconect/entity/Usuario.java
+12
-13
@@ -21,34 +21,33 @@ public class Usuario {
21
@Column(name = "id_usuario")
22
private Integer id;
23
24
- @Column(name = "correo", nullable = false, unique = true, length = 100)
24
+ @Column(name = "correo", nullable = false, unique = true, length = 150)
25
private String email;
26
27
- @Column(nullable = false, length = 100)
28
- private String nombre;
29
-
30
- @Column(nullable = false, length = 100)
31
- private String apellido;
27
+ @Column(name = "nombre_completo", nullable = false, length = 150)
28
+ private String nombreCompleto;
29
30
@Column(name = "contrasena_hash", nullable = false)
31
private String contrasena;
32
36
- @Enumerated(EnumType.STRING)
33
+ @Convert(converter = RolConverter.class)
34
+ @Column(name = "rol", nullable = false)
35
private Rol rol;
36
37
@Builder.Default
40
- @Column(nullable = false)
41
- private Boolean activo = true;
38
+ @Column(name = "puntos_reputacion", nullable = false)
39
+ private Integer puntosReputacion = 0;
40
41
@Builder.Default
42
@Column(nullable = false)
45
- private LocalDateTime fechaCreacion = LocalDateTime.now();
43
+ private Boolean activo = true;
44
47
- @Column
48
- private LocalDateTime fechaActualizacion;
45
+ @Builder.Default
46
+ @Column(name = "fecha_registro", nullable = false)
47
+ private LocalDateTime fechaRegistro = LocalDateTime.now();
48
49
public enum Rol {
51
- ESTUDIANTE, DOCENTE, EGRESADO, ADMIN
50
+ ESTUDIANTE, PROFESOR, ADMINISTRADOR
51
}
52
53
}
src/main/java/com/tecmilenio/mapsconect/service/UsuarioService.java
+2
-4
@@ -31,8 +31,7 @@ public class UsuarioService {
31
32
Usuario usuario = Usuario.builder()
33
.email(registroDTO.getEmail())
34
- .nombre(registroDTO.getNombre())
35
- .apellido(registroDTO.getApellido())
34
+ .nombreCompleto(registroDTO.getNombre() + " " + registroDTO.getApellido())
35
.contrasena(passwordEncoder.encode(registroDTO.getContrasena()))
36
.rol(Usuario.Rol.ESTUDIANTE)
37
.activo(true)
@@ -77,8 +76,7 @@ public class UsuarioService {
76
return UsuarioDTO.builder()
77
.id(usuario.getId())
78
.email(usuario.getEmail())
80
- .nombre(usuario.getNombre())
81
- .apellido(usuario.getApellido())
79
+ .nombre(usuario.getNombreCompleto())
80
.rol(usuario.getRol().toString())
81
.activo(usuario.getActivo())
82
.build();
src/main/resources/application-dev.yml
+2
-2
@@ -1,8 +1,8 @@
1
spring:
2
datasource:
3
- url: jdbc:mysql://localhost:3306/maps_conect_dev
3
+ url: jdbc:mysql://localhost:3306/maps_conect
4
username: root
5
- password: Haloymega59!
5
+ password: Berlin007.
6
7
jpa:
8
hibernate: