From b3c374251e0f142c1fa13f1a0697d24fdb920a33 Mon Sep 17 00:00:00 2001 From: Kilian Date: Fri, 8 May 2026 17:03:39 +0100 Subject: [PATCH] feat: add reservation import script for Excel data and install xlsx dependency --- ALQUILER_CASAS_2026_LIMPIO.xlsx | Bin 0 -> 21995 bytes apps/api/package.json | 3 +- apps/api/scripts/import-reservations.ts | 254 +++++++++++++++++++++ apps/web/src/components/UserManagement.tsx | 55 +++-- package.json | 7 +- pnpm-lock.yaml | 78 ++++++- 6 files changed, 365 insertions(+), 32 deletions(-) create mode 100644 ALQUILER_CASAS_2026_LIMPIO.xlsx create mode 100644 apps/api/scripts/import-reservations.ts diff --git a/ALQUILER_CASAS_2026_LIMPIO.xlsx b/ALQUILER_CASAS_2026_LIMPIO.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f8a58b08739b074b3c76e10f66431e4ab9d3dc14 GIT binary patch literal 21995 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 QNQwQhV*}9*1}DG%14^S+-~a#s literal 0 HcmV?d00001 diff --git a/apps/api/package.json b/apps/api/package.json index cbf45b0..8ba9104 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -20,6 +20,7 @@ "@types/express": "^5.0.0", "@types/node": "^24.10.1", "tsx": "^4.19.2", - "typescript": "~5.9.3" + "typescript": "~5.9.3", + "xlsx": "^0.18.5" } } diff --git a/apps/api/scripts/import-reservations.ts b/apps/api/scripts/import-reservations.ts new file mode 100644 index 0000000..e28bda4 --- /dev/null +++ b/apps/api/scripts/import-reservations.ts @@ -0,0 +1,254 @@ +/** + * Importa reservas reales desde el Excel ALQUILER_CASAS_2026_LIMPIO.xlsx. + * + * - Borra TODAS las reservas existentes (cascade limpia notification_events y contracts) + * - Inserta filas de LOS DRAGOS y LA ESQUINITA como estancias (origin Teneriffa2000) + * - Inserta filas de EVENTOS como reservas tipo evento (1 noche, is_event=true) + * - Encola notificaciones futuras (24h, 10d, 48h) usando la misma lógica que runner.ts + * - NO envía emails CRUD (escribe directo a Supabase, sin pasar por /api/notifications) + * + * Uso: + * pnpm --filter @naturcalabacera/api exec tsx --env-file=.env scripts/import-reservations.ts # dry-run + * pnpm --filter @naturcalabacera/api exec tsx --env-file=.env scripts/import-reservations.ts --apply # ejecuta + */ + +// Override de DNS: traefik.me wildcards no resuelven con DNS público en algunas redes. +// Forzamos el host de Supabase a su IP conocida. +import dns from 'dns'; +const SUPABASE_HOST_IP = '72.62.155.93'; +const originalLookup = dns.lookup.bind(dns); +// @ts-expect-error monkey-patch +dns.lookup = (hostname: string, options: unknown, callback: unknown) => { + if (typeof hostname === 'string' && hostname.endsWith('.traefik.me')) { + const opts = typeof options === 'object' && options !== null ? (options as { all?: boolean }) : {}; + const cb = (typeof options === 'function' ? options : callback) as (err: NodeJS.ErrnoException | null, addressOrList: unknown, family?: number) => void; + if (opts.all) return cb(null, [{ address: SUPABASE_HOST_IP, family: 4 }]); + return cb(null, SUPABASE_HOST_IP, 4); + } + // @ts-expect-error fallback + return originalLookup(hostname, options, callback); +}; + +// Stubs para env vars no necesarias en este script (email/notif destinos). +// Se setean ANTES de importar módulos que las requieren. +for (const k of [ + 'N8N_EMAIL_WEBHOOK_URL', + 'NOTIFICATION_EMAIL_TENERIFFA', + 'NOTIFICATION_EMAIL_NATUR', + 'NOTIFICATION_EMAIL_POOL_HEATING', +]) { + if (!process.env[k]) process.env[k] = 'unused@import.local'; +} + +import { readFileSync } from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import * as XLSX from 'xlsx'; +import type { NewReservation, Property, ReservationOrigin } from '@naturcalabacera/shared'; + +// Imports dinámicos (después de stubear env) +const { supabaseAdmin } = await import('../src/lib/supabase.js'); +const { scheduleNotificationsForReservation } = await import('../src/jobs/runner.js'); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const REPO_ROOT = path.resolve(__dirname, '../../..'); +const EXCEL_PATH = path.join(REPO_ROOT, 'ALQUILER_CASAS_2026_LIMPIO.xlsx'); + +const APPLY = process.argv.includes('--apply'); + +type Row = (string | null)[]; + +function parseDateDMY(s: string | null): string | null { + if (!s) return null; + const m = s.trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); + if (!m) return null; + const [, d, mo, y] = m; + return `${y}-${mo.padStart(2, '0')}-${d.padStart(2, '0')}`; +} + +function num(s: string | null): number { + if (!s) return 0; + const n = parseInt(String(s).replace(/[^0-9-]/g, ''), 10); + return Number.isFinite(n) ? n : 0; +} + +function addDays(iso: string, days: number): string { + const d = new Date(iso + 'T12:00:00Z'); + d.setUTCDate(d.getUTCDate() + days); + return d.toISOString().slice(0, 10); +} + +function detectEventType(evento: string): { event_type: string; event_type_other?: string } { + const t = evento.toLowerCase(); + if (t.includes('boda')) return { event_type: 'Boda' }; + if (t.includes('comuni')) return { event_type: 'Comunión' }; + if (t.includes('cumple')) return { event_type: 'Cumpleaños' }; + if (t.includes('bautizo')) return { event_type: 'Otro', event_type_other: 'Bautizo' }; + return { event_type: 'Otro', event_type_other: evento }; +} + +function mapStayRow(row: Row, property: Property): NewReservation | null { + const [invoice, status, entrada, salida, huesped, adultos, ninos, , , , limpieza, , , , notas] = row; + if (!entrada || !salida || !huesped) return null; + const start = parseDateDMY(entrada); + const end = parseDateDMY(salida); + if (!start || !end) return null; + if (status && String(status).toUpperCase().includes('CANCEL')) return null; + + const notasStr = (notas ?? '').toString(); + const hasPoolHeating = /calefacci/i.test(notasStr); + const hasCleaning = !!limpieza && num(limpieza) > 0; + + return { + start_date: start, + end_date: end, + client_name: String(huesped).trim(), + origin: 'Teneriffa2000' as ReservationOrigin, + property, + invoice_number: invoice ? String(invoice).trim() : undefined, + adults_count: num(adultos), + children_count: num(ninos), + has_cleaning: hasCleaning, + has_pool_heating: hasPoolHeating, + has_flies_products: false, + observations: notasStr.trim() || undefined, + }; +} + +function mapEventRow(row: Row): NewReservation | null { + const [fecha, , casa, evento, pax, organizador, estado, notas] = row; + if (!fecha || !casa || !evento) return null; + const start = parseDateDMY(fecha); + if (!start) return null; + if (estado && /cancel/i.test(String(estado))) return null; + + const property: Property | null = + /esquinita/i.test(String(casa)) ? 'la_esquinita' : + /dragos/i.test(String(casa)) ? 'los_dragos' : null; + if (!property) return null; + + const { event_type, event_type_other } = detectEventType(String(evento)); + const obsParts: string[] = [String(evento).trim()]; + if (organizador) obsParts.push(`Organizador: ${organizador}`); + if (notas) obsParts.push(String(notas)); + + return { + start_date: start, + end_date: addDays(start, 1), + client_name: String(evento).trim(), + origin: 'Naturcalabacera' as ReservationOrigin, + property, + adults_count: 0, + children_count: 0, + has_cleaning: false, + has_pool_heating: false, + has_flies_products: false, + is_event: true, + event_type, + event_type_other, + attendees_count: pax ? num(pax) : undefined, + observations: obsParts.join(' · '), + }; +} + +function isHeaderOrTotalRow(row: Row): boolean { + if (!row || row.every((c) => c === null || c === '')) return true; + const first = (row[0] ?? '').toString().trim().toUpperCase(); + if (first === 'TOTAL' || first === 'Nº FACTURA' || first.startsWith('LOS DRAGOS') || first.startsWith('LA ESQUINITA')) return true; + if (first.startsWith('RESERVAS CON NOCHES')) return true; + return false; +} + +async function main() { + console.log(`[import] Modo: ${APPLY ? 'APPLY (escribe en BD)' : 'DRY-RUN (no escribe)'}`); + console.log(`[import] Excel: ${EXCEL_PATH}`); + + const buf = readFileSync(EXCEL_PATH); + const wb = XLSX.read(buf, { type: 'buffer' }); + + const dragos = XLSX.utils.sheet_to_json(wb.Sheets['LOS DRAGOS'], { header: 1, defval: null, raw: false }); + const esquinita = XLSX.utils.sheet_to_json(wb.Sheets['LA ESQUINITA'], { header: 1, defval: null, raw: false }); + const eventos = XLSX.utils.sheet_to_json(wb.Sheets['EVENTOS'], { header: 1, defval: null, raw: false }); + + const reservations: NewReservation[] = []; + + for (const r of dragos) { + if (isHeaderOrTotalRow(r)) continue; + const m = mapStayRow(r, 'los_dragos'); + if (m) reservations.push(m); + } + for (const r of esquinita) { + if (isHeaderOrTotalRow(r)) continue; + const m = mapStayRow(r, 'la_esquinita'); + if (m) reservations.push(m); + } + for (const r of eventos) { + if (isHeaderOrTotalRow(r)) continue; + const first = (r[0] ?? '').toString().trim().toUpperCase(); + if (first === 'FECHA' || first.startsWith('BODAS') || first.startsWith('EVENTOS PUNTUALES')) continue; + const m = mapEventRow(r); + if (m) reservations.push(m); + } + + console.log(`[import] Reservas a crear: ${reservations.length}`); + console.log(`[import] - Estancias Los Dragos: ${reservations.filter(r => r.property === 'los_dragos' && !r.is_event).length}`); + console.log(`[import] - Estancias La Esquinita: ${reservations.filter(r => r.property === 'la_esquinita' && !r.is_event).length}`); + console.log(`[import] - Eventos: ${reservations.filter(r => r.is_event).length}`); + + console.log('\n[import] Muestra de las primeras 3 filas mapeadas:'); + console.log(JSON.stringify(reservations.slice(0, 3), null, 2)); + + if (!APPLY) { + console.log('\n[import] Dry-run. Re-ejecuta con --apply para escribir en BD.'); + return; + } + + console.log('\n[import] Borrando todas las reservas existentes...'); + const { count: existingCount } = await supabaseAdmin + .from('reservations') + .select('*', { count: 'exact', head: true }); + console.log(`[import] Reservas existentes: ${existingCount ?? '?'}`); + + // Borrado total. El cascade limpia notification_events y contracts. + const { error: delErr } = await supabaseAdmin + .from('reservations') + .delete() + .not('id', 'is', null); // condición trivial requerida por supabase-js + if (delErr) { + console.error('[import] Error al borrar:', delErr.message); + process.exit(1); + } + console.log('[import] ✓ Borrado completado.'); + + console.log('\n[import] Insertando reservas...'); + const { data: inserted, error: insErr } = await supabaseAdmin + .from('reservations') + .insert(reservations) + .select('*'); + if (insErr) { + console.error('[import] Error al insertar:', insErr.message); + process.exit(1); + } + console.log(`[import] ✓ Insertadas ${inserted?.length ?? 0} reservas.`); + + console.log('\n[import] Encolando notificaciones futuras (sin envío inmediato)...'); + let scheduled = 0; + for (const res of inserted ?? []) { + await scheduleNotificationsForReservation(res as never, 'created'); + scheduled++; + } + console.log(`[import] ✓ Procesadas ${scheduled} reservas para scheduling.`); + + const { count: pending } = await supabaseAdmin + .from('notification_events') + .select('*', { count: 'exact', head: true }) + .eq('status', 'pending'); + console.log(`[import] notification_events pendientes: ${pending ?? 0}`); + console.log('\n[import] ✅ Importación completada.'); +} + +main().catch((err) => { + console.error('[import] Error fatal:', err); + process.exit(1); +}); diff --git a/apps/web/src/components/UserManagement.tsx b/apps/web/src/components/UserManagement.tsx index 7966d9a..d74a254 100644 --- a/apps/web/src/components/UserManagement.tsx +++ b/apps/web/src/components/UserManagement.tsx @@ -13,6 +13,9 @@ interface UserProfileRow { created_at: string; } +// Toggle para reactivar el borrado de usuarios desde la UI. +const SHOW_DELETE_USER = false as boolean; + const ROLE_OPTIONS: { value: UserRole; label: string; description: string; icon: typeof Shield }[] = [ { value: 'admin', label: 'Admin', description: 'Acceso total y gestión de usuarios', icon: Shield }, { value: 'internal_staff', label: 'Staff', description: 'Gestión completa de reservas', icon: UsersIcon }, @@ -34,7 +37,8 @@ export function UserManagement() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [inviteOpen, setInviteOpen] = useState(false); + // Invitación deshabilitada por ahora (sin SMTP). El modal y endpoint se conservan. + const [inviteOpen, _setInviteOpen] = useState(false); const [savingId, setSavingId] = useState(null); const loadUsers = async () => { @@ -84,6 +88,7 @@ export function UserManagement() { } }; + // Borrado deshabilitado en UI; se conserva para reactivar. const handleDelete = async (user: UserProfileRow) => { if (!confirm(`¿Eliminar a ${user.email}? Esta acción no se puede deshacer.`)) return; setSavingId(user.id); @@ -104,19 +109,9 @@ export function UserManagement() { return (
-
-
-

