Skip to content

Commit e3b1f67

Browse files
Merge branch 'ui-improvement-sahitya' of https://github.com/ruxailab/RUXAILAB into ui-improvement-sahitya
2 parents c30ad9f + d5cce8c commit e3b1f67

15 files changed

Lines changed: 996 additions & 412 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<template>
2+
<v-card class="pa-6 elevation-3 rounded-xl chart-card">
3+
<div class="mb-4">
4+
<h4 class="text-h6 font-weight-bold mb-2">{{ questionTitle }}</h4>
5+
</div>
6+
<div class="text-body-1 text-medium-emphasis">
7+
<template v-if="Array.isArray(answer) && answer.length && typeof answer[0] === 'string' && answer.length > 1">
8+
<ul class="pl-4">
9+
<li v-for="(comment, cidx) in answer" :key="cidx">{{ comment }}</li>
10+
</ul>
11+
</template>
12+
<template v-else-if="answer">
13+
<span>Respuesta: {{ Array.isArray(answer) ? answer.join(', ') : answer }}</span>
14+
</template>
15+
<template v-else>
16+
<span>No hay respuestas o tipo no soportado.</span>
17+
</template>
18+
</div>
19+
</v-card>
20+
</template>
21+
22+
<script setup>
23+
const props = defineProps({
24+
questionTitle: String,
25+
answer: [String, Array]
26+
});
27+
</script>
28+
29+
<style scoped>
30+
.chart-card {
31+
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
32+
border: 1px solid rgba(59, 130, 246, 0.1);
33+
}
34+
</style>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<template>
2+
<v-card class="pa-6 elevation-3 rounded-xl chart-card">
3+
<div class="mb-4">
4+
<h4 class="font-weight-bold mb-2">{{ questionTitle }}</h4>
5+
</div>
6+
<div class="chart-container-small mb-4">
7+
<canvas :id="canvasId" width="180" height="180"></canvas>
8+
</div>
9+
<div>
10+
<div v-for="(option, idx) in options" :key="option" class="d-flex align-center mb-1">
11+
<div
12+
:style="{ background: chartColors[idx % chartColors.length], width: '14px', height: '14px', borderRadius: '50%', marginRight: '8px' }">
13+
</div>
14+
<span>{{ option }} ({{ counts[option] || 0 }})</span>
15+
</div>
16+
</div>
17+
</v-card>
18+
</template>
19+
20+
<script setup>
21+
import { onMounted, watch, nextTick, ref } from 'vue';
22+
23+
const props = defineProps({
24+
questionTitle: String,
25+
options: Array,
26+
counts: Object,
27+
canvasId: String,
28+
chartColors: {
29+
type: Array,
30+
default: () => ['#42A5F5', '#66BB6A', '#FFA726', '#AB47BC', '#EC407A', '#FF7043', '#26A69A', '#D4E157']
31+
}
32+
});
33+
34+
const drawChart = () => {
35+
nextTick(() => {
36+
const canvas = document.getElementById(props.canvasId);
37+
if (!canvas) return;
38+
const ctx = canvas.getContext('2d');
39+
ctx.clearRect(0, 0, canvas.width, canvas.height);
40+
const total = Object.values(props.counts).reduce((a, b) => a + b, 0);
41+
if (!total) return;
42+
let start = -0.5 * Math.PI;
43+
props.options.forEach((opt, idx) => {
44+
const count = props.counts[opt] || 0;
45+
const angle = (count / total) * 2 * Math.PI;
46+
ctx.beginPath();
47+
ctx.moveTo(90, 90);
48+
ctx.arc(90, 90, 80, start, start + angle);
49+
ctx.closePath();
50+
ctx.fillStyle = props.chartColors[idx % props.chartColors.length];
51+
ctx.fill();
52+
start += angle;
53+
});
54+
// Círculo central blanco (donut)
55+
ctx.beginPath();
56+
ctx.arc(90, 90, 45, 0, 2 * Math.PI);
57+
ctx.fillStyle = '#fff';
58+
ctx.fill();
59+
});
60+
};
61+
62+
watch(() => [props.options, props.counts], drawChart, { immediate: true, deep: true });
63+
onMounted(drawChart);
64+
</script>
65+
66+
<style scoped>
67+
.chart-card {
68+
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
69+
border: 1px solid rgba(59, 130, 246, 0.1);
70+
}
71+
72+
.chart-container-small {
73+
height: 150px;
74+
width: 100%;
75+
position: relative;
76+
display: flex;
77+
align-items: center;
78+
justify-content: center;
79+
}
80+
</style>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<template>
2+
<v-card :class="['pa-6', 'elevation-3', 'rounded-xl', 'h-100', 'ux-metric-card']">
3+
<div class="d-flex align-center mb-4">
4+
<v-avatar :color="color" size="56" class="me-4">
5+
<v-icon color="white" size="28">
6+
{{ icon }}
7+
</v-icon>
8+
</v-avatar>
9+
<div>
10+
<div :class="['text-h4', 'font-weight-bold', `text-${color}`]">
11+
<slot name="value">
12+
{{ value }}
13+
</slot>
14+
</div>
15+
<p class="text-body-1 font-weight-medium mb-0">
16+
<slot name="label">
17+
{{ label }}
18+
</slot>
19+
</p>
20+
</div>
21+
</div>
22+
<p class="text-body-2 text-medium-emphasis">
23+
<slot name="description">
24+
{{ description }}
25+
</slot>
26+
</p>
27+
<v-progress-linear :model-value="progress" :color="color" height="8" rounded class="mt-3" />
28+
</v-card>
29+
</template>
30+
31+
<script setup>
32+
import { defineProps } from 'vue';
33+
const props = defineProps({
34+
value: { type: [String, Number], required: true },
35+
label: { type: String, required: true },
36+
color: { type: String, required: true },
37+
icon: { type: String, required: true },
38+
description: { type: String, default: '' },
39+
progress: { type: Number, default: 0 },
40+
});
41+
</script>
42+
43+
<style scoped>
44+
.ux-metric-card {
45+
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
46+
border: 1px solid rgba(0, 0, 0, 0.05);
47+
}
48+
49+
.ux-metric-card:hover {
50+
transform: translateY(-2px);
51+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1) !important;
52+
}
53+
</style>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<template>
2+
<v-card class="pa-6 elevation-3 rounded-xl chart-card">
3+
<div class="mb-4">
4+
<h4 class="text-h6 font-weight-bold mb-2">{{ question.question }}</h4>
5+
</div>
6+
<div class="chart-container-small mb-4">
7+
<canvas :id="chartId" width="180" height="180"></canvas>
8+
</div>
9+
<div>
10+
<div v-for="(option, idx) in question.selectionFields" :key="option" class="d-flex align-center mb-1">
11+
<div
12+
:style="{ background: chartColors[idx % chartColors.length], width: '14px', height: '14px', borderRadius: '50%', marginRight: '8px' }">
13+
</div>
14+
<span>{{ option }} ({{ selectionCounts[option] || 0 }})</span>
15+
</div>
16+
</div>
17+
</v-card>
18+
</template>
19+
20+
<script setup>
21+
import { ref, watch, nextTick, onMounted } from 'vue';
22+
23+
const props = defineProps({
24+
question: { type: Object, required: true },
25+
answers: { type: Array, required: true },
26+
chartId: { type: String, required: true },
27+
chartColors: { type: Array, default: () => ['#42A5F5', '#66BB6A', '#FFA726', '#AB47BC', '#EC407A', '#FF7043', '#26A69A', '#D4E157'] }
28+
});
29+
30+
const selectionCounts = ref({});
31+
32+
const computeCounts = () => {
33+
const counts = {};
34+
if (!props.question) return counts;
35+
props.question.selectionFields.forEach(opt => { counts[opt] = 0; });
36+
props.answers.forEach(ans => {
37+
if (ans && ans.answer !== undefined) {
38+
const answer = ans.answer;
39+
if (Array.isArray(answer)) {
40+
answer.forEach(a => { if (counts[a] !== undefined) counts[a]++; });
41+
} else if (counts[answer] !== undefined) {
42+
counts[answer]++;
43+
}
44+
}
45+
});
46+
selectionCounts.value = counts;
47+
};
48+
49+
const drawChart = () => {
50+
nextTick(() => {
51+
const q = props.question;
52+
if (!q) return;
53+
const canvas = document.getElementById(props.chartId);
54+
if (!canvas) return;
55+
const ctx = canvas.getContext('2d');
56+
ctx.clearRect(0, 0, canvas.width, canvas.height);
57+
const total = Object.values(selectionCounts.value).reduce((a, b) => a + b, 0);
58+
if (!total) return;
59+
let start = -0.5 * Math.PI;
60+
q.selectionFields.forEach((opt, idx) => {
61+
const count = selectionCounts.value[opt] || 0;
62+
const angle = (count / total) * 2 * Math.PI;
63+
ctx.beginPath();
64+
ctx.moveTo(90, 90);
65+
ctx.arc(90, 90, 80, start, start + angle);
66+
ctx.closePath();
67+
ctx.fillStyle = props.chartColors[idx % props.chartColors.length];
68+
ctx.fill();
69+
start += angle;
70+
});
71+
// Círculo central blanco (donut)
72+
ctx.beginPath();
73+
ctx.arc(90, 90, 45, 0, 2 * Math.PI);
74+
ctx.fillStyle = '#fff';
75+
ctx.fill();
76+
});
77+
};
78+
79+
watch(() => [props.question, props.answers], () => {
80+
computeCounts();
81+
drawChart();
82+
}, { immediate: true, deep: true });
83+
onMounted(() => {
84+
computeCounts();
85+
drawChart();
86+
});
87+
</script>
88+
89+
<style scoped>
90+
.chart-container-small {
91+
height: 150px;
92+
width: 100%;
93+
position: relative;
94+
display: flex;
95+
align-items: center;
96+
justify-content: center;
97+
}
98+
</style>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<template>
2+
<v-card class="pa-6 elevation-3 rounded-xl chart-card">
3+
<div class="mb-4">
4+
<h4 class="text-h6 font-weight-bold mb-2">{{ questionTitle }}</h4>
5+
</div>
6+
<div class="chart-container-small mb-4">
7+
<canvas :id="canvasId" width="180" height="180"></canvas>
8+
</div>
9+
<div>
10+
<div v-for="(option, idx) in options" :key="option" class="d-flex align-center mb-1">
11+
<div
12+
:style="{ background: chartColors[idx % chartColors.length], width: '14px', height: '14px', borderRadius: '50%', marginRight: '8px' }">
13+
</div>
14+
<span>{{ option }} ({{ counts[option] || 0 }})</span>
15+
</div>
16+
</div>
17+
</v-card>
18+
</template>
19+
20+
<script setup>
21+
import { onMounted, watch, nextTick, ref } from 'vue';
22+
23+
const props = defineProps({
24+
questionTitle: String,
25+
options: Array,
26+
counts: Object,
27+
canvasId: String,
28+
chartColors: {
29+
type: Array,
30+
default: () => ['#42A5F5', '#66BB6A', '#FFA726', '#AB47BC', '#EC407A', '#FF7043', '#26A69A', '#D4E157']
31+
}
32+
});
33+
34+
const drawChart = () => {
35+
nextTick(() => {
36+
const canvas = document.getElementById(props.canvasId);
37+
if (!canvas) return;
38+
const ctx = canvas.getContext('2d');
39+
ctx.clearRect(0, 0, canvas.width, canvas.height);
40+
const total = Object.values(props.counts).reduce((a, b) => a + b, 0);
41+
if (!total) return;
42+
let start = -0.5 * Math.PI;
43+
props.options.forEach((opt, idx) => {
44+
const count = props.counts[opt] || 0;
45+
const angle = (count / total) * 2 * Math.PI;
46+
ctx.beginPath();
47+
ctx.moveTo(90, 90);
48+
ctx.arc(90, 90, 80, start, start + angle);
49+
ctx.closePath();
50+
ctx.fillStyle = props.chartColors[idx % props.chartColors.length];
51+
ctx.fill();
52+
start += angle;
53+
});
54+
// Círculo central blanco (donut)
55+
ctx.beginPath();
56+
ctx.arc(90, 90, 45, 0, 2 * Math.PI);
57+
ctx.fillStyle = '#fff';
58+
ctx.fill();
59+
});
60+
};
61+
62+
watch(() => [props.options, props.counts], drawChart, { immediate: true, deep: true });
63+
onMounted(drawChart);
64+
</script>
65+
66+
<style scoped>
67+
.chart-card {
68+
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
69+
border: 1px solid rgba(59, 130, 246, 0.1);
70+
}
71+
72+
.chart-container-small {
73+
height: 150px;
74+
width: 100%;
75+
position: relative;
76+
display: flex;
77+
align-items: center;
78+
justify-content: center;
79+
}
80+
</style>

src/components/dashboard/OfficeHours.vue

Whitespace-only changes.

src/components/dashboard/TestTypes.vue

Whitespace-only changes.

src/components/molecules/Toolbar.vue

Whitespace-only changes.

0 commit comments

Comments
 (0)