mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Add ACMCC app source, Supabase backend, and project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.45.0'
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
}
|
||||
|
||||
const SOURCE_TYPE_LABELS: Record<string, string> = {
|
||||
public_form: 'Public form submission',
|
||||
client_request: 'Client request',
|
||||
homeowner_ticket: 'Homeowner ticket',
|
||||
violation_response: 'Violation response',
|
||||
registration_request: 'Registration request',
|
||||
}
|
||||
|
||||
const SOURCE_TYPE_LINKS: Record<string, string> = {
|
||||
public_form: '/dashboard/form-inbox',
|
||||
client_request: '/dashboard/client-requests',
|
||||
homeowner_ticket: '/dashboard/form-inbox',
|
||||
violation_response: '/dashboard/form-inbox',
|
||||
registration_request: '/dashboard/form-inbox',
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
|
||||
|
||||
try {
|
||||
const { inbox_id } = await req.json()
|
||||
if (!inbox_id) {
|
||||
return new Response(JSON.stringify({ error: 'inbox_id required' }), {
|
||||
status: 400,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
|
||||
)
|
||||
|
||||
// Fetch the inbox entry
|
||||
const { data: inbox, error: inboxError } = await supabase
|
||||
.from('form_inbox')
|
||||
.select('*')
|
||||
.eq('id', inbox_id)
|
||||
.single()
|
||||
|
||||
if (inboxError || !inbox) {
|
||||
console.error('Inbox lookup failed:', inboxError)
|
||||
return new Response(JSON.stringify({ error: 'inbox entry not found' }), {
|
||||
status: 404,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const sourceLabel = SOURCE_TYPE_LABELS[inbox.source_type] || 'Form submission'
|
||||
const link = SOURCE_TYPE_LINKS[inbox.source_type] || '/dashboard/form-inbox'
|
||||
|
||||
// Get admin + manager user IDs
|
||||
const { data: roleRows, error: roleError } = await supabase
|
||||
.from('user_roles')
|
||||
.select('user_id')
|
||||
.in('role', ['admin', 'manager'])
|
||||
|
||||
if (roleError) {
|
||||
console.error('Role lookup failed:', roleError)
|
||||
return new Response(JSON.stringify({ error: 'failed to fetch staff' }), {
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const userIds = Array.from(new Set((roleRows || []).map((r) => r.user_id).filter(Boolean)))
|
||||
if (userIds.length === 0) {
|
||||
return new Response(JSON.stringify({ ok: true, notified: 0 }), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
// Insert in-app notifications for every staff user
|
||||
const notificationRows = userIds.map((uid) => ({
|
||||
user_id: uid,
|
||||
type: 'form_submission_received',
|
||||
title: `New ${sourceLabel.toLowerCase()}`,
|
||||
message: `${inbox.submitter_name || 'Someone'} submitted: ${inbox.title}`,
|
||||
related_item_id: inbox.id,
|
||||
related_item_type: 'form_inbox',
|
||||
link,
|
||||
}))
|
||||
const { error: notifError } = await supabase
|
||||
.from('in_app_notifications')
|
||||
.insert(notificationRows)
|
||||
if (notifError) console.error('Notification insert error:', notifError)
|
||||
|
||||
// Look up staff emails from auth.users
|
||||
const { data: authData, error: authError } = await supabase.auth.admin.listUsers({
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
})
|
||||
if (authError) {
|
||||
console.error('Auth list failed:', authError)
|
||||
}
|
||||
|
||||
const staffEmails = (authData?.users || [])
|
||||
.filter((u) => userIds.includes(u.id) && u.email)
|
||||
.map((u) => ({ id: u.id, email: u.email as string }))
|
||||
|
||||
const baseUrl = Deno.env.get('SUPABASE_URL') || ''
|
||||
const projectId = baseUrl.replace('https://', '').split('.')[0]
|
||||
const fullLink = `https://avria.cloud${link}`
|
||||
|
||||
let emailsSent = 0
|
||||
let emailsFailed = 0
|
||||
|
||||
for (const staff of staffEmails) {
|
||||
try {
|
||||
const { error: emailError } = await supabase.functions.invoke(
|
||||
'send-transactional-email',
|
||||
{
|
||||
body: {
|
||||
templateName: 'ticket-submitted',
|
||||
recipientEmail: staff.email,
|
||||
idempotencyKey: `form-inbox-${inbox.id}-${staff.id}`,
|
||||
templateData: {
|
||||
recipientName: '',
|
||||
homeownerName: inbox.submitter_name || 'Anonymous',
|
||||
ticketTitle: inbox.title,
|
||||
category: sourceLabel,
|
||||
priority: '',
|
||||
summary: inbox.summary || '',
|
||||
link: fullLink,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
if (emailError) {
|
||||
emailsFailed++
|
||||
console.error(`Email to ${staff.email} failed:`, emailError)
|
||||
} else {
|
||||
emailsSent++
|
||||
}
|
||||
} catch (e) {
|
||||
emailsFailed++
|
||||
console.error(`Email exception for ${staff.email}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
notified: userIds.length,
|
||||
emails_sent: emailsSent,
|
||||
emails_failed: emailsFailed,
|
||||
}),
|
||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } },
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('notify-staff-new-form error:', e)
|
||||
return new Response(JSON.stringify({ error: String(e) }), {
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user