feat: multi-destinataires email par societe
This commit is contained in:
@@ -46,14 +46,18 @@ CREATE TABLE IF NOT EXISTS app_settings (
|
||||
-- Entités vers lesquelles envoyer les factures à rembourser
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS companies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL, -- destinataire de l'email de remboursement
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL, -- destinataire principal
|
||||
extra_emails TEXT[] NOT NULL DEFAULT '{}', -- destinataires supplémentaires
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Migration idempotente : ajoute extra_emails si la colonne n'existe pas encore
|
||||
ALTER TABLE companies ADD COLUMN IF NOT EXISTS extra_emails TEXT[] NOT NULL DEFAULT '{}';
|
||||
|
||||
-- =============================================================
|
||||
-- CATÉGORIES DE DÉPENSES
|
||||
-- =============================================================
|
||||
|
||||
@@ -11,35 +11,40 @@ const wrap = (fn: (req: AuthRequest, res: Response, next: NextFunction) => Promi
|
||||
(req: AuthRequest, res: Response, next: NextFunction) => fn(req, res, next).catch(next);
|
||||
|
||||
const companySchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(255),
|
||||
email: z.string().email('Email principal invalide'),
|
||||
extra_emails: z
|
||||
.array(z.string().email('Adresse email supplémentaire invalide'))
|
||||
.default([]),
|
||||
});
|
||||
|
||||
/** GET /api/companies — Liste toutes les sociétés actives */
|
||||
router.get('/', wrap(async (_req: AuthRequest, res: Response): Promise<void> => {
|
||||
const result = await db.query(
|
||||
'SELECT * FROM companies WHERE is_active = TRUE ORDER BY name'
|
||||
'SELECT id, name, email, extra_emails, is_active FROM companies WHERE is_active = TRUE ORDER BY name'
|
||||
);
|
||||
res.json(result.rows);
|
||||
}));
|
||||
|
||||
/** POST /api/companies — Crée une société */
|
||||
router.post('/', validate(companySchema), wrap(async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
const { name, email } = req.body;
|
||||
const { name, email, extra_emails } = req.body;
|
||||
const result = await db.query(
|
||||
`INSERT INTO companies (name, email) VALUES ($1, $2) RETURNING *`,
|
||||
[name, email]
|
||||
`INSERT INTO companies (name, email, extra_emails) VALUES ($1, $2, $3) RETURNING *`,
|
||||
[name, email, extra_emails]
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
}));
|
||||
|
||||
/** PUT /api/companies/:id — Met à jour une société */
|
||||
router.put('/:id', validate(companySchema), wrap(async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
const { name, email } = req.body;
|
||||
const { name, email, extra_emails } = req.body;
|
||||
const result = await db.query(
|
||||
`UPDATE companies SET name=$1, email=$2, updated_at=NOW()
|
||||
WHERE id=$3 AND is_active=TRUE RETURNING *`,
|
||||
[name, email, req.params.id]
|
||||
`UPDATE companies
|
||||
SET name=$1, email=$2, extra_emails=$3, updated_at=NOW()
|
||||
WHERE id=$4 AND is_active=TRUE
|
||||
RETURNING *`,
|
||||
[name, email, extra_emails, req.params.id]
|
||||
);
|
||||
if (!result.rows[0]) { res.status(404).json({ error: 'Société introuvable' }); return; }
|
||||
res.json(result.rows[0]);
|
||||
|
||||
@@ -81,10 +81,11 @@ const createSchema = z.object({
|
||||
async function getInvoiceById(id: string) {
|
||||
const result = await db.query(
|
||||
`SELECT i.*,
|
||||
co.name AS company_name,
|
||||
co.email AS company_email,
|
||||
cat.name AS category_name,
|
||||
u.name AS user_name
|
||||
co.name AS company_name,
|
||||
co.email AS company_email,
|
||||
co.extra_emails AS company_extra_emails,
|
||||
cat.name AS category_name,
|
||||
u.name AS user_name
|
||||
FROM invoices i
|
||||
JOIN companies co ON co.id = i.company_id
|
||||
JOIN categories cat ON cat.id = i.category_id
|
||||
@@ -327,8 +328,12 @@ router.post('/:id/send', wrap(async (req: AuthRequest, res: Response): Promise<v
|
||||
|
||||
await generateInvoicePdf(imagePaths, invoice.guests, pdfPath, user.name);
|
||||
|
||||
// ── Envoi email via Microsoft Graph ──────────────────────
|
||||
await sendInvoiceEmail(user.email, user.name, invoice.company_email, pdfPath, pdfFilename);
|
||||
// ── Envoi email via Microsoft Graph (tous les destinataires) ────
|
||||
const allRecipients = [
|
||||
invoice.company_email,
|
||||
...((invoice.company_extra_emails as string[] | null) ?? []),
|
||||
].filter(Boolean);
|
||||
await sendInvoiceEmail(user.email, user.name, allRecipients, pdfPath, pdfFilename);
|
||||
|
||||
// ── SharePoint (non bloquant) ─────────────────────────────
|
||||
let trackingAdded = false;
|
||||
|
||||
@@ -68,14 +68,14 @@ async function getAccessToken(cfg: GraphEmailConfig): Promise<string> {
|
||||
*
|
||||
* @param senderEmail Email de l'expéditeur (user.email en base)
|
||||
* @param senderName Nom affiché de l'expéditeur (user.name)
|
||||
* @param toEmail Destinataire (company.email)
|
||||
* @param toEmails Destinataires : email principal + éventuels emails supplémentaires
|
||||
* @param pdfAbsPath Chemin absolu vers le PDF généré
|
||||
* @param pdfFilename Nom du fichier à afficher dans la pièce jointe
|
||||
*/
|
||||
export async function sendInvoiceEmail(
|
||||
senderEmail: string,
|
||||
senderName: string,
|
||||
toEmail: string,
|
||||
toEmails: string | string[],
|
||||
pdfAbsPath: string,
|
||||
pdfFilename: string
|
||||
): Promise<void> {
|
||||
@@ -86,6 +86,11 @@ export async function sendInvoiceEmail(
|
||||
const pdfBuffer = await fs.readFile(pdfAbsPath);
|
||||
const pdfBase64 = pdfBuffer.toString('base64');
|
||||
|
||||
// Normalise en tableau et déduplique
|
||||
const recipients = [...new Set(
|
||||
(Array.isArray(toEmails) ? toEmails : [toEmails]).filter(Boolean)
|
||||
)];
|
||||
|
||||
const payload = {
|
||||
message: {
|
||||
subject: 'Facture à rembourser',
|
||||
@@ -96,9 +101,7 @@ export async function sendInvoiceEmail(
|
||||
from: {
|
||||
emailAddress: { name: senderName, address: senderEmail },
|
||||
},
|
||||
toRecipients: [
|
||||
{ emailAddress: { address: toEmail } },
|
||||
],
|
||||
toRecipients: recipients.map(address => ({ emailAddress: { address } })),
|
||||
attachments: [
|
||||
{
|
||||
'@odata.type': '#microsoft.graph.fileAttachment',
|
||||
|
||||
Reference in New Issue
Block a user