chartManD commited on
Commit
d660bac
·
1 Parent(s): e353854

Implementando Napping con modalidad perfil ultra falsh

Browse files
tecnicas/controllers/views_controller/session_management/details/details_controller.py CHANGED
@@ -41,14 +41,9 @@ class DetallesController():
41
  elif technique.repeticion >= technique.repeticiones_max:
42
  return self.controllGetResponse(error="Se ha alcanzado el número de repeticiones máxima", request=request)
43
 
44
- there_participacions = Participacion.objects.filter(
45
- tecnica=technique).exists()
46
-
47
- if there_participacions:
48
- (is_update_participations,
49
- message) = ParticipacionController.outAllInSession(self.session)
50
- if not is_update_participations:
51
- return self.controllGetResponse(error=message, request=request)
52
 
53
  self.session.activo = True
54
  technique.repeticion = technique.repeticion + 1
@@ -61,3 +56,15 @@ class DetallesController():
61
  }
62
  return redirect(
63
  reverse(self.url_next, kwargs=parameters))
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  elif technique.repeticion >= technique.repeticiones_max:
42
  return self.controllGetResponse(error="Se ha alcanzado el número de repeticiones máxima", request=request)
43
 
44
+ is_update_participations = self.setParticipationsToNoFinished()
45
+ if not is_update_participations:
46
+ return self.controllGetResponse(error="Error al actualizar las participaciones", request=request)
 
 
 
 
 
47
 
48
  self.session.activo = True
49
  technique.repeticion = technique.repeticion + 1
 
56
  }
57
  return redirect(
58
  reverse(self.url_next, kwargs=parameters))
59
+
60
+ def setParticipationsToNoFinished(self):
61
+ there_participacions = Participacion.objects.filter(
62
+ tecnica=self.session.tecnica).exists()
63
+
64
+ if there_participacions:
65
+ (is_update_participations,
66
+ message) = ParticipacionController.outAllInSession(self.session)
67
+
68
+ return is_update_participations
69
+
70
+ return True
tecnicas/controllers/views_controller/session_management/details/details_napping_controller.py CHANGED
@@ -44,7 +44,7 @@ class DetallesNappingController(DetallesController):
44
 
45
  if action == "start_perfil_ultra_flash":
46
  name_mode = action.replace("start_", "").replace("_", " ")
47
- return self.controllGetResponse(error="Trabajando en la modalidad", request=request)
48
 
49
  elif action == "delete_session":
50
  self.deleteSesorialSession()
@@ -63,12 +63,19 @@ class DetallesNappingController(DetallesController):
63
  elif self.session.activo:
64
  return self.controllGetResponse(error="La sesión ya está activada", request=request)
65
 
66
- tecnique_mode = TecnicaModalidad.objects.get_or_create(
67
- tecnica=self.session.tecnica, modalidad=Modalidad.objects.get(nombre=name_mode), usando=True)
68
 
69
- if not tecnique_mode:
70
  return self.controllGetResponse(error="Modalidad no encontrada", request=request)
71
 
 
 
 
 
 
 
 
72
  self.session.activo = True
73
  self.session.save()
74
 
 
44
 
45
  if action == "start_perfil_ultra_flash":
46
  name_mode = action.replace("start_", "").replace("_", " ")
47
+ response = self.startNapping(request=request, name_mode=name_mode)
48
 
49
  elif action == "delete_session":
50
  self.deleteSesorialSession()
 
63
  elif self.session.activo:
64
  return self.controllGetResponse(error="La sesión ya está activada", request=request)
65
 
66
+ (technique_mode, created) = TecnicaModalidad.objects.get_or_create(
67
+ tecnica=self.session.tecnica, modalidad=Modalidad.objects.get(nombre=name_mode))
68
 
69
+ if not technique_mode:
70
  return self.controllGetResponse(error="Modalidad no encontrada", request=request)
71
 
72
+ technique_mode.usando = True
73
+ technique_mode.save()
74
+
75
+ is_update_participations = self.setParticipationsToNoFinished()
76
+ if not is_update_participations:
77
+ return self.controllGetResponse(error="Error al actualizar las participaciones", request=request)
78
+
79
  self.session.activo = True
80
  self.session.save()
81
 
tecnicas/controllers/views_controller/sessions_tester/init_session/init_session_napping_controller.py CHANGED
@@ -1,7 +1,7 @@
1
  from django.http import HttpRequest
2
  from django.shortcuts import render, redirect
3
  from django.urls import reverse
4
- from tecnicas.models import Participacion
5
  from tecnicas.controllers import ParticipacionController
6
  from .init_session_controller import InitSessionController
7
 
@@ -19,10 +19,7 @@ class InitSessionNappingController(InitSessionController):
19
  "has_ended": self.isEndedSession()
20
  }
21
 
22
- if self.session.tecnica.repeticion == 1:
23
- self.context["status"] = "En esta sesión se usará Napping"
24
- else:
25
- self.context["status"] = "Se uso Napping puro en la última sesión"
26
 
27
  if "error" in request.GET:
28
  self.context["error"] = request.GET["error"]
@@ -72,3 +69,12 @@ class InitSessionNappingController(InitSessionController):
72
  else:
73
  context["error"] = "Acción sin especificar"
74
  return render(request, self.current_direction, context)
 
 
 
 
 
 
 
 
 
 
1
  from django.http import HttpRequest
2
  from django.shortcuts import render, redirect
3
  from django.urls import reverse
4
+ from tecnicas.models import Participacion, TecnicaModalidad
5
  from tecnicas.controllers import ParticipacionController
6
  from .init_session_controller import InitSessionController
7
 
 
19
  "has_ended": self.isEndedSession()
20
  }
21
 
22
+ self.setStatusSession()
 
 
 
23
 
24
  if "error" in request.GET:
25
  self.context["error"] = request.GET["error"]
 
69
  else:
70
  context["error"] = "Acción sin especificar"
71
  return render(request, self.current_direction, context)
72
+
73
+ def setStatusSession(self):
74
+ technique_mode = TecnicaModalidad.objects.get(
75
+ tecnica=self.session.tecnica, usando=True)
76
+
77
+ if technique_mode.modalidad.nombre == "sin modalidad":
78
+ self.context["status"] = "La sesión usa Napping"
79
+ else:
80
+ self.context["status"] = f"La sesión usa Napping con modalidad {technique_mode.modalidad.nombre}"
tecnicas/controllers/views_controller/sessions_tester/login_session_tester_controller.py CHANGED
@@ -12,6 +12,7 @@ class LoginSessionTesterController():
12
  taster_participation: Participacion
13
  current_direcction = "tecnicas/forms_tester/login_session.html"
14
  destinity_direcction = "cata_system:catador_init_session"
 
15
 
16
  def __init__(self):
17
  self.tester = Catador()
@@ -28,17 +29,17 @@ class LoginSessionTesterController():
28
  return controller_error("Credenciales inválidas")
29
 
30
  def validateEntryEscalas(self, request=HttpRequest):
31
- context = {}
32
  if not self.session.activo:
33
- context["error"] = "La sesión no está activa actualmente"
34
- return render(request, self.current_direcction, context)
35
 
