diff --git a/src/routes/admin/ausencias/+page.server.ts b/src/routes/admin/ausencias/+page.server.ts
new file mode 100644
index 0000000..e505989
--- /dev/null
+++ b/src/routes/admin/ausencias/+page.server.ts
@@ -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
`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.'
+ };
+ }
+};
diff --git a/src/routes/admin/ausencias/+page.svelte b/src/routes/admin/ausencias/+page.svelte
new file mode 100644
index 0000000..caa5088
--- /dev/null
+++ b/src/routes/admin/ausencias/+page.svelte
@@ -0,0 +1,237 @@
+
+
+
+ Ausências de Beneficiários - RefoodOne
+
+
+
+
+
Ausências de Beneficiários
+
Controlo de faltas e assiduidade no período selecionado (entregas 2 vezes por semana)
+
+
+ {#if data.error}
+
+ {data.error}
+
+ {/if}
+
+
+
+
+
+
+
+
+
Dias de Entrega Ocorridos
+ {data.totalDeliveryDays} dias
+ no período selecionado
+
+
+
+
+
+
+
+
+
+
+
Beneficiários com Faltas
+ {data.beneficiaries.length}
+ ativos e com faltas registadas
+
+
+
+
+
+
+
+
+
+
+
Filtro de Relevância
+ ≥ {data.minAbsences} {data.minAbsences === 1 ? 'falta' : 'faltas'}
+ limiar selecionado para aviso
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Número |
+ Nome |
+ Contacto |
+ Faltas / Entregas |
+ Assiduidade |
+ Dias com Ausência no Período |
+ Última Entrega |
+ Ações |
+
+
+
+ {#if data.beneficiaries.length === 0}
+
+ |
+
+ Nenhum beneficiário com ausências registadas.
+ |
+
+ {:else}
+ {#each data.beneficiaries as b}
+ {@const assid = calculateAssiduity(b.receivedCount, b.totalRelevantDeliveries)}
+
+ |
+ #{b.number}
+ |
+
+ {b.name}
+ |
+
+ {b.contact || 'Sem contacto'}
+ |
+
+ {b.absencesCount} / {b.totalRelevantDeliveries}
+ |
+
+
+ {assid}%
+
+ |
+
+
+ {#each b.missedDates as date}
+
+ {formatDate(date)}
+
+ {/each}
+
+ |
+
+ {b.lastDelivery ? formatDate(b.lastDelivery) : 'Nunca'}
+ |
+
+
+
+ Ficha
+
+ |
+
+ {/each}
+ {/if}
+
+
+
+
+
+
+
diff --git a/src/routes/entregas/+page.server.ts b/src/routes/entregas/+page.server.ts
index 9494f96..7c7c903 100644
--- a/src/routes/entregas/+page.server.ts
+++ b/src/routes/entregas/+page.server.ts
@@ -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))
diff --git a/src/routes/entregas/+page.svelte b/src/routes/entregas/+page.svelte
index 40d61b1..f9a92e9 100644
--- a/src/routes/entregas/+page.svelte
+++ b/src/routes/entregas/+page.svelte
@@ -6,12 +6,14 @@
let activeModalBeneficiary = $state(null);
let activeDeleteDelivery = $state(null);
let isLoading = $state(false);
+ let dateInput = $state(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;
+ }
+ }
@@ -49,14 +62,32 @@
Registo de Entregas
-
Selecione o número do beneficiário para registar a entrega do cabaz de hoje
+ {#if isToday}
+
Selecione o número do beneficiário para registar a entrega do cabaz de hoje
+ {:else}
+
Visualização de registos de entrega para a data selecionada (alterações desabilitadas)
+ {/if}
-
-
-
{new Date().toLocaleDateString('pt-PT', { timeZone: 'Europe/Lisbon', weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
+
+
+
+
+
{formatDateLong(data.selectedDate)}
+
+
+
@@ -92,9 +123,9 @@
{@const isDelivered = deliveredSet.has(beneficiary.id)}