feat: integración Google Sheets al completar turno
Deploy / deploy (push) Failing after 11m51s

Cuando el admin marca un turno como COMPLETADO escribe una fila
en el sheet con patente, km, auto, cliente, fecha y trabajo.
Auth via service account con JWT nativo (sin googleapis).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Beldor
2026-06-22 17:38:14 -03:00
parent 4d259558c6
commit e556e2f16b
4 changed files with 442 additions and 52 deletions
+22
View File
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { verificarSesion } from "@/lib/auth-admin";
import { agregarFilaSheet } from "@/lib/google-sheets";
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const sesion = await verificarSesion("admin_session");
@@ -15,5 +16,26 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id
include: { auto: true, tipoServicio: true, cliente: true },
});
if (estado === "COMPLETADO") {
try {
await agregarFilaSheet({
patente: turno.auto.patente,
kilometros: turno.kilometros,
marca: turno.auto.marca,
modelo: turno.auto.modelo,
anio: turno.auto.anio,
nroChasis: turno.auto.nroChasis,
nombre: turno.cliente.nombre,
apellido: turno.cliente.apellido,
email: turno.cliente.email,
telefono: turno.cliente.telefono,
fecha: turno.fechaInicio,
trabajo: turno.tipoServicio.nombre,
});
} catch (err) {
console.error("Error escribiendo en Google Sheet:", err);
}
}
return NextResponse.json(turno);
}
+95
View File
@@ -0,0 +1,95 @@
import crypto from "crypto";
const SHEET_ID = process.env.GOOGLE_SHEET_ID!;
const SHEET_TAB = "Hoja1";
async function getAccessToken(): Promise<string> {
const clientEmail = process.env.GOOGLE_CLIENT_EMAIL!;
const privateKey = process.env.GOOGLE_PRIVATE_KEY!.replace(/\\n/g, "\n");
const now = Math.floor(Date.now() / 1000);
const header = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url");
const payload = Buffer.from(
JSON.stringify({
iss: clientEmail,
scope: "https://www.googleapis.com/auth/spreadsheets",
aud: "https://oauth2.googleapis.com/token",
exp: now + 3600,
iat: now,
})
).toString("base64url");
const sign = crypto.createSign("RSA-SHA256");
sign.update(`${header}.${payload}`);
const signature = sign.sign(privateKey, "base64url");
const jwt = `${header}.${payload}.${signature}`;
const res = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: jwt,
}),
});
const data = await res.json() as { access_token: string };
return data.access_token;
}
export interface FilaTaller {
patente: string;
kilometros: number;
marca: string;
modelo: string;
anio: number;
nroChasis?: string | null;
nombre: string;
apellido: string;
email: string;
telefono: string;
fecha: Date;
trabajo: string;
}
export async function agregarFilaSheet(fila: FilaTaller) {
const token = await getAccessToken();
const fecha = fila.fecha.toLocaleDateString("es-AR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
const valores = [
fila.patente,
fila.kilometros,
fila.marca,
fila.modelo,
fila.anio,
fila.nroChasis ?? "",
fila.nombre,
fila.apellido,
fila.email,
fila.telefono,
fecha,
fila.trabajo,
];
const url = `https://sheets.googleapis.com/v4/spreadsheets/${SHEET_ID}/values/${SHEET_TAB}!A:L:append?valueInputOption=USER_ENTERED`;
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ values: [valores] }),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Sheets API error: ${err}`);
}
}