36
  if self.session.tecnica.repeticion == 1:
37
  try:
38
  self.taster_participation = Participacion.objects.get(
39
  tecnica=self.session.tecnica, catador=self.tester)
40
- context["error"] = "Usted ya esta dentro de la sesión"
41
- return render(request, self.current_direcction, context)
42
 
43
  except Participacion.DoesNotExist:
44
  with transaction.atomic():
@@ -51,8 +52,8 @@ class LoginSessionTesterController():
51
  tecnica=self.session.tecnica).count()
52
 
53
  if current_num_testers >= max_testers:
54
- context["error"] = "La sesión ha alcanzado el número máximo de catadores"
55
- return render(request, self.current_direcction, context)
56
 
57
  self.taster_participation = Participacion.objects.create(
58
  tecnica=self.session.tecnica,
@@ -64,21 +65,21 @@ class LoginSessionTesterController():
64
  }
65
  return redirect(reverse(self.destinity_direcction, kwargs=params))
66
  else:
67
- context["error"] = "Ya no es posible ingresar a la sesión"
68
- return render(request, self.current_direcction, context)
69
 
70
  def validateEntryRataCata(self, request: HttpRequest):
71
- context = {}
72
  if not self.session.activo:
73
- context["error"] = "La sesión no está activa actualmente"
74
- return render(request, self.current_direcction, context)
75
 
76
  if self.session.tecnica.repeticion <= 1:
77
  try:
78
  self.taster_participation = Participacion.objects.get(
79
  tecnica=self.session.tecnica, catador=self.tester)
80
- context["error"] = "Usted ya esta dentro de la sesión"
81
- return render(request, self.current_direcction, context)
82
  except Participacion.DoesNotExist:
83
  with transaction.atomic():
84
  code_session = self.session.codigo_sesion
@@ -96,41 +97,41 @@ class LoginSessionTesterController():
96
  }
97
  return redirect(reverse(self.destinity_direcction, kwargs=params))
98
  else:
99
- context["error"] = "Imposible acceder a esta sesión"
100
- return render(request, self.current_direcction, context)
101
 
102
  def validateEntryLimitTesters(self, request: HttpRequest):
103
- context = {}
104
  if not self.session.activo:
105
- context["error"] = "La sesión no está activa actualmente"
106
- return render(request, self.current_direcction, context)
107
 
108
  if self.session.tecnica.repeticion == 1:
109
  return self.entrySessionLimitTesters(request)
110
 
111
  else:
112
- context["error"] = "Ya no es posible ingresar a la sesión"
113
- return render(request, self.current_direcction, context)
114
 
115
  def validateEntryNapping(self, request: HttpRequest):
116
- context = {}
117
  if not self.session.activo:
118
- context["error"] = "La sesión no está activa actualmente"
119
- return render(request, self.current_direcction, context)
120
 
121
  if self.session.tecnica.repeticion == 0:
122
  return self.entrySessionLimitTesters(request)
123
 
124
  else:
125
- context["error"] = "Ya no es posible ingresar a la sesión"
126
- return render(request, self.current_direcction, context)
127
 
128
  def entrySessionLimitTesters(self, request: HttpRequest):
129
  try:
130
  self.taster_participation = Participacion.objects.get(
131
  tecnica=self.session.tecnica, catador=self.tester)
132
- context["error"] = "Usted ya esta dentro de la sesión"
133
- return render(request, self.current_direcction, context)
134
 
135
  except Participacion.DoesNotExist:
136
  try:
@@ -158,5 +159,5 @@ class LoginSessionTesterController():
158
  return redirect(reverse(self.destinity_direcction, kwargs=params))
159
 
160
  except ValueError as e:
161
- context["error"] = str(e)
162
- return render(request, self.current_direcction, context)
 
12
  taster_participation: Participacion
13
  current_direcction = "tecnicas/forms_tester/login_session.html"
14
  destinity_direcction = "cata_system:catador_init_session"
15
+ context = {}
16
 
17
  def __init__(self):
18
  self.tester = Catador()
 
29
  return controller_error("Credenciales inválidas")
30
 
31
  def validateEntryEscalas(self, request=HttpRequest):
32
+ self.context = {}
33
  if not self.session.activo:
34
+ self.context["error"] = "La sesión no está activa actualmente"
35
+ return render(request, self.current_direcction, self.context)
36
 
37
  if self.session.tecnica.repeticion == 1:
38
  try:
39
  self.taster_participation = Participacion.objects.get(
40
  tecnica=self.session.tecnica, catador=self.tester)
41
+ self.context["error"] = "Usted ya esta dentro de la sesión"
42
+ return render(request, self.current_direcction, self.context)
43
 
44
  except Participacion.DoesNotExist:
45
  with transaction.atomic():
 
52
  tecnica=self.session.tecnica).count()
53
 
54
  if current_num_testers >= max_testers:
55
+ self.context["error"] = "La sesión ha alcanzado el número máximo de catadores"
56
+ return render(request, self.current_direcction, self.context)
57
 
58
  self.taster_participation = Participacion.objects.create(
59
  tecnica=self.session.tecnica,
 
65
  }
66
  return redirect(reverse(self.destinity_direcction, kwargs=params))
67
  else:
68
+ self.context["error"] = "Ya no es posible ingresar a la sesión"
69
+ return render(request, self.current_direcction, self.context)
70
 
71
  def validateEntryRataCata(self, request: HttpRequest):
72
+ self.context = {}
73
  if not self.session.activo:
74
+ self.context["error"] = "La sesión no está activa actualmente"
75
+ return render(request, self.current_direcction, self.context)
76
 
77
  if self.session.tecnica.repeticion <= 1:
78
  try:
79
  self.taster_participation = Participacion.objects.get(
80
  tecnica=self.session.tecnica, catador=self.tester)
81
+ self.context["error"] = "Usted ya esta dentro de la sesión"
82
+ return render(request, self.current_direcction, self.context)
83
  except Participacion.DoesNotExist:
84
  with transaction.atomic():
85
  code_session = self.session.codigo_sesion
 
97
  }
98
  return redirect(reverse(self.destinity_direcction, kwargs=params))
99
  else:
100
+ self.context["error"] = "Imposible acceder a esta sesión"
101
+ return render(request, self.current_direcction, self.context)
102
 
103
  def validateEntryLimitTesters(self, request: HttpRequest):
104
+ self.context = {}
105
  if not self.session.activo:
106
+ self.context["error"] = "La sesión no está activa actualmente"
107
+ return render(request, self.current_direcction, self.context)
108
 
109
  if self.session.tecnica.repeticion == 1:
110
  return self.entrySessionLimitTesters(request)
111
 
112
  else:
113
+ self.context["error"] = "Ya no es posible ingresar a la sesión"
114
+ return render(request, self.current_direcction, self.context)
115
 
116
  def validateEntryNapping(self, request: HttpRequest):
117
+ self.context = {}
118
  if not self.session.activo:
119
+ self.context["error"] = "La sesión no está activa actualmente"
120
+ return render(request, self.current_direcction, self.context)
121
 
