import { Pool } from 'pg' import refreshTokenHelper from "../../utils/refreshTokenHelper" // Average fulfillment handling time (a.k.a. "order-to-ship" / "click-to-ship" // time) for customer outbound shipments: the span from order import // (c_order.created) until the package leaves (m_inout.shipping_date), counting // ONLY business time — Monday–Friday, 09:00–16:00. Weekends and out-of-hours // time are excluded. Limited to shipments shipped in the last 10 days so the // query stays bounded. // // Business time is summed per shipment by walking each calendar day in the // [created, shipped] span and, for weekdays only, measuring the overlap of the // active interval with that day's 09:00–16:00 opening window. const handleFunc = async (event: any) => { const config = useRuntimeConfig() const dbConfig = { host: config.pgHost, port: parseInt(config.pgPort || '5432'), database: config.pgDatabase, user: config.pgUser, password: config.pgPassword, } if (!dbConfig.host || !dbConfig.database || !dbConfig.user || !dbConfig.password) { throw createError({ statusCode: 500, message: 'PostgreSQL credentials not configured. Please set PG_HOST, PG_DATABASE, PG_USER, PG_PASSWORD in .env' }) } // Limited roles (FrontendMenu == 'c') only see their own organization; admins // see all orgs (NULL filter is a no-op in the SQL below). const roleCookie = getCookie(event, 'logship_role') let menuType: string = 'Menu' try { const role = roleCookie ? JSON.parse(roleCookie) : null menuType = role?.FrontendMenu?.id || role?.FrontendMenu || 'Menu' } catch {} const orgIdCookie = getCookie(event, 'logship_organization_id') const orgIdNum = orgIdCookie ? parseInt(orgIdCookie) : NaN const orgFilter: number | null = (menuType === 'c' && Number.isFinite(orgIdNum)) ? orgIdNum : null const pool = new Pool(dbConfig) try { const sql = ` WITH shp AS ( SELECT ino.m_inout_id, ord.created AS order_created, ino.shipping_date AS ship_date FROM m_inout ino JOIN c_order ord ON ord.c_order_id = ino.c_order_id WHERE ino.issotrx = 'Y' AND ino.isactive = 'Y' AND ino.docstatus = 'CO' AND ino.c_doctype_id = 1000011 AND ino.movementtype = 'C-' AND ino.ad_client_id = 1000000 AND ($1::int IS NULL OR ino.ad_org_id = $1) AND ino.shipping_date IS NOT NULL AND ord.created IS NOT NULL AND ino.shipping_date >= ord.created AND ino.shipping_date >= (CURRENT_DATE - 9) ), per_shipment AS ( SELECT s.m_inout_id, SUM( GREATEST(0, EXTRACT(EPOCH FROM ( LEAST(d.day + interval '16 hour', s.ship_date) - GREATEST(d.day + interval '9 hour', s.order_created) ))) ) AS business_seconds FROM shp s CROSS JOIN LATERAL generate_series(s.order_created::date, s.ship_date::date, interval '1 day') AS d(day) WHERE EXTRACT(DOW FROM d.day) BETWEEN 1 AND 5 -- Mon..Fri only GROUP BY s.m_inout_id ) SELECT COUNT(*) AS shipment_count, AVG(business_seconds) AS avg_business_seconds FROM per_shipment ` const result = await pool.query(sql, [orgFilter]) const row: any = result.rows[0] || {} const avgSeconds = row.avg_business_seconds ? Number(row.avg_business_seconds) : 0 return { shipmentCount: parseInt(row.shipment_count || 0), avgMinutes: Math.round(avgSeconds / 60), windowDays: 10 } } catch (err: any) { console.error('PostgreSQL query error:', err) throw createError({ statusCode: 500, message: `Database error: ${err.message}` }) } finally { await pool.end() } } export default defineEventHandler(async (event) => { try { return await handleFunc(event) } catch(err: any) { try { await refreshTokenHelper(event) return await handleFunc(event) } catch(error: any) { console.error('Fatal error in fulfillment-speed:', error) throw error } } })