7b3ce1ab82
Deploy / deploy (push) Successful in 4m2s
El rango A:N confundía a la API cuando había datos manuales en K/L (GASTOS/VARIOS), haciendo que la nueva fila empezara en columna K en vez de A. INSERT_ROWS siempre inserta fila nueva independiente del estado del sheet. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
2.8 KiB
TypeScript
103 lines
2.8 KiB
TypeScript
import crypto from "crypto";
|
|
|
|
const SHEET_ID = process.env.GOOGLE_SHEET_ID!;
|
|
const SHEET_TAB = "Hoja 1";
|
|
|
|
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;
|
|
nombre: string;
|
|
apellido: string;
|
|
email: string;
|
|
telefono: string;
|
|
fecha: Date;
|
|
trabajo: string;
|
|
comentario?: string | null;
|
|
}
|
|
|
|
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",
|
|
});
|
|
|
|
// K=GASTOS, L=VARIOS, M=M DE OBRA son manuales — se dejan vacíos
|
|
const valores = [
|
|
fila.patente,
|
|
fila.kilometros,
|
|
fila.marca,
|
|
fila.modelo,
|
|
fila.anio,
|
|
fila.nombre,
|
|
fila.apellido,
|
|
fecha,
|
|
fila.email,
|
|
fila.telefono,
|
|
"", // K: GASTOS
|
|
"", // L: VARIOS
|
|
"", // M: M DE OBRA
|
|
fila.comentario ?? "", // N: Comentarios
|
|
];
|
|
|
|
// Anclamos en A1 para que la API siempre busque desde el inicio de la tabla.
|
|
// INSERT_ROWS fuerza una fila nueva, evita que desplace los datos cuando hay
|
|
// celdas ocupadas en columnas manuales (GASTOS, VARIOS, etc.).
|
|
const range = encodeURIComponent(`${SHEET_TAB}!A1`);
|
|
const url = `https://sheets.googleapis.com/v4/spreadsheets/${SHEET_ID}/values/${range}:append?valueInputOption=USER_ENTERED&insertDataOption=INSERT_ROWS`;
|
|
|
|
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}`);
|
|
}
|
|
}
|