import { authFetch } from './auth' import { BASE_URL } from 'src/config/erpnext' async function apiGet (path) { const res = await authFetch(BASE_URL + path) if (!res.ok) throw new Error(`API ${res.status}: ${path}`) const ct = (res.headers.get('content-type') || '') if (!ct.includes('application/json')) throw new Error('Not JSON response for: ' + path) const data = await res.json() if (data.exc) throw new Error(data.exc) return data } /** * Get current portal user info from Authentik headers. * Returns { email, customer_id, customer_name } */ export async function getPortalUser () { const data = await apiGet('/api/method/client_portal_get_user_info') return data.message } /** * Fetch paginated Sales Invoices for a customer. */ export async function fetchInvoices (customer, { page = 1, pageSize = 20 } = {}) { const start = (page - 1) * pageSize const filters = JSON.stringify([ ['customer', '=', customer], ['docstatus', '=', 1], ]) const fields = JSON.stringify([ 'name', 'posting_date', 'due_date', 'grand_total', 'outstanding_amount', 'status', 'currency', ]) const path = `/api/resource/Sales Invoice?filters=${encodeURIComponent(filters)}&fields=${encodeURIComponent(fields)}&order_by=posting_date desc&limit_page_length=${pageSize}&limit_start=${start}` const data = await apiGet(path) return data.data || [] } /** * Count total invoices for pagination. */ export async function countInvoices (customer) { const filters = JSON.stringify([ ['customer', '=', customer], ['docstatus', '=', 1], ]) const data = await apiGet(`/api/method/frappe.client.get_count?doctype=Sales Invoice&filters=${encodeURIComponent(filters)}`) return data.message || 0 } /** * Download invoice PDF. */ export async function fetchInvoicePDF (invoiceName, format = 'Facture TARGO') { const url = `${BASE_URL}/api/method/frappe.utils.print_format.download_pdf?doctype=Sales%20Invoice&name=${encodeURIComponent(invoiceName)}&format=${encodeURIComponent(format)}` const res = await authFetch(url) if (!res.ok) throw new Error('PDF download failed') return res.blob() } /** * Fetch customer's Issues/Tickets. */ export async function fetchTickets (customer) { const filters = JSON.stringify([['customer', '=', customer]]) const fields = JSON.stringify([ 'name', 'subject', 'status', 'priority', 'creation', 'issue_type', 'sla_resolution_date', ]) const path = `/api/resource/Issue?filters=${encodeURIComponent(filters)}&fields=${encodeURIComponent(fields)}&order_by=creation desc&limit_page_length=50` const data = await apiGet(path) return data.data || [] } /** * Create a new support ticket. */ export async function createTicket (customer, subject, description) { const res = await authFetch(BASE_URL + '/api/resource/Issue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ customer, subject, description, issue_type: 'Support', priority: 'Medium', }), }) if (!res.ok) throw new Error('Failed to create ticket') const data = await res.json() return data.data } /** * Fetch customer profile with addresses. */ export async function fetchProfile (customer) { const data = await apiGet(`/api/resource/Customer/${encodeURIComponent(customer)}`) return data.data } /** * Fetch addresses linked to customer. */ export async function fetchAddresses (customer) { const filters = JSON.stringify([ ['Dynamic Link', 'link_doctype', '=', 'Customer'], ['Dynamic Link', 'link_name', '=', customer], ]) const fields = JSON.stringify([ 'name', 'address_title', 'address_line1', 'address_line2', 'city', 'state', 'pincode', 'country', 'is_primary_address', ]) const path = `/api/resource/Address?filters=${encodeURIComponent(filters)}&fields=${encodeURIComponent(fields)}` const data = await apiGet(path) return data.data || [] } /** * Fetch Service Locations with their active Service Subscriptions. * Returns locations grouped with subscriptions and monthly totals. */ export async function fetchServiceLocations (customer) { // Fetch locations const locFields = JSON.stringify([ 'name', 'location_name', 'address_line', 'city', 'postal_code', 'province', 'status', 'connection_type', ]) const locFilters = JSON.stringify([['customer', '=', customer]]) const locPath = `/api/resource/Service Location?filters=${encodeURIComponent(locFilters)}&fields=${encodeURIComponent(locFields)}&order_by=creation asc&limit_page_length=50` const locData = await apiGet(locPath) const locations = locData.data || [] // Fetch subscriptions const subFields = JSON.stringify([ 'name', 'service_location', 'status', 'service_category', 'plan_name', 'monthly_price', 'speed_down', 'speed_up', 'billing_cycle', 'start_date', 'end_date', 'promo_end', ]) const subFilters = JSON.stringify([ ['customer', '=', customer], ['status', '=', 'Actif'], ]) const subPath = `/api/resource/Service Subscription?filters=${encodeURIComponent(subFilters)}&fields=${encodeURIComponent(subFields)}&order_by=service_category asc, monthly_price desc&limit_page_length=200` const subData = await apiGet(subPath) const subscriptions = subData.data || [] // Group subscriptions by location const subsByLoc = {} for (const sub of subscriptions) { const loc = sub.service_location || '_unassigned' if (!subsByLoc[loc]) subsByLoc[loc] = [] subsByLoc[loc].push(sub) } // Attach subscriptions to locations and compute totals for (const loc of locations) { loc.subscriptions = subsByLoc[loc.name] || [] loc.monthly_total = loc.subscriptions.reduce((sum, s) => sum + (s.monthly_price || 0), 0) } // Grand total across all locations const grandTotal = locations.reduce((sum, loc) => sum + loc.monthly_total, 0) return { locations, grandTotal, subscriptionCount: subscriptions.length } } /** * Fetch a single Sales Invoice with line items. */ export async function fetchInvoice (invoiceName) { const data = await apiGet(`/api/resource/Sales Invoice/${encodeURIComponent(invoiceName)}`) return data.data } /** * Fetch invoice HTML print preview (Jinja rendered). */ export async function fetchInvoiceHTML (invoiceName, format = 'Facture TARGO') { const url = `${BASE_URL}/api/method/frappe.www.printview.get_html_and_style?doc=Sales Invoice&name=${encodeURIComponent(invoiceName)}&print_format=${encodeURIComponent(format)}&no_letterhead=0` const res = await authFetch(url) if (!res.ok) throw new Error('Print preview failed') const data = await res.json() return data.message || {} } /** * Fetch a single Issue with full details. */ export async function fetchTicket (ticketName) { const data = await apiGet(`/api/resource/Issue/${encodeURIComponent(ticketName)}`) return data.data } /** * Fetch Communications linked to a reference (Issue or Customer). */ export async function fetchCommunications (refDoctype, refName, limit = 100) { const filters = JSON.stringify([ ['reference_doctype', '=', refDoctype], ['reference_name', '=', refName], ]) const fields = JSON.stringify([ 'name', 'communication_medium', 'communication_type', 'sent_or_received', 'sender', 'sender_full_name', 'phone_no', 'subject', 'content', 'creation', 'status', 'reference_doctype', 'reference_name', ]) const path = `/api/resource/Communication?filters=${encodeURIComponent(filters)}&fields=${encodeURIComponent(fields)}&order_by=creation asc&limit_page_length=${limit}` const data = await apiGet(path) return data.data || [] } /** * Fetch all Communications for a customer (across all references + direct). * Returns Communications where reference is Customer or Issue belonging to customer. */ export async function fetchAllCommunications (customer, limit = 200) { // Direct communications on customer const directFilters = JSON.stringify([ ['reference_doctype', '=', 'Customer'], ['reference_name', '=', customer], ]) const fields = JSON.stringify([ 'name', 'communication_medium', 'communication_type', 'sent_or_received', 'sender', 'sender_full_name', 'phone_no', 'subject', 'content', 'creation', 'status', 'reference_doctype', 'reference_name', ]) const directPath = `/api/resource/Communication?filters=${encodeURIComponent(directFilters)}&fields=${encodeURIComponent(fields)}&order_by=creation desc&limit_page_length=${limit}` const directData = await apiGet(directPath) // Communications on customer's Issues const issueFilters = JSON.stringify([ ['reference_doctype', '=', 'Issue'], ['reference_name', 'in', []], // will be filled after ticket fetch ]) // First get the customer's ticket names const ticketFilters = JSON.stringify([['customer', '=', customer]]) const ticketFields = JSON.stringify(['name']) const ticketPath = `/api/resource/Issue?filters=${encodeURIComponent(ticketFilters)}&fields=${encodeURIComponent(ticketFields)}&limit_page_length=200` const ticketData = await apiGet(ticketPath) const ticketNames = (ticketData.data || []).map(t => t.name) let issueComs = [] if (ticketNames.length) { const issueComsFilters = JSON.stringify([ ['reference_doctype', '=', 'Issue'], ['reference_name', 'in', ticketNames], ]) const issueComsPath = `/api/resource/Communication?filters=${encodeURIComponent(issueComsFilters)}&fields=${encodeURIComponent(fields)}&order_by=creation desc&limit_page_length=${limit}` const issueComsData = await apiGet(issueComsPath) issueComs = issueComsData.data || [] } // Merge, deduplicate, sort desc const all = [...(directData.data || []), ...issueComs] const seen = new Set() const unique = all.filter(c => { if (seen.has(c.name)) return false seen.add(c.name) return true }) unique.sort((a, b) => new Date(b.creation) - new Date(a.creation)) return unique } /** * Post a reply on a ticket (creates a Communication). */ export async function replyToTicket (ticketName, message) { const res = await authFetch(BASE_URL + '/api/resource/Communication', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ communication_type: 'Communication', communication_medium: 'Other', sent_or_received: 'Sent', sender: 'portal@gigafibre.ca', content: message, reference_doctype: 'Issue', reference_name: ticketName, status: 'Linked', }), }) if (!res.ok) throw new Error('Failed to send reply') const data = await res.json() return data.data } /** * Fetch Comments linked to a document (visible to portal users). */ export async function fetchComments (refDoctype, refName) { const filters = JSON.stringify([ ['reference_doctype', '=', refDoctype], ['reference_name', '=', refName], ['comment_type', '=', 'Comment'], ]) const fields = JSON.stringify([ 'name', 'comment_by', 'content', 'creation', ]) const path = `/api/resource/Comment?filters=${encodeURIComponent(filters)}&fields=${encodeURIComponent(fields)}&order_by=creation asc&limit_page_length=100` const data = await apiGet(path) return data.data || [] }