diff --git a/backend/src/modules/helpdesk/helpdesk.contact.service.ts b/backend/src/modules/helpdesk/helpdesk.contact.service.ts index f8337df..15ef4ff 100644 --- a/backend/src/modules/helpdesk/helpdesk.contact.service.ts +++ b/backend/src/modules/helpdesk/helpdesk.contact.service.ts @@ -1,5 +1,7 @@ // modules/helpdesk/helpdesk.contact.service.ts import { FastifyInstance } from 'fastify' +import { and, eq, or } from "drizzle-orm"; +import { helpdesk_contacts } from "../../../db/schema"; export async function getOrCreateContact( server: FastifyInstance, @@ -9,30 +11,35 @@ export async function getOrCreateContact( if (!email && !phone) throw new Error('Contact must have at least an email or phone') // Bestehenden Kontakt prüfen - const { data: existing, error: findError } = await server.supabase - .from('helpdesk_contacts') - .select('*') - .eq('tenant_id', tenant_id) - .or(`email.eq.${email || ''},phone.eq.${phone || ''}`) - .maybeSingle() + const matchConditions = [] + if (email) matchConditions.push(eq(helpdesk_contacts.email, email)) + if (phone) matchConditions.push(eq(helpdesk_contacts.phone, phone)) - if (findError) throw findError - if (existing) return existing + const existing = await server.db + .select() + .from(helpdesk_contacts) + .where( + and( + eq(helpdesk_contacts.tenantId, tenant_id), + or(...matchConditions) + ) + ) + .limit(1) + + if (existing[0]) return existing[0] // Anlegen - const { data: created, error: insertError } = await server.supabase - .from('helpdesk_contacts') - .insert({ - tenant_id, + const created = await server.db + .insert(helpdesk_contacts) + .values({ + tenantId: tenant_id, email, phone, - display_name, - customer_id, - contact_id + displayName: display_name, + customerId: customer_id, + contactId: contact_id }) - .select() - .single() + .returning() - if (insertError) throw insertError - return created + return created[0] } diff --git a/backend/src/modules/helpdesk/helpdesk.conversation.service.ts b/backend/src/modules/helpdesk/helpdesk.conversation.service.ts index b7edc2b..ca12414 100644 --- a/backend/src/modules/helpdesk/helpdesk.conversation.service.ts +++ b/backend/src/modules/helpdesk/helpdesk.conversation.service.ts @@ -2,6 +2,8 @@ import { FastifyInstance } from 'fastify' import { getOrCreateContact } from './helpdesk.contact.service.js' import {useNextNumberRangeNumber} from "../../utils/functions"; +import { and, desc, eq } from "drizzle-orm"; +import { customers, helpdesk_contacts, helpdesk_conversations } from "../../../db/schema"; export async function createConversation( server: FastifyInstance, @@ -25,24 +27,34 @@ export async function createConversation( const {usedNumber } = await useNextNumberRangeNumber(server, tenant_id, "tickets") - const { data, error } = await server.supabase - .from('helpdesk_conversations') - .insert({ - tenant_id, - contact_id: contactRecord.id, - channel_instance_id, + const inserted = await server.db + .insert(helpdesk_conversations) + .values({ + tenantId: tenant_id, + contactId: contactRecord.id, + channelInstanceId: channel_instance_id, subject: subject || null, status: 'open', - created_at: new Date().toISOString(), - customer_id, - contact_person_id, - ticket_number: usedNumber + createdAt: new Date(), + customerId: customer_id, + contactPersonId: contact_person_id, + ticketNumber: usedNumber }) - .select() - .single() + .returning() - if (error) throw error - return data + const data = inserted[0] + + return { + ...data, + channel_instance_id: data.channelInstanceId, + contact_id: data.contactId, + contact_person_id: data.contactPersonId, + created_at: data.createdAt, + customer_id: data.customerId, + last_message_at: data.lastMessageAt, + tenant_id: data.tenantId, + ticket_number: data.ticketNumber, + } } export async function getConversations( @@ -52,22 +64,34 @@ export async function getConversations( ) { const { status, limit = 50 } = opts || {} - let query = server.supabase.from('helpdesk_conversations').select('*, customer_id(*)').eq('tenant_id', tenant_id) + const filters = [eq(helpdesk_conversations.tenantId, tenant_id)] + if (status) filters.push(eq(helpdesk_conversations.status, status)) - if (status) query = query.eq('status', status) - query = query.order('last_message_at', { ascending: false }).limit(limit) + const data = await server.db + .select({ + conversation: helpdesk_conversations, + contact: helpdesk_contacts, + customer: customers, + }) + .from(helpdesk_conversations) + .leftJoin(helpdesk_contacts, eq(helpdesk_contacts.id, helpdesk_conversations.contactId)) + .leftJoin(customers, eq(customers.id, helpdesk_conversations.customerId)) + .where(and(...filters)) + .orderBy(desc(helpdesk_conversations.lastMessageAt)) + .limit(limit) - const { data, error } = await query - if (error) throw error - - const mappedData = data.map(entry => { - return { - ...entry, - customer: entry.customer_id - } - }) - - return mappedData + return data.map((entry) => ({ + ...entry.conversation, + helpdesk_contacts: entry.contact, + channel_instance_id: entry.conversation.channelInstanceId, + contact_id: entry.conversation.contactId, + contact_person_id: entry.conversation.contactPersonId, + created_at: entry.conversation.createdAt, + customer_id: entry.customer, + last_message_at: entry.conversation.lastMessageAt, + tenant_id: entry.conversation.tenantId, + ticket_number: entry.conversation.ticketNumber, + })) } export async function updateConversationStatus( @@ -78,13 +102,22 @@ export async function updateConversationStatus( const valid = ['open', 'in_progress', 'waiting_for_customer', 'answered', 'closed'] if (!valid.includes(status)) throw new Error('Invalid status') - const { data, error } = await server.supabase - .from('helpdesk_conversations') - .update({ status }) - .eq('id', conversation_id) - .select() - .single() + const updated = await server.db + .update(helpdesk_conversations) + .set({ status }) + .where(eq(helpdesk_conversations.id, conversation_id)) + .returning() - if (error) throw error - return data + const data = updated[0] + return { + ...data, + channel_instance_id: data.channelInstanceId, + contact_id: data.contactId, + contact_person_id: data.contactPersonId, + created_at: data.createdAt, + customer_id: data.customerId, + last_message_at: data.lastMessageAt, + tenant_id: data.tenantId, + ticket_number: data.ticketNumber, + } } diff --git a/backend/src/modules/helpdesk/helpdesk.message.service.ts b/backend/src/modules/helpdesk/helpdesk.message.service.ts index 2d942eb..e8f1ab3 100644 --- a/backend/src/modules/helpdesk/helpdesk.message.service.ts +++ b/backend/src/modules/helpdesk/helpdesk.message.service.ts @@ -1,5 +1,7 @@ // modules/helpdesk/helpdesk.message.service.ts import { FastifyInstance } from 'fastify' +import { asc, eq } from "drizzle-orm"; +import { helpdesk_conversations, helpdesk_messages } from "../../../db/schema"; export async function addMessage( server: FastifyInstance, @@ -23,38 +25,53 @@ export async function addMessage( ) { if (!payload?.text) throw new Error('Message payload requires text content') - const { data: message, error } = await server.supabase - .from('helpdesk_messages') - .insert({ - tenant_id, - conversation_id, - author_user_id, + const inserted = await server.db + .insert(helpdesk_messages) + .values({ + tenantId: tenant_id, + conversationId: conversation_id, + authorUserId: author_user_id, direction, payload, - raw_meta, - created_at: new Date().toISOString(), + rawMeta: raw_meta, + externalMessageId: external_message_id, + receivedAt: new Date(), }) - .select() - .single() + .returning() - if (error) throw error + const message = inserted[0] // Letzte Nachricht aktualisieren - await server.supabase - .from('helpdesk_conversations') - .update({ last_message_at: new Date().toISOString() }) - .eq('id', conversation_id) + await server.db + .update(helpdesk_conversations) + .set({ lastMessageAt: new Date() }) + .where(eq(helpdesk_conversations.id, conversation_id)) - return message + return { + ...message, + author_user_id: message.authorUserId, + conversation_id: message.conversationId, + created_at: message.createdAt, + external_message_id: message.externalMessageId, + raw_meta: message.rawMeta, + tenant_id: message.tenantId, + } } export async function getMessages(server: FastifyInstance, conversation_id: string) { - const { data, error } = await server.supabase - .from('helpdesk_messages') - .select('*') - .eq('conversation_id', conversation_id) - .order('created_at', { ascending: true }) + const data = await server.db + .select() + .from(helpdesk_messages) + .where(eq(helpdesk_messages.conversationId, conversation_id)) + .orderBy(asc(helpdesk_messages.createdAt)) - if (error) throw error - return data + return data.map((message) => ({ + ...message, + author_user_id: message.authorUserId, + conversation_id: message.conversationId, + created_at: message.createdAt, + external_message_id: message.externalMessageId, + raw_meta: message.rawMeta, + tenant_id: message.tenantId, + })) } diff --git a/backend/src/modules/notification.service.ts b/backend/src/modules/notification.service.ts index 241fea4..85a1068 100644 --- a/backend/src/modules/notification.service.ts +++ b/backend/src/modules/notification.service.ts @@ -1,6 +1,8 @@ // services/notification.service.ts import type { FastifyInstance } from 'fastify'; import {secrets} from "../utils/secrets"; +import { eq } from "drizzle-orm"; +import { notificationsEventTypes, notificationsItems } from "../../db/schema"; export type NotificationStatus = 'queued' | 'sent' | 'failed'; @@ -34,16 +36,16 @@ export class NotificationService { */ async trigger(input: TriggerInput) { const { tenantId, userId, eventType, title, message, payload } = input; - const supabase = this.server.supabase; // 1) Event-Typ prüfen (aktiv?) - const { data: eventTypeRow, error: etErr } = await supabase - .from('notifications_event_types') - .select('event_key,is_active') - .eq('event_key', eventType) - .maybeSingle(); + const eventTypeRows = await this.server.db + .select() + .from(notificationsEventTypes) + .where(eq(notificationsEventTypes.eventKey, eventType)) + .limit(1) + const eventTypeRow = eventTypeRows[0] - if (etErr || !eventTypeRow || eventTypeRow.is_active !== true) { + if (!eventTypeRow || eventTypeRow.isActive !== true) { throw new Error(`Unbekannter oder inaktiver Event-Typ: ${eventType}`); } @@ -54,40 +56,40 @@ export class NotificationService { } // 3) Notification anlegen (status: queued) - const { data: inserted, error: insErr } = await supabase - .from('notifications_items') - .insert({ - tenant_id: tenantId, - user_id: userId, - event_type: eventType, + const insertedRows = await this.server.db + .insert(notificationsItems) + .values({ + tenantId, + userId, + eventType, title, message, payload: payload ?? null, channel: 'email', status: 'queued' }) - .select('id') - .single(); + .returning({ id: notificationsItems.id }) + const inserted = insertedRows[0] - if (insErr || !inserted) { - throw new Error(`Fehler beim Einfügen der Notification: ${insErr?.message}`); + if (!inserted) { + throw new Error("Fehler beim Einfügen der Notification"); } // 4) E-Mail versenden try { await this.sendEmail(user.email, title, message); - await supabase - .from('notifications_items') - .update({ status: 'sent', sent_at: new Date().toISOString() }) - .eq('id', inserted.id); + await this.server.db + .update(notificationsItems) + .set({ status: 'sent', sentAt: new Date() }) + .where(eq(notificationsItems.id, inserted.id)); return { success: true, id: inserted.id }; } catch (err: any) { - await supabase - .from('notifications_items') - .update({ status: 'failed', error: String(err?.message || err) }) - .eq('id', inserted.id); + await this.server.db + .update(notificationsItems) + .set({ status: 'failed', error: String(err?.message || err) }) + .where(eq(notificationsItems.id, inserted.id)); this.server.log.error({ err, notificationId: inserted.id }, 'E-Mail Versand fehlgeschlagen'); return { success: false, error: err?.message || 'E-Mail Versand fehlgeschlagen' }; diff --git a/backend/src/plugins/tenant.ts b/backend/src/plugins/tenant.ts index 0556b36..e48a7f1 100644 --- a/backend/src/plugins/tenant.ts +++ b/backend/src/plugins/tenant.ts @@ -1,5 +1,7 @@ import { FastifyInstance, FastifyRequest } from "fastify"; import fp from "fastify-plugin"; +import { eq } from "drizzle-orm"; +import { tenants } from "../../db/schema"; export default fp(async (server: FastifyInstance) => { server.addHook("preHandler", async (req, reply) => { @@ -9,11 +11,12 @@ export default fp(async (server: FastifyInstance) => { return; } // Tenant aus DB laden - const { data: tenant } = await server.supabase - .from("tenants") - .select("*") - .eq("portalDomain", host) - .single(); + const rows = await server.db + .select() + .from(tenants) + .where(eq(tenants.portalDomain, host)) + .limit(1); + const tenant = rows[0]; if(!tenant) { @@ -38,4 +41,4 @@ declare module "fastify" { settings?: Record; }; } -} \ No newline at end of file +} diff --git a/backend/src/routes/functions.ts b/backend/src/routes/functions.ts index c39da62..e94917d 100644 --- a/backend/src/routes/functions.ts +++ b/backend/src/routes/functions.ts @@ -110,17 +110,6 @@ export default async function functionRoutes(server: FastifyInstance) { const data = await server.db.select().from(citys).where(eq(citys.zip,zip)) - /*const { data, error } = await server.supabase - .from('citys') - .select() - .eq('zip', zip) - .maybeSingle() - - if (error) { - console.log(error) - return reply.code(500).send({ error: 'Database error' }) - }*/ - if (!data) { return reply.code(404).send({ error: 'ZIP not found' }) } @@ -224,4 +213,4 @@ export default async function functionRoutes(server: FastifyInstance) { } })*/ -} \ No newline at end of file +} diff --git a/backend/src/routes/helpdesk.inbound.email.ts b/backend/src/routes/helpdesk.inbound.email.ts index 5b24a4d..f26fbd1 100644 --- a/backend/src/routes/helpdesk.inbound.email.ts +++ b/backend/src/routes/helpdesk.inbound.email.ts @@ -3,8 +3,9 @@ import { FastifyPluginAsync } from 'fastify' import { createConversation } from '../modules/helpdesk/helpdesk.conversation.service.js' import { addMessage } from '../modules/helpdesk/helpdesk.message.service.js' import { getOrCreateContact } from '../modules/helpdesk/helpdesk.contact.service.js' -import {extractDomain, findCustomerOrContactByEmailOrDomain} from "../utils/helpers"; -import {useNextNumberRangeNumber} from "../utils/functions"; +import { findCustomerOrContactByEmailOrDomain } from "../utils/helpers"; +import { eq } from "drizzle-orm"; +import { helpdesk_conversations, helpdesk_messages } from "../../db/schema"; // ------------------------------------------------------------- // 📧 Interne M2M-Route für eingehende E-Mails @@ -52,12 +53,12 @@ const helpdeskInboundEmailRoutes: FastifyPluginAsync = async (server) => { // 3️⃣ Konversation anhand In-Reply-To suchen let conversationId: string | null = null if (in_reply_to) { - const { data: msg } = await server.supabase - .from('helpdesk_messages') - .select('conversation_id') - .eq('external_message_id', in_reply_to) - .maybeSingle() - conversationId = msg?.conversation_id || null + const msg = await server.db + .select({ conversationId: helpdesk_messages.conversationId }) + .from(helpdesk_messages) + .where(eq(helpdesk_messages.externalMessageId, in_reply_to)) + .limit(1) + conversationId = msg[0]?.conversationId || null } // 4️⃣ Neue Konversation anlegen falls keine existiert @@ -73,12 +74,12 @@ const helpdeskInboundEmailRoutes: FastifyPluginAsync = async (server) => { }) conversationId = conversation.id } else { - const { data } = await server.supabase - .from('helpdesk_conversations') - .select('*') - .eq('id', conversationId) - .single() - conversation = data + const rows = await server.db + .select() + .from(helpdesk_conversations) + .where(eq(helpdesk_conversations.id, conversationId)) + .limit(1) + conversation = rows[0] } // 5️⃣ Nachricht speichern @@ -96,7 +97,7 @@ const helpdeskInboundEmailRoutes: FastifyPluginAsync = async (server) => { return res.status(201).send({ success: true, conversation_id: conversationId, - ticket_number: conversation.ticket_number, + ticket_number: conversation?.ticket_number || conversation?.ticketNumber, }) }) } diff --git a/backend/src/routes/helpdesk.inbound.ts b/backend/src/routes/helpdesk.inbound.ts index 49e8ec7..50d7dbf 100644 --- a/backend/src/routes/helpdesk.inbound.ts +++ b/backend/src/routes/helpdesk.inbound.ts @@ -3,70 +3,9 @@ import { FastifyPluginAsync } from 'fastify' import { createConversation } from '../modules/helpdesk/helpdesk.conversation.service.js' import { addMessage } from '../modules/helpdesk/helpdesk.message.service.js' import { getOrCreateContact } from '../modules/helpdesk/helpdesk.contact.service.js' - -/** - * Öffentliche Route zum Empfang eingehender Kontaktformular-Nachrichten. - * Authentifizierung: über `public_token` aus helpdesk_channel_instances - */ - -function extractDomain(email) { - if (!email) return null - const parts = email.split("@") - return parts.length === 2 ? parts[1].toLowerCase() : null -} - -async function findCustomerOrContactByEmailOrDomain(server,fromMail, tenantId) { - const sender = fromMail - const senderDomain = extractDomain(sender) - if (!senderDomain) return null - - - // 1️⃣ Direkter Match über contacts - const { data: contactMatch } = await server.supabase - .from("contacts") - .select("id, customer") - .eq("email", sender) - .eq("tenant", tenantId) - .maybeSingle() - - if (contactMatch?.customer_id) return { - customer: contactMatch.customer, - contact: contactMatch.id - } - - // 2️⃣ Kunden laden, bei denen E-Mail oder Rechnungsmail passt - const { data: customers, error } = await server.supabase - .from("customers") - .select("id, infoData") - .eq("tenant", tenantId) - - if (error) { - console.error(`[Helpdesk] Fehler beim Laden der Kunden:`, error.message) - return null - } - - // 3️⃣ Durch Kunden iterieren und prüfen - for (const c of customers || []) { - const info = c.infoData || {} - const email = info.email?.toLowerCase() - const invoiceEmail = info.invoiceEmail?.toLowerCase() - - const emailDomain = extractDomain(email) - const invoiceDomain = extractDomain(invoiceEmail) - - // exakter Match oder Domain-Match - if ( - sender === email || - sender === invoiceEmail || - senderDomain === emailDomain || - senderDomain === invoiceDomain - ) { - return {customer: c.id, contact:null} - } - } - - return null -} +import { findCustomerOrContactByEmailOrDomain } from "../utils/helpers"; +import { eq } from "drizzle-orm"; +import { helpdesk_channel_instances } from "../../db/schema"; const helpdeskInboundRoutes: FastifyPluginAsync = async (server) => { // Öffentliche POST-Route @@ -85,17 +24,18 @@ const helpdeskInboundRoutes: FastifyPluginAsync = async (server) => { } // 1️⃣ Kanalinstanz anhand des Tokens ermitteln - const { data: channel, error: channelError } = await server.supabase - .from('helpdesk_channel_instances') - .select('*') - .eq('public_token', public_token) - .single() + const channels = await server.db + .select() + .from(helpdesk_channel_instances) + .where(eq(helpdesk_channel_instances.publicToken, public_token)) + .limit(1) + const channel = channels[0] - if (channelError || !channel) { + if (!channel) { return res.status(404).send({ error: 'Invalid channel token' }) } - const tenant_id = channel.tenant_id + const tenant_id = channel.tenantId const channel_instance_id = channel.id // @ts-ignore diff --git a/backend/src/routes/helpdesk.ts b/backend/src/routes/helpdesk.ts index 4f9688e..bbf43a6 100644 --- a/backend/src/routes/helpdesk.ts +++ b/backend/src/routes/helpdesk.ts @@ -5,6 +5,13 @@ import { addMessage, getMessages } from '../modules/helpdesk/helpdesk.message.se import { getOrCreateContact } from '../modules/helpdesk/helpdesk.contact.service.js' import {decrypt, encrypt} from "../utils/crypt"; import nodemailer from "nodemailer" +import { eq } from "drizzle-orm"; +import { + helpdesk_channel_instances, + helpdesk_contacts, + helpdesk_conversations, + helpdesk_messages, +} from "../../db/schema"; const helpdeskRoutes: FastifyPluginAsync = async (server) => { // 📩 1. Liste aller Konversationen @@ -58,15 +65,30 @@ const helpdeskRoutes: FastifyPluginAsync = async (server) => { const tenant_id = req.user?.tenant_id const {id: conversation_id} = req.params as {id: string} - const { data, error } = await server.supabase - .from('helpdesk_conversations') - .select('*, helpdesk_contacts(*)') - .eq('tenant_id', tenant_id) - .eq('id', conversation_id) - .single() + const rows = await server.db + .select({ + conversation: helpdesk_conversations, + contact: helpdesk_contacts + }) + .from(helpdesk_conversations) + .leftJoin(helpdesk_contacts, eq(helpdesk_contacts.id, helpdesk_conversations.contactId)) + .where(eq(helpdesk_conversations.id, conversation_id)) - if (error) return res.status(404).send({ error: 'Conversation not found' }) - return res.send(data) + const data = rows[0] + if (!data || data.conversation.tenantId !== tenant_id) return res.status(404).send({ error: 'Conversation not found' }) + + return res.send({ + ...data.conversation, + channel_instance_id: data.conversation.channelInstanceId, + contact_id: data.conversation.contactId, + contact_person_id: data.conversation.contactPersonId, + created_at: data.conversation.createdAt, + customer_id: data.conversation.customerId, + last_message_at: data.conversation.lastMessageAt, + tenant_id: data.conversation.tenantId, + ticket_number: data.conversation.ticketNumber, + helpdesk_contacts: data.contact, + }) }) // 🔄 4. Konversation Status ändern @@ -181,36 +203,39 @@ const helpdeskRoutes: FastifyPluginAsync = async (server) => { safeConfig.smtp.pass = encrypt(safeConfig.smtp.pass) } - // Speichern in Supabase - const { data, error } = await server.supabase - .from("helpdesk_channel_instances") - .insert({ - tenant_id, - type_id, + const inserted = await server.db + .insert(helpdesk_channel_instances) + .values({ + tenantId: tenant_id, + typeId: type_id, name, config: safeConfig, - is_active, + isActive: is_active, }) - .select() - .single() + .returning() - if (error) throw error + const data = inserted[0] + if (!data) throw new Error("Konnte Channel nicht erstellen") + const responseConfig: any = data.config // sensible Felder aus Response entfernen - if (data.config?.imap) { - delete data.config.imap.host - delete data.config.imap.user - delete data.config.imap.pass + if (responseConfig?.imap) { + delete responseConfig.imap.host + delete responseConfig.imap.user + delete responseConfig.imap.pass } - if (data.config?.smtp) { - delete data.config.smtp.host - delete data.config.smtp.user - delete data.config.smtp.pass + if (responseConfig?.smtp) { + delete responseConfig.smtp.host + delete responseConfig.smtp.user + delete responseConfig.smtp.pass } reply.send({ message: "E-Mail-Channel erfolgreich erstellt", - channel: data, + channel: { + ...data, + config: responseConfig + }, }) } catch (err) { console.error("Fehler bei Channel-Erstellung:", err) @@ -234,29 +259,29 @@ const helpdeskRoutes: FastifyPluginAsync = async (server) => { const { text } = req.body as { text: string } // 🔹 Konversation inkl. Channel + Kontakt laden - const { data: conv, error: convErr } = await server.supabase - .from("helpdesk_conversations") - .select(` - id, - tenant_id, - subject, - channel_instance_id, - helpdesk_contacts(email), - helpdesk_channel_instances(config, name), - ticket_number - `) - .eq("id", conversationId) - .single() + const rows = await server.db + .select({ + conversation: helpdesk_conversations, + contact: helpdesk_contacts, + channel: helpdesk_channel_instances, + }) + .from(helpdesk_conversations) + .leftJoin(helpdesk_contacts, eq(helpdesk_contacts.id, helpdesk_conversations.contactId)) + .leftJoin(helpdesk_channel_instances, eq(helpdesk_channel_instances.id, helpdesk_conversations.channelInstanceId)) + .where(eq(helpdesk_conversations.id, conversationId)) + .limit(1) + + const conv = rows[0] console.log(conv) - if (convErr || !conv) { + if (!conv) { reply.status(404).send({ error: "Konversation nicht gefunden" }) return } - const contact = conv.helpdesk_contacts as unknown as {email: string} - const channel = conv.helpdesk_channel_instances as unknown as {name: string} + const contact = conv.contact as unknown as {email: string} + const channel = conv.channel as unknown as {name: string, config: any} console.log(contact) if (!contact?.email) { @@ -288,7 +313,7 @@ const helpdeskRoutes: FastifyPluginAsync = async (server) => { const mailOptions = { from: `"${channel?.name}" <${user}>`, to: contact.email, - subject: `${conv.ticket_number} | ${conv.subject}` || `${conv.ticket_number} | Antwort vom FEDEO Helpdesk`, + subject: `${conv.conversation.ticketNumber} | ${conv.conversation.subject}` || `${conv.conversation.ticketNumber} | Antwort vom FEDEO Helpdesk`, text, } @@ -296,24 +321,22 @@ const helpdeskRoutes: FastifyPluginAsync = async (server) => { console.log(`[Helpdesk SMTP] Gesendet an ${contact.email}: ${info.messageId}`) // 💾 Nachricht speichern - const { error: insertErr } = await server.supabase - .from("helpdesk_messages") - .insert({ - tenant_id: conv.tenant_id, - conversation_id: conversationId, + await server.db + .insert(helpdesk_messages) + .values({ + tenantId: conv.conversation.tenantId, + conversationId: conversationId, direction: "outgoing", payload: { type: "text", text }, - external_message_id: info.messageId, - received_at: new Date().toISOString(), + externalMessageId: info.messageId, + receivedAt: new Date(), }) - if (insertErr) throw insertErr - // 🔁 Konversation aktualisieren - await server.supabase - .from("helpdesk_conversations") - .update({ last_message_at: new Date().toISOString() }) - .eq("id", conversationId) + await server.db + .update(helpdesk_conversations) + .set({ lastMessageAt: new Date() }) + .where(eq(helpdesk_conversations.id, conversationId)) reply.send({ message: "E-Mail erfolgreich gesendet", diff --git a/backend/src/routes/history.ts b/backend/src/routes/history.ts index dca8105..010465b 100644 --- a/backend/src/routes/history.ts +++ b/backend/src/routes/history.ts @@ -1,12 +1,34 @@ // src/routes/resources/history.ts import { FastifyInstance } from "fastify"; +import { and, asc, eq, inArray } from "drizzle-orm"; +import { authProfiles, historyitems } from "../../db/schema"; -const columnMap: Record = { +const columnMap: Record = { + customers: historyitems.customer, + vendors: historyitems.vendor, + projects: historyitems.project, + plants: historyitems.plant, + contacts: historyitems.contact, + tasks: historyitems.task, + vehicles: historyitems.vehicle, + events: historyitems.event, + files: historyitems.file, + products: historyitems.product, + inventoryitems: historyitems.inventoryitem, + inventoryitemgroups: historyitems.inventoryitemgroup, + checks: historyitems.check, + costcentres: historyitems.costcentre, + ownaccounts: historyitems.ownaccount, + documentboxes: historyitems.documentbox, + hourrates: historyitems.hourrate, + services: historyitems.service, +}; + +const insertFieldMap: Record = { customers: "customer", vendors: "vendor", projects: "project", plants: "plant", - contracts: "contract", contacts: "contact", tasks: "task", vehicles: "vehicle", @@ -15,15 +37,18 @@ const columnMap: Record = { products: "product", inventoryitems: "inventoryitem", inventoryitemgroups: "inventoryitemgroup", - absencerequests: "absencerequest", checks: "check", costcentres: "costcentre", ownaccounts: "ownaccount", documentboxes: "documentbox", hourrates: "hourrate", services: "service", - roles: "role", -}; +} + +const parseId = (value: string) => { + if (/^\d+$/.test(value)) return Number(value) + return value +} export default async function resourceHistoryRoutes(server: FastifyInstance) { server.get<{ @@ -49,29 +74,36 @@ export default async function resourceHistoryRoutes(server: FastifyInstance) { return reply.code(400).send({ error: `History not supported for resource '${resource}'` }); } - const { data, error } = await server.supabase - .from("historyitems") - .select("*") - .eq(column, id) - .order("created_at", { ascending: true }); + const data = await server.db + .select() + .from(historyitems) + .where(eq(column, parseId(id))) + .orderBy(asc(historyitems.createdAt)); - if (error) { - server.log.error(error); - return reply.code(500).send({ error: "Failed to fetch history" }); - } + const userIds = Array.from( + new Set(data.map((item) => item.createdBy).filter(Boolean)) + ) as string[] - const {data:users, error:usersError} = await server.supabase - .from("auth_users") - .select("*, auth_profiles(*), tenants!auth_tenant_users(*)") + const profiles = userIds.length > 0 + ? await server.db + .select() + .from(authProfiles) + .where(and( + eq(authProfiles.tenant_id, req.user?.tenant_id), + inArray(authProfiles.user_id, userIds) + )) + : [] - const filteredUsers = (users ||[]).filter(i => i.tenants.find((t:any) => t.id === req.user?.tenant_id)) + const profileByUserId = new Map( + profiles.map((profile) => [profile.user_id, profile]) + ) - const dataCombined = data.map(historyitem => { - return { - ...historyitem, - created_by_profile: filteredUsers.find(i => i.id === historyitem.created_by) ? filteredUsers.find(i => i.id === historyitem.created_by).auth_profiles[0] : null - } - }) + const dataCombined = data.map((historyitem) => ({ + ...historyitem, + created_at: historyitem.createdAt, + created_by: historyitem.createdBy, + created_by_profile: historyitem.createdBy ? profileByUserId.get(historyitem.createdBy) || null : null, + })) @@ -128,29 +160,33 @@ export default async function resourceHistoryRoutes(server: FastifyInstance) { const userId = (req.user as any)?.user_id; - const fkField = columnMap[resource]; + const fkField = insertFieldMap[resource]; if (!fkField) { return reply.code(400).send({ error: `Unknown resource: ${resource}` }); } - const { data, error } = await server.supabase - .from("historyitems") - .insert({ + const inserted = await server.db + .insert(historyitems) + .values({ text, - [fkField]: id, + [fkField]: parseId(id), oldVal: old_val || null, newVal: new_val || null, config: config || null, tenant: (req.user as any)?.tenant_id, - created_by: userId + createdBy: userId }) - .select() - .single(); + .returning() - if (error) { - return reply.code(500).send({ error: error.message }); + const data = inserted[0] + if (!data) { + return reply.code(500).send({ error: "Failed to create history entry" }); } - return reply.code(201).send(data); + return reply.code(201).send({ + ...data, + created_at: data.createdAt, + created_by: data.createdBy + }); }); } diff --git a/backend/src/routes/notifications.ts b/backend/src/routes/notifications.ts index 395a6c5..6e9802d 100644 --- a/backend/src/routes/notifications.ts +++ b/backend/src/routes/notifications.ts @@ -1,21 +1,22 @@ // routes/notifications.routes.ts import { FastifyInstance } from 'fastify'; import { NotificationService, UserDirectory } from '../modules/notification.service'; +import { eq } from "drizzle-orm"; +import { authUsers } from "../../db/schema"; // Beispiel: E-Mail aus eigener User-Tabelle laden const getUserDirectory: UserDirectory = async (server:FastifyInstance, userId, tenantId) => { - const { data, error } = await server.supabase - .from('auth_users') - .select('email') - .eq('id', userId) - .maybeSingle(); - if (error || !data) return null; + const rows = await server.db + .select({ email: authUsers.email }) + .from(authUsers) + .where(eq(authUsers.id, userId)) + .limit(1) + const data = rows[0] + if (!data) return null; return { email: data.email }; }; export default async function notificationsRoutes(server: FastifyInstance) { - // wichtig: server.supabase ist über app verfügbar - const svc = new NotificationService(server, getUserDirectory); server.post('/notifications/trigger', async (req, reply) => { diff --git a/backend/src/routes/resourcesSpecial.ts b/backend/src/routes/resourcesSpecial.ts index fc29890..c191615 100644 --- a/backend/src/routes/resourcesSpecial.ts +++ b/backend/src/routes/resourcesSpecial.ts @@ -35,7 +35,7 @@ export default async function resourceRoutesSpecial(server: FastifyInstance) { } // --------------------------------------- - // 📌 SELECT: wir ignorieren select string (wie Supabase) + // 📌 SELECT: select-string wird in dieser Route bewusst ignoriert // Drizzle kann kein dynamisches Select aus String! // Wir geben IMMER alle Spalten zurück → kompatibel zum Frontend // --------------------------------------- diff --git a/backend/src/routes/staff/timeconnects.ts b/backend/src/routes/staff/timeconnects.ts index 288e341..db89df1 100644 --- a/backend/src/routes/staff/timeconnects.ts +++ b/backend/src/routes/staff/timeconnects.ts @@ -1,5 +1,7 @@ import { FastifyInstance } from 'fastify' import { StaffTimeEntryConnect } from '../../types/staff' +import { asc, eq } from "drizzle-orm"; +import { stafftimenetryconnects } from "../../../db/schema"; export default async function staffTimeConnectRoutes(server: FastifyInstance) { @@ -8,16 +10,21 @@ export default async function staffTimeConnectRoutes(server: FastifyInstance) { '/staff/time/:id/connects', async (req, reply) => { const { id } = req.params - const { started_at, stopped_at, project_id, customer_id, task_id, ticket_id, notes } = req.body + const { started_at, stopped_at, project_id, notes } = req.body + const parsedProjectId = project_id ? Number(project_id) : null - const { data, error } = await server.supabase - .from('staff_time_entry_connects') - .insert([{ time_entry_id: id, started_at, stopped_at, project_id, customer_id, task_id, ticket_id, notes }]) - .select() - .maybeSingle() + const data = await server.db + .insert(stafftimenetryconnects) + .values({ + stafftimeentry: id, + started_at: new Date(started_at), + stopped_at: new Date(stopped_at), + project_id: parsedProjectId, + notes + }) + .returning() - if (error) return reply.code(400).send({ error: error.message }) - return reply.send(data) + return reply.send(data[0]) } ) @@ -26,13 +33,12 @@ export default async function staffTimeConnectRoutes(server: FastifyInstance) { '/staff/time/:id/connects', async (req, reply) => { const { id } = req.params - const { data, error } = await server.supabase - .from('staff_time_entry_connects') - .select('*') - .eq('time_entry_id', id) - .order('started_at', { ascending: true }) + const data = await server.db + .select() + .from(stafftimenetryconnects) + .where(eq(stafftimenetryconnects.stafftimeentry, id)) + .orderBy(asc(stafftimenetryconnects.started_at)) - if (error) return reply.code(400).send({ error: error.message }) return reply.send(data) } ) @@ -42,15 +48,20 @@ export default async function staffTimeConnectRoutes(server: FastifyInstance) { '/staff/time/connects/:connectId', async (req, reply) => { const { connectId } = req.params - const { data, error } = await server.supabase - .from('staff_time_entry_connects') - .update({ ...req.body, updated_at: new Date().toISOString() }) - .eq('id', connectId) - .select() - .maybeSingle() + const patchData = { ...req.body } as any + if (patchData.started_at) patchData.started_at = new Date(patchData.started_at) + if (patchData.stopped_at) patchData.stopped_at = new Date(patchData.stopped_at) + if (patchData.project_id !== undefined) { + patchData.project_id = patchData.project_id ? Number(patchData.project_id) : null + } - if (error) return reply.code(400).send({ error: error.message }) - return reply.send(data) + const data = await server.db + .update(stafftimenetryconnects) + .set({ ...patchData, updated_at: new Date() }) + .where(eq(stafftimenetryconnects.id, connectId)) + .returning() + + return reply.send(data[0]) } ) @@ -59,12 +70,10 @@ export default async function staffTimeConnectRoutes(server: FastifyInstance) { '/staff/time/connects/:connectId', async (req, reply) => { const { connectId } = req.params - const { error } = await server.supabase - .from('staff_time_entry_connects') - .delete() - .eq('id', connectId) + await server.db + .delete(stafftimenetryconnects) + .where(eq(stafftimenetryconnects.id, connectId)) - if (error) return reply.code(400).send({ error: error.message }) return reply.send({ success: true }) } ) diff --git a/backend/src/utils/export/sepa.ts b/backend/src/utils/export/sepa.ts index d13b127..ba37697 100644 --- a/backend/src/utils/export/sepa.ts +++ b/backend/src/utils/export/sepa.ts @@ -1,12 +1,25 @@ import xmlbuilder from "xmlbuilder"; import {randomUUID} from "node:crypto"; import dayjs from "dayjs"; +import { and, eq, inArray } from "drizzle-orm"; +import { createddocuments, tenants } from "../../../db/schema"; export const createSEPAExport = async (server,idsToExport, tenant_id) => { - const {data,error} = await server.supabase.from("createddocuments").select().eq("tenant", tenant_id).in("id", idsToExport) - const {data:tenantData,error:tenantError} = await server.supabase.from("tenants").select().eq("id", tenant_id).single() + const data = await server.db + .select() + .from(createddocuments) + .where(and( + eq(createddocuments.tenant, tenant_id), + inArray(createddocuments.id, idsToExport) + )) + + const tenantRows = await server.db + .select() + .from(tenants) + .where(eq(tenants.id, tenant_id)) + .limit(1) + const tenantData = tenantRows[0] console.log(tenantData) - console.log(tenantError) console.log(data) @@ -111,4 +124,4 @@ export const createSEPAExport = async (server,idsToExport, tenant_id) => { console.log(doc.end({pretty:true})) -} \ No newline at end of file +} diff --git a/backend/src/utils/history.ts b/backend/src/utils/history.ts index 477c666..afef68e 100644 --- a/backend/src/utils/history.ts +++ b/backend/src/utils/history.ts @@ -1,4 +1,5 @@ import { FastifyInstance } from "fastify" +import { historyitems } from "../../db/schema"; export async function insertHistoryItem( server: FastifyInstance, @@ -63,8 +64,5 @@ export async function insertHistoryItem( newVal: params.newVal ? JSON.stringify(params.newVal) : null } - const { error } = await server.supabase.from("historyitems").insert([entry]) - if (error) { // @ts-ignore - console.log(error) - } + await server.db.insert(historyitems).values(entry as any) }