122
  if self.session.tecnica.repeticion == 0:
123
  return self.entrySessionLimitTesters(request)
124
 
125
  else:
126
+ self.context["error"] = "Ya no es posible ingresar a la sesión"
127
+ return render(request, self.current_direcction, self.context)
128
 
129
  def entrySessionLimitTesters(self, request: HttpRequest):
130
  try:
131
  self.taster_participation = Participacion.objects.get(
132
  tecnica=self.session.tecnica, catador=self.tester)
133
+ self.context["error"] = "Usted ya esta dentro de la sesión"
134
+ return render(request, self.current_direcction, self.context)
135
 
136
  except Participacion.DoesNotExist:
137
  try:
 
159
  return redirect(reverse(self.destinity_direcction, kwargs=params))
160
 
161
  except ValueError as e:
162
+ self.context["error"] = str(e)
163
+ return render(request, self.current_direcction, self.context)
tecnicas/controllers/views_controller/sessions_tester/tests_forms/test_napping_controller.py CHANGED
@@ -2,7 +2,8 @@ from django.http import HttpRequest
2
  from django.shortcuts import redirect, render
3
  from django.urls import reverse
4
  from django.db.models import F
5
- from tecnicas.models import Participacion, Producto, TecnicaModalidad, DatoPunto, Calificacion
 
6
  from tecnicas.utils import noValidTechnique
7
  from .general_test_controller import GenetalTestController
8
 
@@ -11,6 +12,7 @@ class TestNappingController(GenetalTestController):
11
  def __init__(self, sensorial_session, user_tester):
12
  super().__init__(sensorial_session, user_tester)
13
  self.napping_test = "tecnicas/forms_tester/test_napping.html"
 
14
 
15
  def controllGet(self, request: HttpRequest):
16
  technique = self.session.tecnica
@@ -31,11 +33,14 @@ class TestNappingController(GenetalTestController):
31
  if name_mode_activate == "sin modalidad":
32
  self.context["mode"] = "sin modalidad"
33
  return self.nappingTest(request)
 
 
 
34
  else:
