| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- <template>
- <q-dialog ref="dialogRef" @hide="onDialogHide">
- <div style="width: 100%; max-width: 560px">
- <q-card style="display: flex; flex-direction: column; max-height: 88vh">
- <DefaultDialogHeader
- :title="`Registrar Presença - ${classData.title}`"
- @close="onDialogCancel"
- />
- <q-card-section class="q-pt-none" style="flex: 1; overflow-y: auto">
- <!-- Cabeçalho da aula -->
- <div class="attendance-head">
- <div class="attendance-head__badge">{{ startTime }}</div>
- <div class="column">
- <span class="text-subtitle1 text-primary text-weight-medium">
- {{ classData.title }}
- </span>
- <div class="row items-center q-gutter-x-md text-caption text-foreground">
- <span class="row items-center" style="gap: 4px">
- <q-icon name="mdi-map-marker-outline" color="secondary" size="15px" />
- {{ classData.room || "—" }}
- </span>
- <span class="row items-center" style="gap: 4px">
- <q-icon name="mdi-account-outline" color="secondary" size="15px" />
- {{ classData.instructor || "—" }}
- </span>
- <span class="row items-center" style="gap: 4px">
- <q-icon name="mdi-clock-outline" color="secondary" size="15px" />
- {{ timeRange }}
- </span>
- </div>
- </div>
- </div>
- <!-- Lista de alunos -->
- <div class="q-mt-md row items-center" style="gap: 6px">
- <q-icon name="mdi-account-group-outline" color="primary" size="18px" />
- <span class="text-weight-medium text-primary">Alunos ({{ students.length }})</span>
- </div>
- <q-separator class="q-mt-sm" />
- <div v-if="loading" class="flex flex-center q-pa-lg">
- <q-spinner color="primary" size="32px" />
- </div>
- <div v-else-if="!students.length" class="text-center text-grey-6 q-pa-lg">
- Nenhum aluno com contrato ativo neste pacote.
- </div>
- <q-markup-table v-else flat dense class="attendance-table">
- <thead>
- <tr>
- <th class="text-left">Aluno</th>
- <th class="text-center">Aulas</th>
- <th class="text-center">Presença</th>
- <th class="text-center">Justificar</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="row in students" :key="row.student_id">
- <td class="text-left">{{ row.name }}</td>
- <td class="text-center">{{ row.classes_count }}</td>
- <!-- Presença: verde = presente, vermelho = falta -->
- <td class="text-center">
- <div class="row no-wrap items-center justify-center" style="gap: 6px">
- <q-btn
- :disable="!canEdit"
- round
- unelevated
- size="sm"
- icon="mdi-check"
- :color="row.in_class ? 'positive' : 'grey-4'"
- :text-color="row.in_class ? 'white' : 'grey-7'"
- @click="markPresent(row)"
- >
- <q-tooltip>Presente</q-tooltip>
- </q-btn>
- <q-btn
- :disable="!canEdit"
- round
- unelevated
- size="sm"
- icon="mdi-close"
- :color="!row.in_class ? 'negative' : 'grey-4'"
- :text-color="!row.in_class ? 'white' : 'grey-7'"
- @click="markAbsent(row)"
- >
- <q-tooltip>Falta</q-tooltip>
- </q-btn>
- </div>
- </td>
- <!-- Justificar: só habilita para faltas -->
- <td class="text-center">
- <q-btn
- :disable="!canEdit || row.in_class"
- no-caps
- unelevated
- size="sm"
- :color="row.justified ? 'primary' : 'secondary'"
- text-color="white"
- :label="row.justified ? 'Justificada' : 'Justificar'"
- @click="openJustify(row)"
- >
- <q-tooltip v-if="row.in_class">
- Só é possível justificar faltas
- </q-tooltip>
- </q-btn>
- </td>
- </tr>
- </tbody>
- </q-markup-table>
- <!-- Resumo -->
- <div v-if="students.length" class="attendance-summary q-mt-md">
- <div class="text-weight-medium text-primary q-mb-xs">Resumo</div>
- <div class="row justify-between attendance-summary__line">
- <span>Total de Alunos</span><span>{{ students.length }}</span>
- </div>
- <div class="row justify-between attendance-summary__line">
- <span>Presente</span><span>{{ presentCount }}</span>
- </div>
- </div>
- </q-card-section>
- <q-card-actions align="right" class="q-px-md q-pb-md">
- <q-btn outline color="primary" label="Cancelar" no-caps @click="onDialogCancel" />
- <q-btn
- v-if="canEdit"
- color="primary"
- label="Salvar"
- no-caps
- :loading="saving"
- :disable="!students.length"
- @click="onSave"
- />
- </q-card-actions>
- </q-card>
- </div>
- </q-dialog>
- </template>
- <script setup>
- import { ref, computed, onMounted } from "vue";
- import { useDialogPluginComponent, useQuasar } from "quasar";
- import { format, parseISO } from "date-fns";
- import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
- import JustifyAttendanceDialog from "./JustifyAttendanceDialog.vue";
- import { getClassStudents, saveClassAttendance } from "src/api/class";
- import { permissionStore } from "src/stores/permission";
- defineEmits([...useDialogPluginComponent.emits]);
- const props = defineProps({
- classData: { type: Object, required: true },
- });
- const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
- useDialogPluginComponent();
- const $q = useQuasar();
- const permissions = permissionStore();
- const canEdit = computed(() =>
- permissions.getAccess("franchisee_classes", "edit"),
- );
- const students = ref([]);
- const loading = ref(false);
- const saving = ref(false);
- const startTime = computed(() =>
- props.classData.date_time_start
- ? format(parseISO(props.classData.date_time_start), "HH:mm")
- : "--:--",
- );
- const timeRange = computed(() => {
- const { date_time_start, date_time_end } = props.classData;
- if (!date_time_start || !date_time_end) return "";
- return `${format(parseISO(date_time_start), "HH:mm")} - ${format(parseISO(date_time_end), "HH:mm")}`;
- });
- const presentCount = computed(
- () => students.value.filter((s) => s.in_class).length,
- );
- const loadStudents = async () => {
- loading.value = true;
- try {
- students.value = await getClassStudents(props.classData.id);
- } finally {
- loading.value = false;
- }
- };
- // Clicar no verde marca presente automaticamente (e remove qualquer justificativa)
- const markPresent = (row) => {
- row.in_class = true;
- row.justified = false;
- row.notes = null;
- };
- const markAbsent = (row) => {
- row.in_class = false;
- };
- const openJustify = (row) => {
- // Só faz sentido justificar faltas
- if (row.in_class) return;
- $q.dialog({
- component: JustifyAttendanceDialog,
- componentProps: { notes: row.notes ?? "" },
- }).onOk((notes) => {
- row.in_class = false;
- row.justified = true;
- row.notes = notes;
- });
- };
- const onSave = async () => {
- saving.value = true;
- try {
- const attendances = students.value.map((s) => ({
- student_id: s.student_id,
- in_class: s.in_class,
- justified: s.justified,
- notes: s.notes ?? null,
- }));
- await saveClassAttendance(props.classData.id, attendances);
- $q.notify({ type: "positive", message: "Presença registrada." });
- onDialogOK(true);
- } catch {
- // erros já notificados pelo interceptor do axios
- } finally {
- saving.value = false;
- }
- };
- onMounted(loadStudents);
- </script>
- <style scoped>
- .attendance-head {
- display: flex;
- align-items: center;
- gap: 12px;
- background: #eaf8f8;
- border-radius: 10px;
- padding: 12px;
- margin-top: 8px;
- }
- .attendance-head__badge {
- background: #ace4e4;
- color: #003e29;
- font-weight: 600;
- border-radius: 8px;
- padding: 10px 12px;
- font-size: 14px;
- }
- .attendance-table :deep(th) {
- font-size: 12px;
- color: #909090;
- font-weight: 500;
- }
- .attendance-table :deep(td) {
- font-size: 13px;
- }
- .attendance-summary {
- background: #eaf8f8;
- border-radius: 8px;
- padding: 10px 12px;
- font-size: 13px;
- color: #003e29;
- }
- .attendance-summary__line {
- padding: 2px 0;
- }
- </style>
|