DispatchPage.vue: 1320→1217 lines - Extract SbModal.vue + SbContextMenu.vue reusable components - Extract useAbsenceResize composable - Extract dispatch constants to config/dispatch.js ProjectWizard.vue: 1185→673 lines (-43%) - Extract useWizardPublish composable (270-line publish function) - Extract useWizardCatalog composable - Extract wizard-constants.js (step labels, options, categories) SettingsPage.vue: 1172→850 lines (-27%) - Extract usePermissionMatrix composable - Extract useUserGroups composable - Extract useLegacySync composable ClientDetailPage.vue: 1169→864 lines (-26%) - Extract useClientData composable (loadCustomer broken into sub-functions) - Extract useEquipmentActions composable - Extract client-constants.js + erp-pdf.js utility checkout.js: 639→408 lines (-36%) - Extract address-search.js module - Extract otp.js module - Extract email-templates.js module - Extract project-templates.js module - Add erpQuery() helper to DRY repeated URL construction Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
101 lines
3.8 KiB
JavaScript
101 lines
3.8 KiB
JavaScript
'use strict'
|
|
const { log, erpFetch } = require('./helpers')
|
|
const { otpEmailHtml } = require('./email-templates')
|
|
|
|
const otpStore = new Map()
|
|
|
|
function generateOTP () {
|
|
return String(Math.floor(100000 + Math.random() * 900000))
|
|
}
|
|
|
|
function erpQuery (doctype, filters, fields, limit) {
|
|
let url = `/api/resource/${encodeURIComponent(doctype)}?filters=${encodeURIComponent(JSON.stringify(filters))}&fields=${encodeURIComponent(JSON.stringify(fields))}`
|
|
if (limit) url += `&limit_page_length=${limit}`
|
|
return erpFetch(url)
|
|
}
|
|
|
|
async function sendOTP (identifier) {
|
|
const isEmail = identifier.includes('@')
|
|
const code = generateOTP()
|
|
const expires = Date.now() + 10 * 60 * 1000
|
|
|
|
let customer = null
|
|
if (isEmail) {
|
|
const res = await erpQuery('Customer', [['email_id', '=', identifier]], ['name', 'customer_name', 'cell_phone', 'email_id'], 1)
|
|
if (res.status === 200 && res.data?.data?.length) customer = res.data.data[0]
|
|
} else {
|
|
const { lookupCustomerByPhone } = require('./helpers')
|
|
customer = await lookupCustomerByPhone(identifier)
|
|
}
|
|
|
|
if (!customer) return { found: false }
|
|
|
|
otpStore.set(identifier, { code, expires, customerId: customer.name, customerName: customer.customer_name })
|
|
|
|
if (isEmail) {
|
|
const { sendEmail } = require('./email')
|
|
await sendEmail({
|
|
to: identifier,
|
|
subject: 'Code de vérification Gigafibre',
|
|
html: otpEmailHtml(code),
|
|
})
|
|
} else {
|
|
try {
|
|
const { sendSmsInternal } = require('./twilio')
|
|
await sendSmsInternal(identifier, `Gigafibre — Votre code de vérification : ${code}\nExpire dans 10 min.`)
|
|
} catch (e) { log('OTP SMS failed:', e.message) }
|
|
}
|
|
|
|
log(`OTP sent to ${identifier} for customer ${customer.name}`)
|
|
return { found: true, sent: true, channel: isEmail ? 'email' : 'sms' }
|
|
}
|
|
|
|
async function verifyOTP (identifier, code) {
|
|
const entry = otpStore.get(identifier)
|
|
if (!entry) return { valid: false, reason: 'no_otp' }
|
|
if (Date.now() > entry.expires) { otpStore.delete(identifier); return { valid: false, reason: 'expired' } }
|
|
if (entry.code !== code) return { valid: false, reason: 'wrong_code' }
|
|
otpStore.delete(identifier)
|
|
|
|
const result = { valid: true, customer_id: entry.customerId, customer_name: entry.customerName }
|
|
try {
|
|
const custRes = await erpFetch(`/api/resource/Customer/${encodeURIComponent(entry.customerId)}?fields=${encodeURIComponent(JSON.stringify(['name', 'customer_name', 'cell_phone', 'email_id', 'tel_home']))}`)
|
|
if (custRes.status === 200 && custRes.data?.data) {
|
|
const c = custRes.data.data
|
|
result.phone = c.cell_phone || c.tel_home || ''
|
|
result.email = c.email_id || ''
|
|
}
|
|
|
|
if (!result.email) {
|
|
try {
|
|
const contRes = await erpQuery('Contact',
|
|
[['Dynamic Link', 'link_doctype', '=', 'Customer'], ['Dynamic Link', 'link_name', '=', entry.customerId]],
|
|
['email_id'], 1)
|
|
if (contRes.status === 200 && contRes.data?.data?.[0]?.email_id) {
|
|
result.email = contRes.data.data[0].email_id
|
|
}
|
|
} catch (e) { log('OTP - Contact email fallback error:', e.message) }
|
|
}
|
|
|
|
const locRes = await erpQuery('Service Location',
|
|
[['customer', '=', entry.customerId]],
|
|
['name', 'address_line', 'city', 'postal_code', 'location_name', 'latitude', 'longitude'], 20)
|
|
if (locRes.status === 200 && locRes.data?.data?.length) {
|
|
result.addresses = locRes.data.data.map(l => ({
|
|
name: l.name,
|
|
address: l.address_line || l.location_name || l.name,
|
|
city: l.city || '',
|
|
postal_code: l.postal_code || '',
|
|
latitude: l.latitude || null,
|
|
longitude: l.longitude || null,
|
|
}))
|
|
}
|
|
} catch (e) {
|
|
log('OTP verify - customer details fetch error:', e.message)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
module.exports = { otpStore, generateOTP, sendOTP, verifyOTP }
|