35
  return noValidTechnique(
36
  name_view=self.previus_directory,
37
  query_params={
38
- "error": "La técnica no tiene modalidad activada"
39
  },
40
  params={
41
  "code_sesion": self.session.codigo_sesion
@@ -53,6 +58,24 @@ class TestNappingController(GenetalTestController):
53
 
54
  return render(request, self.napping_test, self.context)
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  def setCoordinates(self):
57
  technique = self.session.tecnica
58
 
@@ -65,10 +88,10 @@ class TestNappingController(GenetalTestController):
65
  data_points = DatoPunto.objects.filter(
66
  calificacion__in=ratings
67
  ).values(
68
- code= F("calificacion__id_producto__codigoProducto"),
69
- px= F("x"),
70
- py= F("y"),
71
- id_product= F("calificacion__id_producto")
72
  )
73
 
74
  self.context["data_points"] = list(data_points)
 
2
  from django.shortcuts import redirect, render
3
  from django.urls import reverse
4
  from django.db.models import F
5
+ from tecnicas.models import Participacion, Producto, TecnicaModalidad, DatoPunto, Calificacion, Modalidad
6
+ from tecnicas.forms import ListWordsForm
7
  from tecnicas.utils import noValidTechnique
8
  from .general_test_controller import GenetalTestController
9
 
 
12
  def __init__(self, sensorial_session, user_tester):
13
  super().__init__(sensorial_session, user_tester)
14
  self.napping_test = "tecnicas/forms_tester/test_napping.html"
15
+ self.napping_puf_test = "tecnicas/forms_tester/test_napping_puf.html"
16
 
17
  def controllGet(self, request: HttpRequest):
18
  technique = self.session.tecnica
 
33
  if name_mode_activate == "sin modalidad":
34
  self.context["mode"] = "sin modalidad"
35
  return self.nappingTest(request)
36
+ if name_mode_activate == "perfil ultra flash":
37
+ self.context["mode"] = "perfil ultra flash"
38
+ return self.nappingPufTest(request)
39
  else:
40
  return noValidTechnique(
41
  name_view=self.previus_directory,
42
  query_params={
43
+ "error": f"Trabajando en la modalidad: {name_mode_activate}"
44
  },
45
  params={
46
  "code_sesion": self.session.codigo_sesion
 
58
 
59
  return render(request, self.napping_test, self.context)
60
 
61
+ def nappingPufTest(self, request: HttpRequest):
62
+ maked_previus_napping = TecnicaModalidad.objects.get(
63
+ tecnica=self.session.tecnica,
64
+ modalidad=Modalidad.objects.get(nombre="sin modalidad")
65
+ )
66
+
67
+ self.context["maked_napping"] = True if maked_previus_napping else False
68
+ self.context["mode"] = "perfil ultra flash"
69
+ self.context["form"] = ListWordsForm()
70
+
71
+ self.context["session"] = self.session
72
+ technique = self.session.tecnica
73
+ products_in_technique = Producto.objects.filter(id_tecnica=technique)
74
+ self.context["products"] = products_in_technique
75
+ self.setCoordinates()
76
+
77
+ return render(request, self.napping_puf_test, self.context)
78
+
79
  def setCoordinates(self):
80
  technique = self.session.tecnica
81
 
 
88
  data_points = DatoPunto.objects.filter(
89
  calificacion__in=ratings
90
  ).values(
91
+ code=F("calificacion__id_producto__codigoProducto"),
92
+ px=F("x"),
93
+ py=F("y"),
94
+ id_product=F("calificacion__id_producto")
95
  )
96
 
97
  self.context["data_points"] = list(data_points)
tecnicas/static/js/test-napping-plane.js ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const planeContainer = document.getElementById('napping-plane');
2
+ const productsContainer = document.getElementById('items');
3
+ const products = document.querySelectorAll('.item-product');
4
+
5
+ // Configuration for the physical dimensions of the tablecloth (in cm)
6
+ const PHYSICAL_WIDTH = 60;
7
+ const PHYSICAL_HEIGHT = 40;
8
+
9
+ let selectedProductCode = null;
10
+ let selectedProductId = null;
11
+
12
+ // Object to store coordinates: { "CODE": { x: 10.5, y: 20.1, id: 123 } }
13
+ window.placedPoints = {};
14
+
15
+ const modeElement = document.querySelector('[data-mode]');
16
+ window.isUltraFlash = modeElement && modeElement.dataset.mode.toLowerCase().includes('ultra flash') || None;
17
+
18
+ if (window.isUltraFlash) {
19
+ document.getElementById("question-save").classList.add("hidden");
20
+ window.isPlacementActive = true;
21
+ } else {
22
+ window.isPlacementActive = false;
23
+ }
24
+
25
+ // 1. Handle Product Selection
26
+ products.forEach(product => {
27
+ product.addEventListener('click', () => {
28
+ if (!window.isPlacementActive) return;
29
+
30
+ // Remove selection from others
31
+ products.forEach(p => p.classList.remove('ring-4', 'ring-primary'));
32
+
33
+ // Select current
34
+ product.classList.add('ring-4', 'ring-primary');
35
+ selectedProductCode = product.dataset.code;
36
+ selectedProductId = product.dataset.idProduct;
37
+ });
38
+ });
39
+
40
+ // 2. Handle Plane Click (Placing Points)
41
+ planeContainer.addEventListener('click', (e) => {
42
+ // If placement is not active, do nothing (or handle differently in other script)
43
+ if (!window.isPlacementActive) return;
44
+
45
+ if (!selectedProductCode) {
46
+ spanNotifaction("Por favor, selecciona un producto primero")
47
+ return;
48
+ }
49
+
50
+ const rect = planeContainer.getBoundingClientRect();
51
+
52
+ // Calculate click position relative to the container (in pixels)
53
+ const xPixel = e.clientX - rect.left;
54
+ const yPixel = e.clientY - rect.top;
55
+
56
+ // Calculate scaled coordinates (0 to PHYSICAL_DIMENSIONS)
57
+ // X axis: 0 on left, 60 on right
58
+ const xCoord = (xPixel / rect.width) * PHYSICAL_WIDTH;
59
+
60
+ // Y axis: 0 on bottom, 40 on top (Invert Y because DOM Y is 0 at top)
61
+ const yCoord = PHYSICAL_HEIGHT - ((yPixel / rect.height) * PHYSICAL_HEIGHT);
62
+
63
+ // Update Data Object
64
+ window.placedPoints[selectedProductCode] = {
65
+ x: parseFloat(xCoord.toFixed(2)),
66
+ y: parseFloat(yCoord.toFixed(2)),
67
+ id: selectedProductId
68
+ };
69
+
70
+ // Render Point
71
+ window.renderPoint(selectedProductCode, xPixel, yPixel, xCoord, yCoord);
72
+ });
73
+
74
+ // Make renderPoint global
75
+ window.renderPoint = function (code, xPx, yPx, xVal, yVal) {
76
+ // Remove existing point for this product if it exists
77
+ const existingPoint = document.getElementById(`point-${code}`);
78
+ if (existingPoint) {
79
+ existingPoint.remove();
80
+ }
81
+
82
+ const point = document.createElement('div');
83
+ point.id = `point-${code}`;
84
+ point.className = 'data-point absolute w-4 h-4 bg-red-600 rounded-full transform -translate-x-1/2 -translate-y-1/2 cursor-pointer border-2 border-white shadow-md group';
85
+ point.dataset.code = code;
86
+ point.dataset.px = xVal;
87
+ point.dataset.py = yVal;
88
+ point.dataset.idProduct = window.placedPoints[code]?.id;
89
+
90
+ point.style.left = `${xPx}px`;
91
+ point.style.top = `${yPx}px`;
92
+
93
+ const label = document.createElement('div');
94
+ label.className = 'absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-800 text-white text-xs rounded whitespace-nowrap z-10 hidden group-hover:block';
95
+ label.innerHTML = `
96
+ <strong>${code}</strong><br>
97
+ X: ${xVal.toFixed(1)}<br>
98
+ Y: ${yVal.toFixed(1)}
99
+ `;
100
+
101
+ point.appendChild(label);
102
+ planeContainer.appendChild(point);
103
+
104
+ const textLabel = document.createElement('span');
105
+ textLabel.className = 'absolute top-4 left-1/2 transform -translate-x-1/2 text-xs font-bold text-gray-700 pointer-events-none';
106
+ textLabel.innerText = code;
107
+ point.appendChild(textLabel);
108
+ }
109
+
110
+ function checkPoints() {
111
+ const points = document.querySelectorAll('.data-point');
112
+
113
+ points.forEach(point => {
114
+ const code = point.dataset.code;
115
+
116
+ const xVal = parseFloat(point.dataset.px);
117
+ const yVal = parseFloat(point.dataset.py);
118
+
119
+ const rect = planeContainer.getBoundingClientRect();
120
+
121
+ const px = (xVal / PHYSICAL_WIDTH) * rect.width;
122
+ const py = ((PHYSICAL_HEIGHT - yVal) / PHYSICAL_HEIGHT) * rect.height;
123
+
124
+ window.placedPoints[code] = {
125
+ x: xVal.toFixed(2),
126
+ y: yVal.toFixed(2),
127
+ id: point.dataset.idProduct
128
+ };
129
+
130
+ window.renderPoint(code, px, py, xVal, yVal);
131
+ });
132
+ }
133
+
134
+ setTimeout(checkPoints, 100);
135
+
136
+ /*
137
+ ////
138
+ //////
139
+ //////// Question to finish session
140
+ //////
141
+ ////
142
+ */
143
+
144
+ function showOptionsSave() {
145
+ document.getElementById("question-save").classList.add("hidden");
146
+ document.getElementById("finish-session").classList.remove("hidden");
147
+ document.getElementById("cancel-save").classList.remove("hidden");
148
+ }
149
+
150
+ function showQuestionSave() {
151
+ document.getElementById("question-save").classList.remove("hidden");
152
+ document.getElementById("finish-session").classList.add("hidden");
153
+ document.getElementById("cancel-save").classList.add("hidden");
154
+ }
155
+
156
+ document
157
+ .getElementById("question-save")
158
+ .addEventListener("click", showOptionsSave);
159
+
160
+ document
161
+ .getElementById("cancel-save")
162
+ .addEventListener("click", showQuestionSave);
163
+
164
+ /*
165
+ ////
166
+ //////
167
+ //////// Save data and finish session
168
+ //////
169
+ ////
170
+ */
171
+
172
+ window.saveData = async function () {
173
+ const codeProducts = Object.keys(window.placedPoints);
174
+ const data = [];
175
+
176
+ if (products.length != codeProducts.length) {
177
+ spanNotifaction("Por favor, coloca todos los puntos")
178
+ return;
179
+ }
180
+
181
+ codeProducts.forEach((code) => {
182
+ const point = window.placedPoints[code];
183
+
184
+ const objData = {
185
+ code: code,
186
+ x: point.x,
187
+ y: point.y,
188
+ idProduct: point.id
189
+ };
190
+
191
+ data.push(objData);
192
+ })
193
+
194
+ const URL = "/cata/testers/api/rating-napping/no-mode"
195
+ const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
196
+
197
+ try {
198
+ const response = await fetch(URL, {
199
+ method: "POST",
200
+ headers: {
201
+ "Content-Type": "application/json",
202
+ "X-CSRFToken": csrfToken,
203
+ },
204
+ body: JSON.stringify(data),
205
+ })
206
+
207
+ if (!response.ok) {
208
+ spanNotifaction("Error en la respuesta del servidor")
209
+ return false;
210
+ }
211
+
212
+ const result = await response.json()
213
+
214
+ if (result.error) {
215
+ spanNotifaction(result.error)
216
+ return false
217
+ } else {
218
+ spanNotifaction(result.message, false)
219
+ return true
220
+ }
221
+ } catch (error) {
222
+ spanNotifaction("Error en proceso de guardar los datos")
223
+ return false
224
+ }
225
+ }
226
+
227
+ document
228
+ .getElementById("save-progress")
229
+ .addEventListener("click", window.saveData);
tecnicas/static/js/test-napping-ultra-flash.js ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Store words: { "CODE": ["word1", "word2"] }
2
+ const productWords = {};
3
+ if (window.isUltraFlash) {
4
+ initUltraFlash(productWords);
5
+ }
6
+
7
+ function initUltraFlash(productWords) {
8
+ const continueBtn = document.getElementById('continue-description');
9
+ const questionSaveBtn = document.getElementById('question-save');
10
+ const saveProgressBtn = document.getElementById('save-progress');
11
+ const dialog = document.getElementById('word-dialog');
12
+ const wordForm = document.getElementById('word-form');
13
+ const wordInput = document.getElementById('word-input');
14
+ const wordList = document.getElementById('word-list');
15
+ const dialogProductCode = document.getElementById('dialog-product-code');
16
+
17
+ let isDescriptionPhase = false;
18
+ let currentProductCode = null;
19
+
20
+ const points = document.querySelectorAll('.data-point');
21
+ let hasExistingWords = false;
22
+
23
+ // Check if there are existing words
24
+ points.forEach(point => {
25
+ const code = point.dataset.code;
26
+ const wordsAttr = point.dataset.words;
27
+
28
+ if (wordsAttr) {
29
+ productWords[code] = wordsAttr.split(',').filter(w => w);
30
+ hasExistingWords = true;
31
+ }
32
+ });
33
+
34
+ if (hasExistingWords) {
35
+ startDescriptionPhase();
36
+ points.forEach(point => {
37
+ const code = point.dataset.code;
38
+ updatePointLabel(code);
39
+ console.log(productWords[code]);
40
+ });
41
+ } else {
42
+ continueBtn.classList.remove('hidden');
43
+ }
44
+
45
+ // Check if all products are placed
46
+ continueBtn.addEventListener('click', () => {
47
+ const placedCount = Object.keys(window.placedPoints).length;
48
+ const totalProducts = document.querySelectorAll('.item-product').length;
49
+
50
+ if (placedCount !== totalProducts) {
51
+ spanNotifaction("Por favor, coloca todos los productos antes de continuar.");
52
+ return;
53
+ }
54
+
55
+ startDescriptionPhase();
56
+ });
57
+
58
+ function startDescriptionPhase() {
59
+ isDescriptionPhase = true;
60
+ window.isPlacementActive = false;
61
+ continueBtn.classList.add('hidden');
62
+ document.getElementById("question-save").classList.remove("hidden");
63
+
64
+ spanNotifaction("Fase de descripción: Haz clic en un punto para agregar palabras.", false);
65
+
66
+ document.getElementById('napping-plane').classList.remove('cursor-crosshair');
67
+ document.getElementById('napping-plane').classList.add('cursor-default');
68
+ document.querySelectorAll('.item-product').forEach(p => p.classList.remove('ring-4', 'ring-primary'))
69
+ }
70
+
71
+ // Handle Point Click for Description
72
+ // We need to attach this to the plane or points.
73
+ // Since points are re-rendered, delegating to plane is better, or hooking into renderPoint.
74
+ // But renderPoint is in the other file.
75
+ // Let's use event delegation on the plane, but we need to catch the click on the point.
76
+
77
+ document.getElementById('napping-plane').addEventListener('click', (e) => {
78
+ if (!isDescriptionPhase) return;
79
+
80
+ const point = e.target.closest('.data-point');
81
+
82
+ if (point) {
83
+ e.stopPropagation();
84
+ openWordDialog(point.dataset.code);
85
+ }
86
+ });
87
+
88
+ function openWordDialog(code) {
89
+ currentProductCode = code;
90
+ dialogProductCode.innerText = code;
91
+ renderWordListInDialog();
92
+ dialog.showModal();
93
+ }
94
+
95
+ function renderWordListInDialog() {
96
+ wordList.innerHTML = '';
97
+ const words = productWords[currentProductCode] || [];
98
+
99
+ words.forEach((word, index) => {
100
+ const badge = document.createElement('div');
101
+ badge.className = 'badge badge-secondary gap-2 p-3';
102
+ badge.innerHTML = `
103
+ ${word}
104
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-4 h-4 stroke-current cursor-pointer remove-word" data-index="${index}"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
105
+ `;
106
+
107
+ badge.querySelector('.remove-word').addEventListener('click', () => {
108
+ removeWord(index);
109
+ });
110
+
111
+ wordList.appendChild(badge);
112
+ });
113
+
114
+ // Update visualization on the plane
115
+ updatePointLabel(currentProductCode);
116
+ }
117
+
118
+ function addWord(word) {
119
+ if (!productWords[currentProductCode]) {
120
+ productWords[currentProductCode] = [];
121
+ }
122
+
123
+ if (productWords[currentProductCode].length >= 5) {
124
+ spanNotifaction("Máximo 5 palabras por producto.");
125
+ return;
126
+ }
127
+
128
+ if (productWords[currentProductCode].includes(word)) {
129
+ spanNotifaction("Palabra duplicada");
130
+ return;
131
+ }
132
+
133
+ productWords[currentProductCode].push(word);
134
+ renderWordListInDialog();
135
+ }
136
+
137
+ function removeWord(index) {
138
+ if (productWords[currentProductCode]) {
139
+ productWords[currentProductCode].splice(index, 1);
140
+ renderWordListInDialog();
141
+ }
142
+ }
143
+
144
+ wordForm.addEventListener('submit', (e) => {
145
+ e.preventDefault();
146
+ const word = wordInput.value.trim();
147
+ if (word) {
148
+ addWord(word);
149
+ wordInput.value = '';
150
+ wordInput.focus();
151
+ }
152
+ });
153
+
154
+ function updatePointLabel(code) {
155
+ const point = document.getElementById(`point-${code}`);
156
+ if (!point) return;
157
+
158
+ // Find or create the words container below the point
159
+ const tooltip = point.querySelector('.group-hover\\:block');
160
+ if (tooltip) {
161
+ // Rebuild tooltip content
162
+ const xVal = parseFloat(point.dataset.px).toFixed(1);
163
+ const yVal = parseFloat(point.dataset.py).toFixed(1);
164
+ const words = productWords[code] || [];
165
+
166
+ let wordsHtml = '';
167
+ if (words.length > 0) {
168
+ wordsHtml = `<div class="mt-1 pt-1 border-t border-gray-600 text-yellow-300 italic">${words.join(', ')}</div>`;
169
+ }
170
+
171
+ tooltip.innerHTML = `
172
+ <strong>${code}</strong><br>
173
+ X: ${xVal}<br>
174
+ Y: ${yVal}
175
+ ${wordsHtml}
176
+ `;
177
+ }
178
+ }
179
+
180
+ // Override saveData to include words
181
+ // We need to hook into the existing saveData or replace it.
182
+ // Since we made saveData global, we can wrap it.
183
+
184
+ const originalSaveData = window.saveData;
185
+
186
+ window.saveData = async function () {
187
+ // If in description phase, validate words
188
+ if (isDescriptionPhase) {
189
+ const codes = Object.keys(window.placedPoints);
190
+ for (const code of codes) {
191
+ const words = productWords[code] || [];
192
+ if (words.length < 3) {
193
+ spanNotifaction(`El producto ${code} debe tener al menos 3 palabras.`);
194
+ return false;
195
+ }
196
+ }
197
+ }
198
+
199
+ // Prepare data
200
+ const codeProducts = Object.keys(window.placedPoints);
201
+ const data = [];
202
+
203
+ if (document.querySelectorAll('.item-product').length != codeProducts.length) {
204
+ spanNotifaction("Por favor, coloca todos los puntos")
205
+ return false;
206
+ }
207
+
208
+ codeProducts.forEach((code) => {
209
+ const point = window.placedPoints[code];
210
+ const words = productWords[code] || [];
211
+
212
+ const objData = {
213
+ code: code,
214
+ x: point.x,
215
+ y: point.y,
216
+ idProduct: point.id,
217
+ words: words // Add words here
218
+ };
219
+
220
+ data.push(objData);
221
+ })
222
+
223
+ // We can reuse the rest of the logic, but we need to send the data.
224
+ // The original function constructs data inside it. We can't easily inject data into it unless we rewrite it.
225
+ // So I will rewrite the fetch part here.
226
+
227
+ const URL = "/cata/testers/api/rating-napping/no-mode"
228
+ const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
229
+
230
+ try {
231
+ const response = await fetch(URL, {
232
+ method: "POST",
233
+ headers: {
234
+ "Content-Type": "application/json",
235
+ "X-CSRFToken": csrfToken,
236
+ },
237
+ body: JSON.stringify(data),
238
+ })
239
+
240
+ if (!response.ok) {
241
+ spanNotifaction("Error en la respuesta del servidor")
242
+ return false;
243
+ }
244
+
245
+ const result = await response.json()
246
+
247
+ if (result.error) {
248
+ spanNotifaction(result.error)
249
+ return false
250
+ } else {
251
+ spanNotifaction(result.message, false)
252
+ return true
253
+ }
254
+ } catch (error) {
255
+ spanNotifaction("Error en proceso de guardar los datos")
256
+ return false
257
+ }
258
+ }
259
+ }
tecnicas/static/js/test-napping.js DELETED
@@ -1,209 +0,0 @@
1
- const planeContainer = document.getElementById('napping-plane');
2
- const productsContainer = document.getElementById('items');
3
- const products = document.querySelectorAll('.item-product');
4
-
5
- // Configuration for the physical dimensions of the tablecloth (in cm)
6
- const PHYSICAL_WIDTH = 60;
7
- const PHYSICAL_HEIGHT = 40;
8
-
9
- let selectedProductCode = null;
10
- let selectedProductId = null;
11
-
12
- // Object to store coordinates: { "CODE": { x: 10.5, y: 20.1, id: 123 } }
13
- const placedPoints = {};
14
-
15
- // 1. Handle Product Selection
16
- products.forEach(product => {
17
- product.addEventListener('click', () => {
18
- // Remove selection from others
19
- products.forEach(p => p.classList.remove('ring-4', 'ring-primary'));
20
-
21
- // Select current
22
- product.classList.add('ring-4', 'ring-primary');
23
- selectedProductCode = product.dataset.code;
24
- selectedProductId = product.dataset.idProduct;
25
- });
26
- });
27
-
28
- // 2. Handle Plane Click (Placing Points)
29
- planeContainer.addEventListener('click', (e) => {
30
- if (!selectedProductCode) {
31
- spanNotifaction("Por favor, selecciona un producto primero")
32
- return;
33
- }
34
-
35
- const rect = planeContainer.getBoundingClientRect();
36
-
37
- // Calculate click position relative to the container (in pixels)
38
- const xPixel = e.clientX - rect.left;
39
- const yPixel = e.clientY - rect.top;
40
-
41
- // Calculate scaled coordinates (0 to PHYSICAL_DIMENSIONS)
42
- // X axis: 0 on left, 60 on right
43
- const xCoord = (xPixel / rect.width) * PHYSICAL_WIDTH;
44
-
45
- // Y axis: 0 on bottom, 40 on top (Invert Y because DOM Y is 0 at top)
46
- const yCoord = PHYSICAL_HEIGHT - ((yPixel / rect.height) * PHYSICAL_HEIGHT);
47
-
48
- // Update Data Object
49
- placedPoints[selectedProductCode] = {
50
- x: parseFloat(xCoord.toFixed(2)),
51
- y: parseFloat(yCoord.toFixed(2)),
52
- id: selectedProductId
53
- };
54
-
55
- // Render Point
56
- renderPoint(selectedProductCode, xPixel, yPixel, xCoord, yCoord);
57
- });
58
-
59
- function renderPoint(code, xPx, yPx, xVal, yVal) {
60
- // Remove existing point for this product if it exists
61
- const existingPoint = document.getElementById(`point-${code}`);
62
- if (existingPoint) {
63
- existingPoint.remove();
64
- }
65
-
66
- const point = document.createElement('div');
67
- point.id = `point-${code}`;
68
- point.className = 'absolute w-4 h-4 bg-red-600 rounded-full transform -translate-x-1/2 -translate-y-1/2 cursor-pointer border-2 border-white shadow-md group';
69
- point.style.left = `${xPx}px`;
70
- point.style.top = `${yPx}px`;
71
-
72
- // Tooltip/Label
73
- const label = document.createElement('div');
74
- label.className = 'absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-800 text-white text-xs rounded whitespace-nowrap z-10 hidden group-hover:block';
75
- label.innerHTML = `
76
- <strong>${code}</strong><br>
77
- X: ${xVal.toFixed(1)}<br>
78
- Y: ${yVal.toFixed(1)}
79
- `;
80
-
81
- point.appendChild(label);
82
- planeContainer.appendChild(point);
83
-
84
- const textLabel = document.createElement('span');
85
- textLabel.className = 'absolute top-4 left-1/2 transform -translate-x-1/2 text-xs font-bold text-gray-700 pointer-events-none';
86
- textLabel.innerText = code;
87
- point.appendChild(textLabel);
88
- }
89
-
90
- function checkPoints() {
91
- const points = document.querySelectorAll('.data-point');
92
-
93
- points.forEach(point => {
94
- const code = point.dataset.code;
95
-
96
- const xVal = parseFloat(point.dataset.px);
97
- const yVal = parseFloat(point.dataset.py);
98
-
99
- const rect = planeContainer.getBoundingClientRect();
100
-
101
- const px = (xVal / PHYSICAL_WIDTH) * rect.width;
102
- const py = ((PHYSICAL_HEIGHT - yVal) / PHYSICAL_HEIGHT) * rect.height;
103
-
104
- placedPoints[code] = {
105
- x: parseFloat(xVal.toFixed(2)),
106
- y: parseFloat(yVal.toFixed(2)),
107
- id: point.dataset.idProduct
108
- };
109
-
110
- renderPoint(code, px, py, xVal, yVal);
111
- });
112
- }
113
-
114
- checkPoints();
115
-
116
- /*
117
- ////
118
- //////
119
- //////// Question to finish session
120
- //////
121
- ////
122
- */
123
-
124
- function showOptionsSave() {
125
- document.getElementById("question-save").classList.add("hidden");
126
- document.getElementById("finish-session").classList.remove("hidden");
127
- document.getElementById("cancel-save").classList.remove("hidden");
128
- }
129
-
130
- function showQuestionSave() {
131
- document.getElementById("question-save").classList.remove("hidden");
132
- document.getElementById("finish-session").classList.add("hidden");
133
- document.getElementById("cancel-save").classList.add("hidden");
134
- }
135
-
136
- document
137
- .getElementById("question-save")
138
- .addEventListener("click", showOptionsSave);
139
-
140
- document
141
- .getElementById("cancel-save")
142
- .addEventListener("click", showQuestionSave);
143
-
144
- /*
145
- ////
146
- //////
147
- //////// Save data and finish session
148
- //////
149
- ////
150
- */
151
-
152
- async function saveData() {
153
- const codeProducts = Object.keys(placedPoints);
154
- const data = [];
155
-
156
- if (products.length != codeProducts.length) {
157
- spanNotifaction("Por favor, coloca todos los puntos")
158
- return;
159
- }
160
-
161
- codeProducts.forEach((code) => {
162
- const point = placedPoints[code];
163
-
164
- const objData = {
165
- code: code,
166
- x: point.x,
167
- y: point.y,
168
- idProduct: point.id
169
- };
170
-
171
- data.push(objData);
172
- })
173
-
174
- const URL = "/cata/testers/api/rating-napping/no-mode"
175
- const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
176
-
177
- try {
178
- const response = await fetch(URL, {
179
- method: "POST",
180
- headers: {
181
- "Content-Type": "application/json",
182
- "X-CSRFToken": csrfToken,
183
- },
184
- body: JSON.stringify(data),
185
- })
186
-
187
- if (!response.ok) {
188
- spanNotifaction("Error en la respuesta del servidor")
189
- return false;
190
- }
191
-
192
- const result = await response.json()
193
-
194
- if (result.error) {
195
- spanNotifaction(result.error)
196
- return false
197
- } else {
198
- spanNotifaction(result.message, false)
199
- return true
200
- }
201
- } catch (error) {
202
- spanNotifaction("Error en proceso de guardar los datos")
203
- return false
204
- }
205
- }
206
-
207
- document
208
- .getElementById("save-progress")
209
- .addEventListener("click", saveData);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tecnicas/templates/tecnicas/components/dialog-nap-puf.html ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <dialog id="word-dialog" class="modal">
2
+ <div class="modal-box bg-surface-ligt">
3
+ <h3 class="font-bold text-lg">
4
+ Describir producto
5
+ <span id="dialog-product-code"></span>
6
+ </h3>
7
+ <p class="py-4">Agrega palabras que describan este producto (máximo 5).</p>
8
+ <form id="word-form" class="flex gap-2 mb-4">
9
+ <input type="text" id="word-input" placeholder="Palabra..."
10
+ class="input input-bordered bg-surface-light w-full text-black" autocomplete="off">
11
+ <button type="submit" class="btn btn-primary">Agregar</button>
12
+ </form>
13
+
14
+ <div id="word-list" class="flex flex-wrap gap-2 mb-4 min-h-[50px]"></div>
15
+
16
+ <div class="modal-action">
17
+ <form method="dialog">
18
+ <button class="btn">Cerrar</button>
19
+ </form>
20
+ </div>
21
+ </div>
22
+ <form method="dialog" class="modal-backdrop">
23
+ <button>close</button>
24
+ </form>
25
+ </dialog>
tecnicas/templates/tecnicas/forms_tester/test_napping.html CHANGED
@@ -41,8 +41,8 @@
41
  </section>
42
 
43
  <section class="flex items-center justify-center flex-wrap gap-4 bg-surface-ligt p-2 rounded-lg">
44
- <p class="text-xl font-bold text-center capitalize">
45
- Modalidad: {% if mode != "sin modalidad" %}{{ mode }} {% else %} Nappging {% endif %}
46
  </p>
47
  </section>
48
  </article>
@@ -123,6 +123,11 @@
123
  <button id="save-progress" class="cts-btn-general cts-btn-primary btn-push">
124
  Guardar progreso
125
  </button>
 
 
 
 
 
126
  <div class="flex gap-2">
127
  <button id="question-save" class="cts-btn-general cts-btn-secondary btn-push flex-1">
128
  ¿Finalizar sesión?
@@ -138,7 +143,6 @@
138
  <span id="loading-data-save" class="loading loading-spinner loading-xl text-accent hidden"></span>
139
  </section>
140
  </article>
141
-
142
  {% include "../components/toast-container.html" %}
143
  </article>
144
  </article>
@@ -146,5 +150,5 @@
146
 
147
  {% block extra_js %}
148
  <script src="{% static 'js/actions-form.js' %}"></script>
149
- <script src="{% static 'js/test-napping.js' %}"></script>
150
  {% endblock %}
 
41
  </section>
42
 
43
  <section class="flex items-center justify-center flex-wrap gap-4 bg-surface-ligt p-2 rounded-lg">
44
+ <p class="text-xl font-bold text-center capitalize" data-mode="{{ mode }}">
45
+ Modalidad: {% if mode != "sin modalidad" %}{{ mode }} {% else %} Napping {% endif %}
46
  </p>
47
  </section>
48
  </article>
 
123
  <button id="save-progress" class="cts-btn-general cts-btn-primary btn-push">
124
  Guardar progreso
125
  </button>
126
+ {% if mode == "perfil ultra flash" %}
127
+ <button id="continue-description" class="cts-btn-general cts-btn-primary btn-push hidden">
128
+ Continuar a descripción
129
+ </button>
130
+ {% endif %}
131
  <div class="flex gap-2">
132
  <button id="question-save" class="cts-btn-general cts-btn-secondary btn-push flex-1">
133
  ¿Finalizar sesión?
 
143
  <span id="loading-data-save" class="loading loading-spinner loading-xl text-accent hidden"></span>
144
  </section>
145
  </article>
 
146
  {% include "../components/toast-container.html" %}
147
  </article>
148
  </article>
 
150
 
151
  {% block extra_js %}
152
  <script src="{% static 'js/actions-form.js' %}"></script>
153
+ <script src="{% static 'js/test-napping-plane.js' %}"></script>
154
  {% endblock %}
tecnicas/templates/tecnicas/forms_tester/test_napping_puf.html ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'tecnicas/layouts/base.html' %}
2
+
3
+ {% load static %}
4
+
5
+ {% block title %}Napping{% endblock %}
6
+
7
+ {% block content %}
8
+ <article class="cts-container-main">
9
+ <article class="cts-wrap-content text-black max-w-4xl">
10
+ <header class="text-center flex-row w-full items-stretch flex justify-around flex-wrap gap-2">
11
+ <h1 class="rounded font-bold text-2xl bg-surface-ligt p-4 flex-1">
12
+ Sesión usando <br>técnica
13
+ <span class="uppercase">{{ session.tecnica.tipo_tecnica }}</span>
14
+ </h1>
15
+ <button class="cts-btn-general cts-btn-error btn-push" onclick="exit_sesion('form-actions')">
16
+ Salir de la sesión
17
+ </button>
18
+ </header>
19
+
20
+ <article class="hidden">
21
+ <form action="{% url 'cata_system:catador_init_session' code_sesion=session.codigo_sesion %}" method="post"
22
+ class="form-actions">
23
+ {% csrf_token %}
24
+ <input type="hidden" name="action" class="action-input">
25
+ </form>
26
+ </article>
27
+
28
+ <section class="hidden">
29
+ <input type="hidden" value="{{ session.tecnica.id }}" name="id-tecnica" class="ct-input-id-tech">
30
+ </section>
31
+
32
+ {% if error %}
33
+ {% include "../components/error-message.html" with message=error %}
34
+ {% endif %}
35
+
36
+ <article class="rounded flex flex-col gap-4">
37
+ <section class="flex items-center justify-center bg-surface-ligt p-2 rounded-lg">
38
+ <p class="text-lg font-medium text-center">
39
+ {{ session.tecnica.instrucciones }}
40
+ </p>
41
+ </section>
42
+
43
+ <section class="flex items-center justify-center flex-wrap gap-4 bg-surface-ligt p-2 rounded-lg">
44
+ <p class="text-xl font-bold text-center capitalize" data-mode="{{ mode }}">
45
+ Modalidad: {% if mode != "sin modalidad" %}{{ mode }} {% else %} Nappging {% endif %}
46
+ </p>
47
+ </section>
48
+ </article>
49
+
50
+ <article class="container-rating-word p-2 py-6 space-y-6 bg-surface-ligt rounded min-w-3xl">
51
+ <h2 class="text-xl font-bold">Productos en la sesión</h2>
52
+ <div id="items" class="original-products flex gap-4 flex-wrap justify-center"
53
+ data-original-products="original">
54
+ {% for product in products %}
55
+ <div class="item-product bg-btn-secondary text-black font-bold px-4 py-2 rounded cursor-grab transition-all"
56
+ data-id-product="{{ product.id }}" data-code="{{ product.codigoProducto }}">
57
+ {{ product.codigoProducto }}
58
+ </div>
59
+ {% endfor %}
60
+ </div>
61
+
62
+ <!-- Cartesian Plane Container -->
63
+ <div class="flex justify-center w-full">
64
+ <div class="relative w-full max-w-[800px] aspect-[3/2] bg-white border-2 border-gray-400 shadow-inner cursor-crosshair"
65
+ id="napping-plane"
66
+ style="background-image: linear-gradient(#e5e7eb 1px, transparent 1px), linear-gradient(90deg, #e5e7eb 1px, transparent 1px); background-size: 20px 20px;">
67
+
68
+ <span class="absolute bottom-1 right-2 text-xs text-gray-500 font-bold">60cm (X)</span>
69
+ <span class="absolute top-2 left-1 text-xs text-gray-500 font-bold">40cm (Y)</span>
70
+ <span class="absolute bottom-1 left-1 text-xs text-gray-500 font-bold">0</span>
71
+
72
+ {% for point in data_points %}
73
+ <div id="point-{{ point.code }}"
74
+ class="data-point absolute w-4 h-4 bg-red-600 rounded-full transform -translate-x-1/2 -translate-y-1/2 cursor-pointer border-2 border-white shadow-md group"
75
+ data-px="{{ point.px }}" data-py="{{ point.py }}" data-code="{{ point.code }}"
76
+ data-id-product="{{ point.id_product }}" {% if point.words %}
77
+ data-words="{{ point.words|join:',' }}" {% endif %}>
78
+ <div
79
+ class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-800 text-white text-xs rounded whitespace-nowrap z-10 hidden group-hover:block">
80
+ <strong>{{ point.code }}</strong><br>
81
+ X: {{ point.px }}<br>
82
+ Y: {{ point.py }}
83
+ </div>
84
+ <span
85
+ class="absolute top-4 left-1/2 transform -translate-x-1/2 text-xs font-bold text-gray-700 pointer-events-none">
86
+ {{ point.code }}
87
+ </span>
88
+ </div>
89
+ {% endfor %}
90
+ </div>
91
+ </div>
92
+
93
+ <section role="alert" class="alert alert-info">
94
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
95
+ class="h-6 w-6 shrink-0 stroke-current">
96
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
97
+ d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 0 0118 0z"></path>
98
+ </svg>
99
+ <span class="text-lg">
100
+ Para guardar los datos sin finalizar su sesión use el botón
101
+ <b>"Guardar progreso"</b>
102
+ </span>
103
+ </section>
104
+
105
+ <section role="alert" class="alert alert-warning">
106
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none"
107
+ viewBox="0 0 24 24">
108
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
109
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
110
+ </svg>
111
+ <div class="flex-col">
112
+ <span class="text-lg block">
113
+ Si usas el botón <b>“Salir de la sesión”</b> asegúrese de guardar el progreso de lo contrario
114
+ perderás todo lo que no hayas guardado antes
115
+ </span>
116
+ <span class="text-lg block">
117
+ Con el botón <b>“Finalizar”</b>, se guardan las posiciones, sales de la sesión y no
118
+ podrás ingresar otra vez
119
+ </span>
120
+ </div>
121
+ </section>
122
+
123
+ <section class="flex justify-end gap-4 max-sm:flex-col">
124
+ <button id="save-progress" class="cts-btn-general cts-btn-primary btn-push">
125
+ Guardar progreso
126
+ </button>
127
+ {% if mode == "perfil ultra flash" %}
128
+ <button id="continue-description" class="cts-btn-general cts-btn-primary btn-push hidden">
129
+ Continuar a descripción
130
+ </button>
131
+ {% endif %}
132
+ <div class="flex gap-2">
133
+ <button id="question-save" class="cts-btn-general cts-btn-secondary btn-push flex-1">
134
+ ¿Finalizar sesión?
135
+ </button>
136
+ <button id="finish-session" class="cts-btn-general cts-btn-tertiary btn-push hidden flex-1"
137
+ onclick="finishSession('form-actions')">
138
+ Finalizar
139
+ </button>
140
+ <button id="cancel-save" class="cts-btn-general cts-btn-error btn-push hidden flex-1">
141
+ Cancelar
142
+ </button>
143
+ </div>
144
+ <span id="loading-data-save" class="loading loading-spinner loading-xl text-accent hidden"></span>
145
+ </section>
146
+ </article>
147
+
148
+ {% if mode == "perfil ultra flash" %}
149
+ {% include "../components/dialog-nap-puf.html" %}
150
+ {% endif %}
151
+
152
+ {% include "../components/toast-container.html" %}
153
+ </article>
154
+ </article>
155
+ {% endblock %}
156
+
157
+ {% block extra_js %}
158
+ <script src="{% static 'js/actions-form.js' %}"></script>
159
+ <script src="{% static 'js/test-napping-plane.js' %}"></script>
160
+ {% if mode == "perfil ultra flash" %}
161
+ <script src="{% static 'js/test-napping-ultra-flash.js' %}"></script>
162
+ {% endif %}
163
+ {% endblock %}