Usuarios

-

Gestiona accesos y roles del equipo.

-
- +
+

Usuarios

+

Gestiona accesos y roles del equipo. Las altas se hacen desde Supabase.

{error && ( @@ -138,13 +133,13 @@ export function UserManagement() { Email / Nombre Rol Creado - Acciones + {SHOW_DELETE_USER && Acciones} {users.length === 0 ? ( - + No hay usuarios registrados. @@ -171,17 +166,19 @@ export function UserManagement() { {new Date(user.created_at).toLocaleDateString('es-ES')} - - - + {SHOW_DELETE_USER && ( + + + + )} ))} @@ -192,10 +189,10 @@ export function UserManagement() { {inviteOpen && ( setInviteOpen(false)} + onClose={() => _setInviteOpen(false)} onInvited={(newUser) => { setUsers(prev => [newUser, ...prev]); - setInviteOpen(false); + _setInviteOpen(false); toast.success('Invitación enviada'); }} /> diff --git a/package.json b/package.json index d8fdaf0..0996aab 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "private": true, "version": "0.0.0", "pnpm": { - "onlyBuiltDependencies": ["esbuild"] + "onlyBuiltDependencies": [ + "esbuild" + ] }, "scripts": { "dev:web": "pnpm --filter @naturcalabacera/web dev", @@ -13,5 +15,8 @@ "build": "pnpm run build:web && pnpm run build:api", "lint": "pnpm --filter @naturcalabacera/web lint", "test": "pnpm --filter @naturcalabacera/shared test" + }, + "devDependencies": { + "xlsx": "^0.18.5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1726fa..392828c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,11 @@ settings: importers: - .: {} + .: + devDependencies: + xlsx: + specifier: ^0.18.5 + version: 0.18.5 apps/api: dependencies: @@ -38,6 +42,9 @@ importers: typescript: specifier: ~5.9.3 version: 5.9.3 + xlsx: + specifier: ^0.18.5 + version: 0.18.5 apps/web: dependencies: @@ -933,6 +940,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} @@ -1030,6 +1041,10 @@ packages: caniuse-lite@1.0.30001787: resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1050,6 +1065,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1086,6 +1105,11 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1313,6 +1337,10 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} @@ -1861,6 +1889,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2108,10 +2140,18 @@ packages: engines: {node: '>=8'} hasBin: true + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + ws@8.20.0: resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} @@ -2124,6 +2164,11 @@ packages: utf-8-validate: optional: true + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2844,6 +2889,8 @@ snapshots: acorn@8.16.0: {} + adler-32@1.3.1: {} + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 @@ -2945,6 +2992,11 @@ snapshots: caniuse-lite@1.0.30001787: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -2974,6 +3026,8 @@ snapshots: clsx@2.1.1: {} + codepage@1.15.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3001,6 +3055,8 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + crc-32@1.2.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3301,6 +3357,8 @@ snapshots: forwarded@0.2.0: {} + frac@1.1.2: {} + fraction.js@5.3.4: {} framer-motion@12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): @@ -3793,6 +3851,10 @@ snapshots: source-map-js@1.2.1: {} + ssf@0.11.2: + dependencies: + frac: 1.1.2 + stackback@0.0.2: {} statuses@2.0.2: {} @@ -4018,10 +4080,24 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wmf@1.0.2: {} + word-wrap@1.2.5: {} + word@0.3.0: {} + ws@8.20.0: {} + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + yallist@3.1.1: {} yocto-queue@0.1.0: {}