Ecrão das ausências
This commit is contained in:
+1
-1
@@ -47,7 +47,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
const path = event.url.pathname;
|
||||
|
||||
// Route protection
|
||||
if (path.startsWith('/admin')) {
|
||||
if (path.startsWith('/admin') || path.startsWith('/super')) {
|
||||
if (!event.locals.user) {
|
||||
throw redirect(303, `/login?redirectTo=${encodeURIComponent(path)}`);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
export const load: LayoutServerLoad = async ({ locals, url }) => {
|
||||
const hostname = url.hostname;
|
||||
const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
|
||||
|
||||
return {
|
||||
user: locals.user
|
||||
user: locals.user,
|
||||
isLocalhost
|
||||
};
|
||||
};
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
<li>
|
||||
<a class="dropdown-item py-2 px-3 rounded-2" href="/admin/beneficiarios">Beneficiários</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item py-2 px-3 rounded-2" href="/admin/ausencias">Ausências</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item py-2 px-3 rounded-2" href="/admin/turnos">Turnos</a>
|
||||
</li>
|
||||
@@ -54,6 +57,27 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 rounded-2" href="/entregas">Entregas</a>
|
||||
</li>
|
||||
{#if data.user.role === 'admin' && data.isLocalhost}
|
||||
<li class="nav-item dropdown">
|
||||
<button type="button" class="nav-link dropdown-toggle px-3 rounded-2 bg-transparent border-0 text-warning fw-bold" id="superDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Super
|
||||
</button>
|
||||
<ul class="dropdown-menu border-0 shadow mt-2" aria-labelledby="superDropdown">
|
||||
<li>
|
||||
<span class="dropdown-item-text text-muted small py-2 px-3 fw-semibold">Ferramentas Super</span>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item py-2 px-3 rounded-2" href="/super/dummy-data">Dummy Data</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<span class="dropdown-item py-2 px-3 text-secondary small">Acesso Localhost Ativo</span>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
|
||||
@@ -37,6 +37,17 @@
|
||||
<span class="card-label fw-bold text-dark fs-4">Beneficiários</span>
|
||||
</a>
|
||||
|
||||
<!-- Ausências Card (Admin only) -->
|
||||
<a href="/admin/ausencias" class="dashboard-card d-flex flex-column align-items-center justify-content-center text-decoration-none shadow p-4 rounded-4 transition">
|
||||
<div class="icon-wrapper d-flex align-items-center justify-content-center mb-3 text-white rounded-circle shadow-sm">
|
||||
<!-- Calendar X / Absences Icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-calendar-x-fill" viewBox="0 0 16 16">
|
||||
<path d="M4 .5a.5.5 0 0 0-1 0V1H2a2 2 0 0 0-2 2v1h16V3a2 2 0 0 0-2-2h-1V.5a.5.5 0 0 0-1 0V1H4zm12 5H0v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2zM6.854 8.146 8 9.293l1.146-1.147a.5.5 0 1 1 .708.708L8.707 10l1.147 1.146a.5.5 0 0 1-.708.708L8 10.707l-1.146 1.147a.5.5 0 0 1-.708-.708L7.293 10 6.146 8.854a.5.5 0 1 1 .708-.708"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="card-label fw-bold text-dark fs-4">Ausências</span>
|
||||
</a>
|
||||
|
||||
<!-- Utilizadores Card (Admin only) -->
|
||||
<a href="/admin/utilizadores" class="dashboard-card d-flex flex-column align-items-center justify-content-center text-decoration-none shadow p-4 rounded-4 transition">
|
||||
<div class="icon-wrapper d-flex align-items-center justify-content-center mb-3 text-white rounded-circle shadow-sm">
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import * as schema from '$lib/server/db/schema';
|
||||
import { eq, and, gte, lte, sql } from 'drizzle-orm';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
const search = url.searchParams.get('search') || '';
|
||||
const minAbsencesStr = url.searchParams.get('minAbsences') || '1';
|
||||
const minAbsences = parseInt(minAbsencesStr, 10) || 1;
|
||||
|
||||
// Define timezone-adjusted default date boundaries
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||
|
||||
const defaultStartDate = thirtyDaysAgo.toLocaleDateString('sv-SE', { timeZone: 'Europe/Lisbon' });
|
||||
const defaultEndDate = yesterday.toLocaleDateString('sv-SE', { timeZone: 'Europe/Lisbon' });
|
||||
|
||||
const startDate = url.searchParams.get('startDate') || defaultStartDate;
|
||||
const endDate = url.searchParams.get('endDate') || defaultEndDate;
|
||||
|
||||
try {
|
||||
// 1. Get all active parent beneficiaries
|
||||
const activeBeneficiaries = db
|
||||
.select()
|
||||
.from(schema.beneficiaries)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.beneficiaries.status, 'ativo'),
|
||||
eq(schema.beneficiaries.isParent, true)
|
||||
)
|
||||
)
|
||||
.all();
|
||||
|
||||
// 3. Find all unique delivery dates where at least one delivery occurred in the selected range
|
||||
const deliveryDatesResult = db
|
||||
.select({
|
||||
date: schema.deliveries.date
|
||||
})
|
||||
.from(schema.deliveries)
|
||||
.where(
|
||||
and(
|
||||
gte(schema.deliveries.date, startDate),
|
||||
lte(schema.deliveries.date, endDate)
|
||||
)
|
||||
)
|
||||
.all();
|
||||
|
||||
const deliveryDates = Array.from(new Set(deliveryDatesResult.map((r) => r.date))).sort();
|
||||
|
||||
// 4. Fetch all deliveries in the selected range
|
||||
const recentDeliveries = db
|
||||
.select()
|
||||
.from(schema.deliveries)
|
||||
.where(
|
||||
and(
|
||||
gte(schema.deliveries.date, startDate),
|
||||
lte(schema.deliveries.date, endDate)
|
||||
)
|
||||
)
|
||||
.all();
|
||||
|
||||
// 5. Fetch the last delivery date of all time for each beneficiary
|
||||
const lastDeliveries = db
|
||||
.select({
|
||||
beneficiaryId: schema.deliveries.beneficiaryId,
|
||||
lastDate: sql<string>`max(${schema.deliveries.date})`
|
||||
})
|
||||
.from(schema.deliveries)
|
||||
.groupBy(schema.deliveries.beneficiaryId)
|
||||
.all();
|
||||
|
||||
const lastDeliveryMap = new Map(lastDeliveries.map((d) => [d.beneficiaryId, d.lastDate]));
|
||||
|
||||
// Helper to convert timestamp to date string
|
||||
const timestampToDateStr = (timestamp: number): string => {
|
||||
return new Date(timestamp).toLocaleDateString('sv-SE', { timeZone: 'Europe/Lisbon' });
|
||||
};
|
||||
|
||||
// 6. Map and process absences per beneficiary
|
||||
let list = activeBeneficiaries.map((b) => {
|
||||
const registeredDateStr = timestampToDateStr(b.createdAt);
|
||||
|
||||
// Only delivery dates on/after registration date are relevant
|
||||
const relevantDeliveryDates = deliveryDates.filter((date) => date >= registeredDateStr);
|
||||
|
||||
// Actual deliveries received by this beneficiary
|
||||
const receivedDates = recentDeliveries
|
||||
.filter((d) => d.beneficiaryId === b.id)
|
||||
.map((d) => d.date);
|
||||
|
||||
// Missed delivery dates
|
||||
const missedDates = relevantDeliveryDates.filter((date) => !receivedDates.includes(date));
|
||||
|
||||
return {
|
||||
...b,
|
||||
totalRelevantDeliveries: relevantDeliveryDates.length,
|
||||
receivedCount: receivedDates.length,
|
||||
absencesCount: missedDates.length,
|
||||
missedDates: missedDates.reverse(), // Show latest missed dates first
|
||||
lastDelivery: lastDeliveryMap.get(b.id) || null
|
||||
};
|
||||
});
|
||||
|
||||
// 7. Filter by minimum absences (default >= 1)
|
||||
list = list.filter((b) => b.absencesCount >= minAbsences);
|
||||
|
||||
// 8. Apply search filters (name/number/contact)
|
||||
if (search) {
|
||||
const searchLower = search.toLowerCase();
|
||||
list = list.filter(
|
||||
(b) =>
|
||||
b.name.toLowerCase().includes(searchLower) ||
|
||||
(b.number !== null && b.number.toString().includes(searchLower)) ||
|
||||
(b.contact && b.contact.includes(searchLower))
|
||||
);
|
||||
}
|
||||
|
||||
// Sort by absences count descending, then by number ascending
|
||||
list.sort((a, b) => b.absencesCount - a.absencesCount || (a.number || 0) - (b.number || 0));
|
||||
|
||||
return {
|
||||
beneficiaries: list,
|
||||
totalDeliveryDays: deliveryDates.length,
|
||||
search,
|
||||
minAbsences,
|
||||
startDate,
|
||||
endDate
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error loading absences data:', err);
|
||||
return {
|
||||
beneficiaries: [],
|
||||
totalDeliveryDays: 0,
|
||||
search,
|
||||
minAbsences,
|
||||
startDate,
|
||||
endDate,
|
||||
error: 'Erro ao processar o histórico de ausências dos beneficiários.'
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,237 @@
|
||||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
const [year, month, day] = dateStr.split('-');
|
||||
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
||||
const formatted = date.toLocaleDateString('pt-PT', { weekday: 'short', day: 'numeric', month: 'short' });
|
||||
// Capitalize weekday
|
||||
return formatted.charAt(0).toUpperCase() + formatted.slice(1);
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateAssiduity(received: number, total: number): number {
|
||||
if (total === 0) return 100;
|
||||
return Math.round((received / total) * 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Ausências de Beneficiários - RefoodOne</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="mb-4">
|
||||
<h2 class="fw-bold text-dark mb-1">Ausências de Beneficiários</h2>
|
||||
<p class="text-muted mb-0">Controlo de faltas e assiduidade no período selecionado (entregas 2 vezes por semana)</p>
|
||||
</div>
|
||||
|
||||
{#if data.error}
|
||||
<div class="alert alert-danger border-0 shadow-sm rounded-3 mb-4" role="alert">
|
||||
{data.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Summary Statistics Cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm rounded-4 p-3 bg-white h-100">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="stat-icon bg-success-subtle text-success rounded-3 p-3 d-flex align-items-center justify-content-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-calendar-check" viewBox="0 0 16 16">
|
||||
<path d="M10.854 7.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 9.793l2.646-2.647a.5.5 0 0 1 .708 0"/>
|
||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5M1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-secondary fw-semibold small mb-1">Dias de Entrega Ocorridos</h6>
|
||||
<h3 class="fw-bold text-dark mb-0">{data.totalDeliveryDays} dias</h3>
|
||||
<span class="text-muted small">no período selecionado</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm rounded-4 p-3 bg-white h-100">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="stat-icon bg-danger-subtle text-danger rounded-3 p-3 d-flex align-items-center justify-content-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-person-exclamation" viewBox="0 0 16 16">
|
||||
<path d="M11 5a3 3 0 1 1-6 0 3 3 0 0 1 6 0M8 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4m.002 6a4.99 4.99 0 0 1 5.021-1.268 4 4 0 0 0-3.094-1.148H6a4 4 0 0 0-4 4v.5c0 .502.405.82 1 .82h5.216a5 5 0 0 1-.214-1.025L3.008 13zm9.998-1.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0m-.5-3.875a.41.41 0 0 0-.406.416v1.854a.41.41 0 0 0 .812 0V8.041a.41.41 0 0 0-.406-.416"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-secondary fw-semibold small mb-1">Beneficiários com Faltas</h6>
|
||||
<h3 class="fw-bold text-dark mb-0">{data.beneficiaries.length}</h3>
|
||||
<span class="text-muted small">ativos e com faltas registadas</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm rounded-4 p-3 bg-white h-100">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="stat-icon bg-warning-subtle text-warning-emphasis rounded-3 p-3 d-flex align-items-center justify-content-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-percent" viewBox="0 0 16 16">
|
||||
<path d="M13.485 1.431a.5.5 0 0 1 .37.662l-9.2 13.5a.5.5 0 1 1-.84-.572l9.2-13.5a.5.5 0 0 1 .67-.19zM4.15 2.5a2 2 0 1 1-2.757 2.915A2 2 0 0 1 4.15 2.5zm0 1a1 1 0 1 0-1.378 1.458A1 1 0 0 0 4.15 3.5zm7.7 7.5a2 2 0 1 1-2.757 2.915A2 2 0 0 1 11.85 11zm0 1a1 1 0 1 0-1.378 1.458A1 1 0 0 0 11.85 12.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-secondary fw-semibold small mb-1">Filtro de Relevância</h6>
|
||||
<h3 class="fw-bold text-dark mb-0">≥ {data.minAbsences} {data.minAbsences === 1 ? 'falta' : 'faltas'}</h3>
|
||||
<span class="text-muted small">limiar selecionado para aviso</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters & Search -->
|
||||
<div class="card border-0 shadow-sm rounded-4 p-3 mb-4 bg-white">
|
||||
<form method="GET" class="row g-3 align-items-end">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<label for="search" class="form-label fw-semibold text-secondary small">Pesquisar</label>
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
id="search"
|
||||
value={data.search}
|
||||
placeholder="Pesquise por nome, número ou contacto..."
|
||||
class="form-control rounded-3 border-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-2">
|
||||
<label for="minAbsences" class="form-label fw-semibold text-secondary small">Mínimo de Faltas</label>
|
||||
<select name="minAbsences" id="minAbsences" value={data.minAbsences.toString()} class="form-select rounded-3 border-2">
|
||||
<option value="1">1 ou mais faltas</option>
|
||||
<option value="2">2 ou mais faltas</option>
|
||||
<option value="3">3 ou mais faltas</option>
|
||||
<option value="4">4 ou mais faltas</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-lg-2">
|
||||
<label for="startDate" class="form-label fw-semibold text-secondary small">Data de Início</label>
|
||||
<input
|
||||
type="date"
|
||||
name="startDate"
|
||||
id="startDate"
|
||||
value={data.startDate}
|
||||
class="form-control rounded-3 border-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-lg-2">
|
||||
<label for="endDate" class="form-label fw-semibold text-secondary small">Data de Fim</label>
|
||||
<input
|
||||
type="date"
|
||||
name="endDate"
|
||||
id="endDate"
|
||||
value={data.endDate}
|
||||
class="form-control rounded-3 border-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-lg-2 d-grid">
|
||||
<button type="submit" class="btn btn-primary rounded-3 fw-semibold border-0 py-2" style="background-color: #2c562e;">
|
||||
Filtrar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Main Absences Table -->
|
||||
<div class="card border-0 shadow-sm rounded-4 overflow-hidden bg-white">
|
||||
<div class="card-header border-0 bg-light py-3 px-4">
|
||||
<h5 class="fw-bold text-dark mb-0">Relatório de Assiduidade</h5>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-secondary fw-semibold">
|
||||
<tr>
|
||||
<th class="px-4 py-3" style="width: 120px;">Número</th>
|
||||
<th class="py-3" style="width: 220px;">Nome</th>
|
||||
<th class="py-3" style="width: 150px;">Contacto</th>
|
||||
<th class="py-3 text-center" style="width: 120px;">Faltas / Entregas</th>
|
||||
<th class="py-3 text-center" style="width: 120px;">Assiduidade</th>
|
||||
<th class="py-3">Dias com Ausência no Período</th>
|
||||
<th class="py-3" style="width: 140px;">Última Entrega</th>
|
||||
<th class="px-4 py-3 text-end" style="width: 120px;">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if data.beneficiaries.length === 0}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-5 text-muted">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" class="bi bi-check2-circle mb-2 text-success" viewBox="0 0 16 16">
|
||||
<path d="M2.5 8a5.5 5.5 0 0 1 8.25-4.764.5.5 0 0 0 .5-.866A6.5 6.5 0 1 0 14.5 8a.5.5 0 0 0-1 0 5.5 5.5 0 1 1-11 0"/>
|
||||
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0z"/>
|
||||
</svg>
|
||||
<p class="mb-0 fw-semibold">Nenhum beneficiário com ausências registadas.</p>
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each data.beneficiaries as b}
|
||||
{@const assid = calculateAssiduity(b.receivedCount, b.totalRelevantDeliveries)}
|
||||
<tr>
|
||||
<td class="px-4 fw-bold text-secondary">
|
||||
#{b.number}
|
||||
</td>
|
||||
<td class="fw-semibold text-dark">
|
||||
{b.name}
|
||||
</td>
|
||||
<td class="text-secondary small">
|
||||
{b.contact || 'Sem contacto'}
|
||||
</td>
|
||||
<td class="text-center fw-semibold">
|
||||
<span class="text-danger">{b.absencesCount}</span> / {b.totalRelevantDeliveries}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge rounded-pill fw-bold text-uppercase px-2.5 py-1.5 border {assid >= 75 ? 'bg-success-subtle text-success border-success-subtle' : assid >= 50 ? 'bg-warning-subtle text-warning border-warning-subtle' : 'bg-danger-subtle text-danger border-danger-subtle'}" style="font-size: 0.7rem;">
|
||||
{assid}%
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{#each b.missedDates as date}
|
||||
<span class="badge bg-danger-subtle text-danger border border-danger-subtle fw-semibold rounded-2 px-2 py-1" style="font-size: 0.75rem;">
|
||||
{formatDate(date)}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-secondary small fw-medium">
|
||||
{b.lastDelivery ? formatDate(b.lastDelivery) : 'Nunca'}
|
||||
</td>
|
||||
<td class="px-4 text-end">
|
||||
<a href="/admin/beneficiarios/{b.id}" class="btn btn-sm btn-outline-secondary rounded-2 px-2.5 py-1 d-inline-flex align-items-center gap-1.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/>
|
||||
</svg>
|
||||
Ficha
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.badge {
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</style>
|
||||
@@ -4,12 +4,13 @@ import { eq, and, asc, isNull } from 'drizzle-orm';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
export const load: PageServerLoad = async ({ url, locals }) => {
|
||||
if (!locals.user) {
|
||||
throw redirect(303, '/login');
|
||||
}
|
||||
|
||||
const todayStr = new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Lisbon' }); // 'YYYY-MM-DD'
|
||||
const selectedDate = url.searchParams.get('date') || todayStr;
|
||||
|
||||
try {
|
||||
// Fetch active beneficiaries sorted by their unique number, who are main beneficiaries (no parentOdooId)
|
||||
@@ -32,7 +33,7 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
.orderBy(asc(schema.shifts.startTime))
|
||||
.all();
|
||||
|
||||
// Fetch today's deliveries with associated beneficiary and shift details
|
||||
// Fetch deliveries for the selected date with associated beneficiary and shift details
|
||||
const todayDeliveries = db
|
||||
.select({
|
||||
id: schema.deliveries.id,
|
||||
@@ -44,17 +45,19 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
.from(schema.deliveries)
|
||||
.innerJoin(schema.beneficiaries, eq(schema.deliveries.beneficiaryId, schema.beneficiaries.id))
|
||||
.innerJoin(schema.shifts, eq(schema.deliveries.shiftId, schema.shifts.id))
|
||||
.where(eq(schema.deliveries.date, todayStr))
|
||||
.where(eq(schema.deliveries.date, selectedDate))
|
||||
.all();
|
||||
|
||||
// Compile the list of beneficiary IDs who already received a basket today
|
||||
// Compile the list of beneficiary IDs who already received a basket
|
||||
const deliveredIds = todayDeliveries.map((d) => d.beneficiary.id);
|
||||
|
||||
return {
|
||||
beneficiaries: activeBeneficiaries,
|
||||
shifts: shiftsList,
|
||||
todayDeliveries,
|
||||
deliveredIds
|
||||
deliveredIds,
|
||||
selectedDate,
|
||||
todayStr
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error loading deliveries page:', err);
|
||||
@@ -63,6 +66,8 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
shifts: [],
|
||||
todayDeliveries: [],
|
||||
deliveredIds: [],
|
||||
selectedDate,
|
||||
todayStr,
|
||||
error: 'Erro ao carregar os dados de entregas.'
|
||||
};
|
||||
}
|
||||
@@ -184,6 +189,22 @@ export const actions: Actions = {
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the delivery to verify its date
|
||||
const delivery = db
|
||||
.select()
|
||||
.from(schema.deliveries)
|
||||
.where(eq(schema.deliveries.id, deliveryId))
|
||||
.get();
|
||||
|
||||
if (!delivery) {
|
||||
return fail(404, { error: 'Registo de entrega não encontrado.' });
|
||||
}
|
||||
|
||||
const todayStr = new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Lisbon' });
|
||||
if (delivery.date !== todayStr) {
|
||||
return fail(400, { error: 'Não é permitido apagar registos de dias passados.' });
|
||||
}
|
||||
|
||||
// Delete the delivery record
|
||||
db.delete(schema.deliveries)
|
||||
.where(eq(schema.deliveries.id, deliveryId))
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
let activeModalBeneficiary = $state<any>(null);
|
||||
let activeDeleteDelivery = $state<any>(null);
|
||||
let isLoading = $state(false);
|
||||
let dateInput = $state<HTMLInputElement | null>(null);
|
||||
|
||||
// Derived list of delivered IDs so UI updates dynamically
|
||||
const deliveredSet = $derived(new Set(data.deliveredIds || []));
|
||||
const isToday = $derived(data.selectedDate === data.todayStr);
|
||||
|
||||
function openConfirmation(beneficiary: any) {
|
||||
if (deliveredSet.has(beneficiary.id)) return;
|
||||
if (deliveredSet.has(beneficiary.id) || !isToday) return;
|
||||
activeModalBeneficiary = beneficiary;
|
||||
}
|
||||
|
||||
@@ -39,6 +41,17 @@
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateLong(dateStr: string): string {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
const [year, month, day] = dateStr.split('-');
|
||||
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
||||
return date.toLocaleDateString('pt-PT', { timeZone: 'Europe/Lisbon', weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -49,14 +62,32 @@
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-3 mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold text-dark mb-1">Registo de Entregas</h2>
|
||||
<p class="text-muted mb-0">Selecione o número do beneficiário para registar a entrega do cabaz de hoje</p>
|
||||
{#if isToday}
|
||||
<p class="text-muted mb-0">Selecione o número do beneficiário para registar a entrega do cabaz de hoje</p>
|
||||
{:else}
|
||||
<p class="text-muted mb-0">Visualização de registos de entrega para a data selecionada (alterações desabilitadas)</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="bg-dark text-white px-3 py-2 rounded-3 shadow-sm d-flex align-items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar-event" viewBox="0 0 16 16">
|
||||
<path d="M11 6.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5z"/>
|
||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5M1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/>
|
||||
</svg>
|
||||
<span class="fw-semibold">{new Date().toLocaleDateString('pt-PT', { timeZone: 'Europe/Lisbon', weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</span>
|
||||
<div class="position-relative d-inline-block">
|
||||
<!-- original style and layout wrapper -->
|
||||
<div class="bg-dark text-white px-3 py-2 rounded-3 shadow-sm d-flex align-items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar-event" viewBox="0 0 16 16">
|
||||
<path d="M11 6.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5z"/>
|
||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5M1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/>
|
||||
</svg>
|
||||
<span class="fw-semibold text-capitalize">{formatDateLong(data.selectedDate)}</span>
|
||||
</div>
|
||||
<!-- Overlay input overlayed on top -->
|
||||
<form method="GET" class="position-absolute top-0 start-0 w-100 h-100 m-0 opacity-0" style="z-index: 5;">
|
||||
<input
|
||||
type="date"
|
||||
name="date"
|
||||
value={data.selectedDate}
|
||||
onchange={(e) => e.currentTarget.form?.submit()}
|
||||
class="datepicker-overlay w-100 h-100 border-0 cursor-pointer position-absolute top-0 start-0"
|
||||
style="cursor: pointer; z-index: 5;"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,9 +123,9 @@
|
||||
{@const isDelivered = deliveredSet.has(beneficiary.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="btn-grid-beneficiary d-flex flex-column align-items-center justify-content-center rounded-4 border-2 transition shadow-sm {isDelivered ? 'delivered' : 'active'}"
|
||||
onclick={() => openConfirmation(beneficiary)}
|
||||
disabled={isDelivered}
|
||||
class="btn-grid-beneficiary d-flex flex-column align-items-center justify-content-center rounded-4 border-2 transition shadow-sm {isDelivered ? 'delivered' : isToday ? 'active' : 'disabled-past'}"
|
||||
onclick={() => isToday && openConfirmation(beneficiary)}
|
||||
disabled={isDelivered || !isToday}
|
||||
aria-label="Beneficiário número {beneficiary.number} - {beneficiary.name}"
|
||||
>
|
||||
<span class="beneficiary-number fw-bold mb-0">{beneficiary.number ?? '-'}</span>
|
||||
@@ -117,7 +148,7 @@
|
||||
<!-- Today's Recent Deliveries -->
|
||||
<div class="card border-0 shadow-sm rounded-4 overflow-hidden bg-white">
|
||||
<div class="card-header border-0 bg-light py-3 px-4">
|
||||
<h5 class="fw-bold text-dark mb-0">Entregas de Hoje</h5>
|
||||
<h5 class="fw-bold text-dark mb-0">{isToday ? 'Entregas de Hoje' : 'Entregas Registadas'}</h5>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
@@ -127,14 +158,16 @@
|
||||
<th class="py-3">Nome do Beneficiário</th>
|
||||
<th class="py-3">Turno</th>
|
||||
<th class="py-3" style="width: 180px;">Hora do Registo</th>
|
||||
<th class="py-3 px-4 text-end" style="width: 100px;">Ações</th>
|
||||
{#if isToday}
|
||||
<th class="py-3 px-4 text-end" style="width: 100px;">Ações</th>
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if data.todayDeliveries.length === 0}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4 text-muted">
|
||||
Nenhum cabaz entregue hoje.
|
||||
<td colspan={isToday ? 5 : 4} class="text-center py-4 text-muted">
|
||||
Nenhum cabaz entregue nesta data.
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
@@ -154,19 +187,21 @@
|
||||
<td class="text-secondary fw-medium">
|
||||
{formatTime(delivery.createdAt)}
|
||||
</td>
|
||||
<td class="px-4 text-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-link text-danger p-0 border-0 align-middle transition"
|
||||
title="Apagar Registo"
|
||||
onclick={() => openDeleteConfirmation(delivery)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-trash3" viewBox="0 0 16 16">
|
||||
<path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
{#if isToday}
|
||||
<td class="px-4 text-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-link text-danger p-0 border-0 align-middle transition"
|
||||
title="Apagar Registo"
|
||||
onclick={() => openDeleteConfirmation(delivery)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-trash3" viewBox="0 0 16 16">
|
||||
<path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -332,6 +367,13 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-grid-beneficiary.disabled-past {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #dee2e6;
|
||||
color: #adb5bd;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.beneficiary-number {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
@@ -340,4 +382,16 @@
|
||||
.transition {
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.datepicker-overlay::-webkit-calendar-picker-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import * as schema from '$lib/server/db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
if (!locals.user || locals.user.role !== 'admin') {
|
||||
throw redirect(303, '/login');
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
generate: async ({ locals }) => {
|
||||
if (!locals.user || locals.user.role !== 'admin') {
|
||||
return fail(403, { error: 'Não autorizado.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Clear the deliveries table
|
||||
db.delete(schema.deliveries).run();
|
||||
|
||||
// 2. Ensure at least one shift exists
|
||||
let shift = db.select().from(schema.shifts).get();
|
||||
if (!shift) {
|
||||
const nowTs = Date.now();
|
||||
db.insert(schema.shifts)
|
||||
.values({
|
||||
code: 'T1',
|
||||
startTime: '14:30',
|
||||
endTime: '16:30',
|
||||
days: '2,4',
|
||||
createdAt: nowTs,
|
||||
updatedAt: nowTs
|
||||
})
|
||||
.run();
|
||||
shift = db.select().from(schema.shifts).get();
|
||||
}
|
||||
|
||||
if (!shift) {
|
||||
return fail(500, { error: 'Não foi possível encontrar ou criar um turno padrão.' });
|
||||
}
|
||||
|
||||
// 3. Ensure test beneficiaries #1, #2, #3 exist
|
||||
const testCreationTime = Date.now() - 35 * 24 * 60 * 60 * 1000;
|
||||
for (const num of [1, 2, 3]) {
|
||||
const existing = db
|
||||
.select()
|
||||
.from(schema.beneficiaries)
|
||||
.where(eq(schema.beneficiaries.number, num))
|
||||
.get();
|
||||
|
||||
if (!existing) {
|
||||
db.insert(schema.beneficiaries)
|
||||
.values({
|
||||
number: num,
|
||||
name: `Beneficiário Teste #${num}`,
|
||||
contact: `91234560${num}`,
|
||||
householdSize: num,
|
||||
status: 'ativo',
|
||||
isParent: true,
|
||||
createdAt: testCreationTime,
|
||||
updatedAt: Date.now()
|
||||
})
|
||||
.run();
|
||||
} else {
|
||||
db.update(schema.beneficiaries)
|
||||
.set({
|
||||
status: 'ativo',
|
||||
isParent: true,
|
||||
createdAt: testCreationTime,
|
||||
updatedAt: Date.now()
|
||||
})
|
||||
.where(eq(schema.beneficiaries.id, existing.id))
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fetch all active parent beneficiaries
|
||||
const activeBeneficiaries = db
|
||||
.select()
|
||||
.from(schema.beneficiaries)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.beneficiaries.status, 'ativo'),
|
||||
eq(schema.beneficiaries.isParent, true)
|
||||
)
|
||||
)
|
||||
.all();
|
||||
|
||||
// 5. Calculate Tuesdays & Thursdays in the last 30 days
|
||||
const dates: string[] = [];
|
||||
const today = new Date();
|
||||
const todayStr = today.toLocaleDateString('sv-SE', { timeZone: 'Europe/Lisbon' });
|
||||
|
||||
for (let i = 1; i <= 30; i++) {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - i);
|
||||
const dayOfWeek = d.getDay();
|
||||
|
||||
if (dayOfWeek === 2 || dayOfWeek === 4) {
|
||||
const dateStr = d.toLocaleDateString('sv-SE', { timeZone: 'Europe/Lisbon' });
|
||||
if (dateStr < todayStr) {
|
||||
dates.push(dateStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dates.sort();
|
||||
|
||||
if (dates.length === 0) {
|
||||
return fail(400, { error: 'Não foram encontradas datas de entregas válidas nos últimos 30 dias.' });
|
||||
}
|
||||
|
||||
let insertedCount = 0;
|
||||
for (const b of activeBeneficiaries) {
|
||||
let datesToInsert = [...dates];
|
||||
|
||||
if (b.number === 1) {
|
||||
datesToInsert = dates.slice(1);
|
||||
} else if (b.number === 2) {
|
||||
datesToInsert = dates.slice(2);
|
||||
} else if (b.number === 3) {
|
||||
datesToInsert = dates.slice(3);
|
||||
}
|
||||
|
||||
for (const dateStr of datesToInsert) {
|
||||
db.insert(schema.deliveries)
|
||||
.values({
|
||||
beneficiaryId: b.id,
|
||||
shiftId: shift.id,
|
||||
date: dateStr,
|
||||
createdAt: Date.now()
|
||||
})
|
||||
.run();
|
||||
insertedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
totalDays: dates.length,
|
||||
insertedCount
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error('Error generating dummy data:', err);
|
||||
return fail(500, { error: err.message || 'Erro interno ao gerar dados.' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
let { form } = $props();
|
||||
let isLoading = $state(false);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Dummy Data Generator - RefoodOne</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="mb-4">
|
||||
<h2 class="fw-bold text-dark mb-1">Dummy Data Generator</h2>
|
||||
<p class="text-muted mb-0">Ferramenta para redefinir e carregar dados fictícios de entregas</p>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
<div class="alert alert-danger border-0 shadow-sm rounded-3 mb-4 d-flex align-items-center gap-2" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-exclamation-triangle-fill text-danger" viewBox="0 0 16 16">
|
||||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||
</svg>
|
||||
<div>
|
||||
{form.error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if form?.success}
|
||||
<div class="alert alert-success border-0 shadow-sm rounded-3 mb-4 d-flex align-items-center gap-2" role="alert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-check-circle-fill text-success" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
||||
</svg>
|
||||
<div>
|
||||
Dados gerados com sucesso! Foram limpos todos os registos e inseridas <strong>{form.insertedCount} entregas</strong> ao longo de <strong>{form.totalDays} dias de entrega</strong>.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="card border-0 shadow-sm rounded-4 p-4 bg-white mb-4" style="max-width: 700px;">
|
||||
<h5 class="fw-bold text-dark mb-3">Como funciona a geração de dados?</h5>
|
||||
|
||||
<div class="alert alert-warning border-0 rounded-3 mb-4" role="alert">
|
||||
<div class="d-flex gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-exclamation-triangle-fill flex-shrink-0" viewBox="0 0 16 16">
|
||||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong>Atenção:</strong> Esta ação irá apagar permanentemente todas as entregas atuais registadas na base de dados antes de carregar o conjunto de teste.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="list-group list-group-flush mb-4">
|
||||
<li class="list-group-item border-0 px-0 py-2.5 d-flex align-items-start gap-2">
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle rounded-circle p-1">✔</span>
|
||||
<div>
|
||||
<strong>Limpeza Completa:</strong> Apaga todos os registos da tabela <code>deliveries</code>.
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item border-0 px-0 py-2.5 d-flex align-items-start gap-2">
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle rounded-circle p-1">✔</span>
|
||||
<div>
|
||||
<strong>Garantia de Turno:</strong> Cria um turno padrão (T1) às Terças e Quintas caso nenhum esteja configurado.
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item border-0 px-0 py-2.5 d-flex align-items-start gap-2">
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle rounded-circle p-1">✔</span>
|
||||
<div>
|
||||
<strong>Garantia de Beneficiários:</strong> Cria ou ativa os beneficiários <strong>#1, #2 e #3</strong> (configurados como principais/agregado e com data de criação superior a 35 dias).
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item border-0 px-0 py-2.5 d-flex align-items-start gap-2">
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle rounded-circle p-1">✔</span>
|
||||
<div>
|
||||
<strong>Distribuição de Ausências nos Últimos 30 Dias:</strong>
|
||||
<ul class="mt-1 mb-0 ps-3 small text-secondary">
|
||||
<li>O beneficiário <strong>#1</strong> faltará exatamente a <strong>1 entrega</strong>.</li>
|
||||
<li>O beneficiário <strong>#2</strong> faltará exatamente a <strong>2 entregas</strong>.</li>
|
||||
<li>O beneficiário <strong>#3</strong> faltará exatamente a <strong>3 entregas</strong>.</li>
|
||||
<li>Os restantes beneficiários ativos receberão em todas as datas (0 faltas).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/generate"
|
||||
use:enhance={() => {
|
||||
isLoading = true;
|
||||
return async ({ update }) => {
|
||||
isLoading = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
onsubmit={(e) => {
|
||||
if (!confirm('Tem a certeza que deseja APAGAR todas as entregas e gerar os dados de teste?')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-danger rounded-3 px-4 py-2.5 fw-bold"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{#if isLoading}
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
A Gerar Dados...
|
||||
{:else}
|
||||
Generate Data
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if form?.success}
|
||||
<a href="/admin/ausencias" class="btn btn-outline-success rounded-3 px-4 py-2.5 fw-semibold">
|
||||
Ver Relatório de Ausências →
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user