Implementert Firebase backend konfigurasjon og Cloud Functions
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
import * as functions from 'firebase-functions';
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
admin.initializeApp();
|
||||
|
||||
const db = admin.firestore();
|
||||
|
||||
// ===== TYPER =====
|
||||
|
||||
interface TariffRules {
|
||||
maxDailyHours: number;
|
||||
maxWeeklyHours: number;
|
||||
minDailyRest: number; // minutter
|
||||
minWeeklyRest: number; // minutter
|
||||
useAverageCalculation: boolean;
|
||||
averagePeriodWeeks?: number;
|
||||
maxAverageHours?: number;
|
||||
}
|
||||
|
||||
interface TimeRegistration {
|
||||
id: string;
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
startTime: admin.firestore.Timestamp;
|
||||
endTime: admin.firestore.Timestamp | null;
|
||||
duration: number; // minutter
|
||||
breaks: Array<{
|
||||
startTime: admin.firestore.Timestamp;
|
||||
endTime: admin.firestore.Timestamp;
|
||||
duration: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
// ===== CLOUD FUNCTION: Evaluer timeregistrering =====
|
||||
|
||||
export const evaluateTimeRegistration = functions.firestore
|
||||
.document('time_registrations/{registrationId}')
|
||||
.onWrite(async (change, context) => {
|
||||
// Ignorer sletting
|
||||
if (!change.after.exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const registration = change.after.data() as TimeRegistration;
|
||||
|
||||
// Ignorer hvis ikke fullført
|
||||
if (!registration.endTime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userId = registration.userId;
|
||||
|
||||
try {
|
||||
// Hent brukerens tariffprofil
|
||||
const userDoc = await db.collection('users').doc(userId).get();
|
||||
if (!userDoc.exists) {
|
||||
console.error('Bruker ikke funnet:', userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const userData = userDoc.data()!;
|
||||
const tariffProfileId = userData.tariffProfileId;
|
||||
|
||||
const profileDoc = await db.collection('tariff_profiles').doc(tariffProfileId).get();
|
||||
if (!profileDoc.exists) {
|
||||
console.error('Tariffprofil ikke funnet:', tariffProfileId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const rules = profileDoc.data()!.rules as TariffRules;
|
||||
|
||||
// Kjør evalueringer
|
||||
await Promise.all([
|
||||
checkDailyLimit(userId, registration, rules),
|
||||
checkWeeklyLimit(userId, registration, rules),
|
||||
checkDailyRest(userId, registration, rules),
|
||||
]);
|
||||
|
||||
console.log('Evaluering fullført for registrering:', context.params.registrationId);
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Feil ved evaluering:', error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// ===== SJEKK DAGLIG GRENSE =====
|
||||
|
||||
async function checkDailyLimit(
|
||||
userId: string,
|
||||
registration: TimeRegistration,
|
||||
rules: TariffRules
|
||||
): Promise<void> {
|
||||
const regDate = registration.startTime.toDate();
|
||||
const startOfDay = new Date(regDate.getFullYear(), regDate.getMonth(), regDate.getDate());
|
||||
const endOfDay = new Date(regDate.getFullYear(), regDate.getMonth(), regDate.getDate(), 23, 59, 59);
|
||||
|
||||
// Hent alle registreringer for dagen
|
||||
const snapshot = await db.collection('time_registrations')
|
||||
.where('userId', '==', userId)
|
||||
.where('startTime', '>=', admin.firestore.Timestamp.fromDate(startOfDay))
|
||||
.where('startTime', '<=', admin.firestore.Timestamp.fromDate(endOfDay))
|
||||
.get();
|
||||
|
||||
let totalMinutes = 0;
|
||||
snapshot.docs.forEach(doc => {
|
||||
const data = doc.data();
|
||||
if (data.endTime) {
|
||||
const netMinutes = data.duration - (data.breaks || []).reduce((sum: number, b: any) => sum + b.duration, 0);
|
||||
totalMinutes += netMinutes;
|
||||
}
|
||||
});
|
||||
|
||||
const totalHours = totalMinutes / 60;
|
||||
const maxHours = rules.maxDailyHours;
|
||||
|
||||
if (totalHours > maxHours) {
|
||||
await createDeviation(
|
||||
userId,
|
||||
registration.organizationId,
|
||||
registration.id,
|
||||
'dailyMax',
|
||||
'violation',
|
||||
`Daglig arbeidstid overskredet: ${totalHours.toFixed(1)} timer (maks ${maxHours} timer)`,
|
||||
{
|
||||
actualValue: totalHours,
|
||||
limitValue: maxHours,
|
||||
period: startOfDay.toISOString().split('T')[0],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== SJEKK UKENTLIG GRENSE =====
|
||||
|
||||
async function checkWeeklyLimit(
|
||||
userId: string,
|
||||
registration: TimeRegistration,
|
||||
rules: TariffRules
|
||||
): Promise<void> {
|
||||
const regDate = registration.startTime.toDate();
|
||||
|
||||
// Finn mandag i uken
|
||||
const dayOfWeek = regDate.getDay();
|
||||
const diff = regDate.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
|
||||
const monday = new Date(regDate.getFullYear(), regDate.getMonth(), diff);
|
||||
monday.setHours(0, 0, 0, 0);
|
||||
|
||||
const sunday = new Date(monday);
|
||||
sunday.setDate(monday.getDate() + 6);
|
||||
sunday.setHours(23, 59, 59, 999);
|
||||
|
||||
// Hent alle registreringer for uken
|
||||
const snapshot = await db.collection('time_registrations')
|
||||
.where('userId', '==', userId)
|
||||
.where('startTime', '>=', admin.firestore.Timestamp.fromDate(monday))
|
||||
.where('startTime', '<=', admin.firestore.Timestamp.fromDate(sunday))
|
||||
.get();
|
||||
|
||||
let totalMinutes = 0;
|
||||
snapshot.docs.forEach(doc => {
|
||||
const data = doc.data();
|
||||
if (data.endTime) {
|
||||
const netMinutes = data.duration - (data.breaks || []).reduce((sum: number, b: any) => sum + b.duration, 0);
|
||||
totalMinutes += netMinutes;
|
||||
}
|
||||
});
|
||||
|
||||
const totalHours = totalMinutes / 60;
|
||||
const maxHours = rules.maxWeeklyHours;
|
||||
|
||||
if (totalHours > maxHours) {
|
||||
const weekNumber = getWeekNumber(monday);
|
||||
await createDeviation(
|
||||
userId,
|
||||
registration.organizationId,
|
||||
registration.id,
|
||||
'weeklyMax',
|
||||
'violation',
|
||||
`Ukentlig arbeidstid overskredet: ${totalHours.toFixed(1)} timer (maks ${maxHours} timer)`,
|
||||
{
|
||||
actualValue: totalHours,
|
||||
limitValue: maxHours,
|
||||
period: `${regDate.getFullYear()}-W${weekNumber}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== SJEKK DAGLIG HVILETID =====
|
||||
|
||||
async function checkDailyRest(
|
||||
userId: string,
|
||||
registration: TimeRegistration,
|
||||
rules: TariffRules
|
||||
): Promise<void> {
|
||||
if (!registration.endTime) return;
|
||||
|
||||
const endTime = registration.endTime.toDate();
|
||||
|
||||
// Finn neste registrering
|
||||
const nextSnapshot = await db.collection('time_registrations')
|
||||
.where('userId', '==', userId)
|
||||
.where('startTime', '>', registration.endTime)
|
||||
.orderBy('startTime', 'asc')
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (nextSnapshot.empty) return;
|
||||
|
||||
const nextReg = nextSnapshot.docs[0].data();
|
||||
const nextStartTime = nextReg.startTime.toDate();
|
||||
|
||||
const restMinutes = (nextStartTime.getTime() - endTime.getTime()) / (1000 * 60);
|
||||
const minRestMinutes = rules.minDailyRest;
|
||||
|
||||
if (restMinutes < minRestMinutes) {
|
||||
const restHours = restMinutes / 60;
|
||||
const minRestHours = minRestMinutes / 60;
|
||||
|
||||
await createDeviation(
|
||||
userId,
|
||||
registration.organizationId,
|
||||
registration.id,
|
||||
'dailyRest',
|
||||
'violation',
|
||||
`Daglig hviletid for kort: ${restHours.toFixed(1)} timer (minimum ${minRestHours} timer)`,
|
||||
{
|
||||
actualValue: restHours,
|
||||
limitValue: minRestHours,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== OPPRETT AVVIK =====
|
||||
|
||||
async function createDeviation(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
timeRegistrationId: string,
|
||||
type: string,
|
||||
severity: string,
|
||||
description: string,
|
||||
metadata: any
|
||||
): Promise<void> {
|
||||
// Sjekk om avvik allerede eksisterer
|
||||
const existing = await db.collection('deviations')
|
||||
.where('userId', '==', userId)
|
||||
.where('timeRegistrationId', '==', timeRegistrationId)
|
||||
.where('type', '==', type)
|
||||
.get();
|
||||
|
||||
if (!existing.empty) {
|
||||
console.log('Avvik eksisterer allerede for denne registreringen og typen');
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection('deviations').add({
|
||||
userId,
|
||||
organizationId,
|
||||
timeRegistrationId,
|
||||
type,
|
||||
severity,
|
||||
description,
|
||||
detectedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
acknowledgedAt: null,
|
||||
acknowledgedBy: null,
|
||||
metadata,
|
||||
});
|
||||
|
||||
console.log('Avvik opprettet:', type, 'for bruker:', userId);
|
||||
}
|
||||
|
||||
// ===== HJELPEFUNKSJONER =====
|
||||
|
||||
function getWeekNumber(date: Date): number {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
||||
}
|
||||
|
||||
// ===== AUDIT LOG =====
|
||||
|
||||
export const createAuditLog = functions.firestore
|
||||
.document('{collection}/{documentId}')
|
||||
.onUpdate(async (change, context) => {
|
||||
const collection = context.params.collection;
|
||||
|
||||
// Kun logg for spesifikke collections
|
||||
if (!['time_registrations', 'users', 'tariff_profiles'].includes(collection)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const before = change.before.data();
|
||||
const after = change.after.data();
|
||||
|
||||
await db.collection('audit_logs').add({
|
||||
userId: after.lastModifiedBy || after.userId || 'system',
|
||||
organizationId: after.organizationId || 'unknown',
|
||||
action: 'update',
|
||||
entityType: collection === 'time_registrations' ? 'timeRegistration' :
|
||||
collection === 'users' ? 'user' : 'tariffProfile',
|
||||
entityId: context.params.documentId,
|
||||
changes: {
|
||||
before,
|
||||
after,
|
||||
},
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
Reference in New Issue
Block a user