532 lines
21 KiB
JavaScript
532 lines
21 KiB
JavaScript
import { Router } from 'express';
|
|
import multer from 'multer';
|
|
import xlsx from 'xlsx';
|
|
import path from 'node:path';
|
|
import fs from 'node:fs';
|
|
import db from '../db/index.js';
|
|
import { HttpError } from '../middleware/errorHandler.js';
|
|
import { requireInvestisseur } from '../middleware/investisseurScope.js';
|
|
import { generateSimul, generateSimulWithReinvestissements } from '../utils/schedule.js';
|
|
|
|
const router = Router();
|
|
|
|
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.resolve('./uploads');
|
|
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
|
|
|
const upload = multer({
|
|
dest: UPLOAD_DIR,
|
|
limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB
|
|
});
|
|
|
|
/**
|
|
* Step 1: POST /api/imports/preview
|
|
* multipart/form-data with `file` (xlsx/csv)
|
|
* -> returns: { headers: [...], sampleRows: [...], allRowCount, tempId }
|
|
*
|
|
* Step 2: POST /api/imports/apply
|
|
* body: { tempId, module, mapping, defaults }
|
|
* -> applies the mapping and inserts rows in the chosen module
|
|
*/
|
|
|
|
// Modules scoped to a specific investisseur (require X-Investisseur-Id)
|
|
const INVESTISSEUR_SCOPED = ['depots_retraits', 'investissements', 'remboursements'];
|
|
|
|
const MODULES = {
|
|
depots_retraits: {
|
|
requiredTargets: ['plateforme_id', 'date_operation', 'type', 'montant'],
|
|
optionalTargets: ['libelle', 'reference', 'notes'],
|
|
},
|
|
investissements: {
|
|
requiredTargets: ['plateforme_id', 'nom_projet', 'date_souscription', 'montant_investi'],
|
|
optionalTargets: ['emetteur','date_premiere_echeance','date_cible','taux_interet','duree_mois','type_remb','freq_interets','statut','reference','notes'],
|
|
},
|
|
remboursements: {
|
|
requiredTargets: ['investissement_id', 'date_remb'],
|
|
optionalTargets: ['capital','cashback','interets_bruts','prelev_sociaux','prelev_forfaitaire','statut','notes'],
|
|
},
|
|
plateformes: {
|
|
requiredTargets: ['nom'],
|
|
optionalTargets: ['url', 'notes'],
|
|
},
|
|
taux_pfu: {
|
|
requiredTargets: ['annee', 'pfu_total', 'impot_revenu', 'prelev_sociaux'],
|
|
optionalTargets: [],
|
|
},
|
|
};
|
|
|
|
/** Parse un fichier uploadé selon son extension → tableau d'objets */
|
|
function parseFile(filePath, originalName) {
|
|
const ext = path.extname(originalName).toLowerCase();
|
|
if (ext === '.json') {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
let parsed;
|
|
try { parsed = JSON.parse(content); } catch {
|
|
throw new HttpError(400, 'Fichier JSON invalide — vérifiez la syntaxe');
|
|
}
|
|
if (!Array.isArray(parsed)) {
|
|
throw new HttpError(400, 'Le fichier JSON doit contenir un tableau d\'objets à la racine');
|
|
}
|
|
return { rows: parsed, sheetName: 'json' };
|
|
}
|
|
// Excel / CSV
|
|
const wb = xlsx.readFile(filePath, { cellDates: true });
|
|
const sheetName = wb.SheetNames[0];
|
|
const ws = wb.Sheets[sheetName];
|
|
return { rows: xlsx.utils.sheet_to_json(ws, { defval: null, raw: false }), sheetName };
|
|
}
|
|
|
|
router.post('/preview', upload.single('file'), (req, res, next) => {
|
|
try {
|
|
if (!req.file) throw new HttpError(400, 'No file uploaded');
|
|
const { rows, sheetName } = parseFile(req.file.path, req.file.originalname);
|
|
const headers = rows.length ? Object.keys(rows[0]) : [];
|
|
|
|
res.json({
|
|
tempId: path.basename(req.file.path),
|
|
filename: req.file.originalname,
|
|
sheetName,
|
|
headers,
|
|
sampleRows: rows.slice(0, 10),
|
|
allRowCount: rows.length,
|
|
});
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
router.post('/apply', (req, res, next) => {
|
|
try {
|
|
const { tempId, module, mapping, defaults = {} } = req.body || {};
|
|
if (!tempId || !module || !mapping) {
|
|
throw new HttpError(400, 'tempId, module and mapping are required');
|
|
}
|
|
const def = MODULES[module];
|
|
if (!def) throw new HttpError(400, 'Unknown module');
|
|
|
|
// Require a specific investisseur for transactional modules
|
|
if (INVESTISSEUR_SCOPED.includes(module)) {
|
|
requireInvestisseur(req, res, () => {});
|
|
}
|
|
|
|
const tempPath = path.join(UPLOAD_DIR, tempId);
|
|
if (!fs.existsSync(tempPath)) throw new HttpError(404, 'Uploaded file expired');
|
|
|
|
for (const target of def.requiredTargets) {
|
|
if (!mapping[target] && defaults[target] === undefined) {
|
|
throw new HttpError(400, `Missing mapping for required field: ${target}`);
|
|
}
|
|
}
|
|
|
|
// Déterminer la source selon l'extension du fichier original (passé en body optionnel)
|
|
const origName = req.body.originalFilename || '';
|
|
const isJson = path.extname(origName).toLowerCase() === '.json';
|
|
const srcLabel = isJson ? 'import_json' : 'import_excel';
|
|
|
|
const { rows } = parseFile(tempPath, origName || 'file.xlsx');
|
|
|
|
let inserted = 0, skipped = 0;
|
|
const errors = [];
|
|
|
|
const tx = db.transaction(() => {
|
|
for (let idx = 0; idx < rows.length; idx++) {
|
|
const row = rows[idx];
|
|
try {
|
|
const v = (target) => {
|
|
const col = mapping[target];
|
|
if (col && row[col] !== undefined && row[col] !== null && row[col] !== '') {
|
|
return row[col];
|
|
}
|
|
return defaults[target];
|
|
};
|
|
|
|
if (module === 'depots_retraits') {
|
|
db.prepare(`
|
|
INSERT INTO depots_retraits
|
|
(investisseur_id, plateforme_id, date_operation, type, montant, libelle, reference, source)
|
|
VALUES (?,?,?,?,?,?,?,?)
|
|
`).run(
|
|
req.investisseur.id,
|
|
Number(v('plateforme_id')),
|
|
normaliseDate(v('date_operation')),
|
|
normaliseType(v('type')),
|
|
num(v('montant')),
|
|
v('libelle') || null,
|
|
v('reference') || null,
|
|
srcLabel,
|
|
);
|
|
|
|
} else if (module === 'investissements') {
|
|
db.prepare(`
|
|
INSERT INTO investissements
|
|
(investisseur_id, plateforme_id, nom_projet, emetteur, date_souscription,
|
|
date_premiere_echeance, date_cible, montant_investi, taux_interet, duree_mois,
|
|
type_remb, freq_interets, statut, reference, source)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
`).run(
|
|
req.investisseur.id,
|
|
Number(v('plateforme_id')),
|
|
String(v('nom_projet')),
|
|
v('emetteur') || null,
|
|
normaliseDate(v('date_souscription')),
|
|
v('date_premiere_echeance') ? normaliseDate(v('date_premiere_echeance')) : null,
|
|
v('date_cible') ? normaliseDate(v('date_cible')) : null,
|
|
num(v('montant_investi')),
|
|
v('taux_interet') ? Number(String(v('taux_interet')).replace(',', '.')) : null,
|
|
v('duree_mois') ? parseInt(v('duree_mois'), 10) : null,
|
|
v('type_remb') || null,
|
|
v('freq_interets') || 'mensuel',
|
|
v('statut') || 'en_cours',
|
|
v('reference') || null,
|
|
srcLabel,
|
|
);
|
|
|
|
} else if (module === 'remboursements') {
|
|
const capital = num(v('capital'));
|
|
const cashback = num(v('cashback'));
|
|
const bruts = num(v('interets_bruts'));
|
|
const ps = num(v('prelev_sociaux'));
|
|
const pf = num(v('prelev_forfaitaire'));
|
|
const interets_nets = Math.round((bruts - ps - pf) * 100) / 100;
|
|
const net_recu = Math.round((capital + cashback + interets_nets) * 100) / 100;
|
|
db.prepare(`
|
|
INSERT INTO remboursements
|
|
(investissement_id, date_remb, capital, cashback, interets_bruts, prelev_sociaux,
|
|
prelev_forfaitaire, interets_nets, net_recu, statut, source)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
`).run(
|
|
Number(v('investissement_id')),
|
|
normaliseDate(v('date_remb')),
|
|
capital, cashback, bruts, ps, pf, interets_nets, net_recu,
|
|
v('statut') || 'paye',
|
|
srcLabel,
|
|
);
|
|
|
|
} else if (module === 'plateformes') {
|
|
const nom = String(v('nom') || '').trim();
|
|
if (!nom) throw new Error('Le champ nom est vide');
|
|
const r = db.prepare(`
|
|
INSERT OR IGNORE INTO plateformes (user_id, nom, url, notes)
|
|
VALUES (?, ?, ?, ?)
|
|
`).run(
|
|
req.user.id,
|
|
nom,
|
|
v('url') || null,
|
|
v('notes') || null,
|
|
);
|
|
// changes = 0 means the row was ignored (nom already exists)
|
|
if (r.changes === 0) throw new Error(`Plateforme "${nom}" existe déjà — ignorée`);
|
|
|
|
} else if (module === 'taux_pfu') {
|
|
const annee = parseInt(v('annee'), 10);
|
|
if (!annee || annee < 2000 || annee > 2100) throw new Error('Année invalide');
|
|
db.prepare(`
|
|
INSERT INTO taux_pfu (annee, pfu_total, impot_revenu, prelev_sociaux)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(annee) DO UPDATE SET
|
|
pfu_total = excluded.pfu_total,
|
|
impot_revenu = excluded.impot_revenu,
|
|
prelev_sociaux = excluded.prelev_sociaux,
|
|
updated_at = datetime('now')
|
|
`).run(
|
|
annee,
|
|
num(v('pfu_total')),
|
|
num(v('impot_revenu')),
|
|
num(v('prelev_sociaux')),
|
|
);
|
|
}
|
|
|
|
inserted++;
|
|
} catch (err) {
|
|
skipped++;
|
|
errors.push({ row: idx + 2, error: err.message });
|
|
}
|
|
}
|
|
});
|
|
tx();
|
|
|
|
db.prepare(`
|
|
INSERT INTO imports (user_id, investisseur_id, module, filename, rows_total, rows_inserted, rows_skipped, mapping_json)
|
|
VALUES (?,?,?,?,?,?,?,?)
|
|
`).run(
|
|
req.user.id,
|
|
req.investisseur?.id ?? null,
|
|
module,
|
|
tempId,
|
|
rows.length,
|
|
inserted,
|
|
skipped,
|
|
JSON.stringify(mapping),
|
|
);
|
|
|
|
// Clean up temp file
|
|
try { fs.unlinkSync(tempPath); } catch { /* */ }
|
|
|
|
res.json({ inserted, skipped, total: rows.length, errors: errors.slice(0, 50) });
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
/**
|
|
* POST /api/imports/dossier
|
|
* Importe un dossier investissement complet (format d'export natif).
|
|
* Scénario CREATE : le dossier n'existe pas → création complète.
|
|
* Scénario UPDATE : le dossier existe déjà → mise à jour des champs + remboursements manquants.
|
|
* Identification : (investisseur_id, nom_projet, date_souscription) — clé naturelle portable.
|
|
*/
|
|
router.post('/dossier', (req, res, next) => {
|
|
try {
|
|
requireInvestisseur(req, res, () => {});
|
|
|
|
const { dossier } = req.body || {};
|
|
if (!dossier || dossier.type !== 'dossier_investissement') {
|
|
throw new HttpError(400, 'Format invalide — attendu { dossier: { type: "dossier_investissement", ... } }');
|
|
}
|
|
const { investissement: inv, plateforme: platInfo, remboursements = [], reinvestissements = [], historique = [] } = dossier;
|
|
if (!inv?.nom_projet || !inv?.date_souscription) {
|
|
throw new HttpError(400, 'Champs obligatoires manquants : nom_projet, date_souscription');
|
|
}
|
|
|
|
let action, investissementId;
|
|
|
|
const tx = db.transaction(() => {
|
|
/* ── 1. Résoudre / créer la plateforme ─────────────────── */
|
|
let platRow = db.prepare('SELECT id FROM plateformes WHERE user_id = ? AND nom = ?')
|
|
.get(req.user.id, platInfo?.nom || '');
|
|
if (!platRow && platInfo?.nom) {
|
|
const r = db.prepare('INSERT INTO plateformes (user_id, nom, url) VALUES (?,?,?)')
|
|
.run(req.user.id, platInfo.nom, platInfo.url || null);
|
|
platRow = { id: r.lastInsertRowid };
|
|
}
|
|
if (!platRow) throw new HttpError(400, 'Plateforme introuvable et nom manquant dans le dossier');
|
|
const plateformeId = platRow.id;
|
|
|
|
/* ── 2. Chercher l'investissement existant (clé naturelle) */
|
|
const existing = db.prepare(`
|
|
SELECT id FROM investissements
|
|
WHERE investisseur_id = ? AND nom_projet = ? AND date_souscription = ?
|
|
LIMIT 1
|
|
`).get(req.investisseur.id, inv.nom_projet, inv.date_souscription);
|
|
|
|
if (!existing) {
|
|
/* ────────────── SCÉNARIO CREATE ──────────────────────── */
|
|
const r = db.prepare(`
|
|
INSERT INTO investissements
|
|
(investisseur_id, plateforme_id, nom_projet, emetteur, date_souscription,
|
|
date_premiere_echeance, date_cible, date_debut_simul, montant_investi,
|
|
taux_interet, duree_mois, type_remb, freq_interets, statut, reference, source, notes)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, 'import_dossier', ?)
|
|
`).run(
|
|
req.investisseur.id, plateformeId,
|
|
inv.nom_projet, inv.emetteur || null,
|
|
inv.date_souscription,
|
|
inv.date_premiere_echeance || null, inv.date_cible || null, inv.date_debut_simul || null,
|
|
Number(inv.montant_investi), inv.taux_interet ?? null, inv.duree_mois ?? null,
|
|
inv.type_remb || 'in_fine', inv.freq_interets || 'mensuel',
|
|
inv.statut || 'en_cours', inv.reference || null, inv.notes || null,
|
|
);
|
|
investissementId = Number(r.lastInsertRowid);
|
|
|
|
// Remboursements
|
|
for (const rb of remboursements) {
|
|
db.prepare(`
|
|
INSERT INTO remboursements
|
|
(investissement_id, date_remb, capital, cashback, interets_bruts,
|
|
prelev_sociaux, prelev_forfaitaire, interets_nets, net_recu, statut, notes, source)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?, 'import_dossier')
|
|
`).run(
|
|
investissementId, rb.date_remb,
|
|
rb.capital || 0, rb.cashback || 0, rb.interets_bruts || 0,
|
|
rb.prelev_sociaux || 0, rb.prelev_forfaitaire || 0,
|
|
rb.interets_nets || 0, rb.net_recu || 0,
|
|
rb.statut || 'paye', rb.notes || null,
|
|
);
|
|
}
|
|
|
|
// Réinvestissements
|
|
for (const rv of reinvestissements) {
|
|
if (!rv.date_reinvestissement || !rv.montant) continue;
|
|
db.prepare(`
|
|
INSERT INTO reinvestissements (investissement_id, montant, date_reinvestissement, note, source)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(
|
|
investissementId,
|
|
Number(rv.montant),
|
|
rv.date_reinvestissement,
|
|
rv.note || null,
|
|
rv.source || 'manuel',
|
|
);
|
|
}
|
|
|
|
// Historique (lecture seule — on importe pour la traçabilité)
|
|
for (const h of historique) {
|
|
db.prepare(`
|
|
INSERT INTO investissement_historique
|
|
(investissement_id, type_evenement, changements, notes, created_at)
|
|
VALUES (?,?,?,?,?)
|
|
`).run(
|
|
investissementId,
|
|
h.type_evenement || 'import',
|
|
typeof h.changements === 'string' ? h.changements : JSON.stringify(h.changements || []),
|
|
h.notes || null,
|
|
h.created_at || null,
|
|
);
|
|
}
|
|
// Entrée d'historique de l'import lui-même
|
|
db.prepare(`
|
|
INSERT INTO investissement_historique (investissement_id, type_evenement, changements)
|
|
VALUES (?, 'import', ?)
|
|
`).run(investissementId, JSON.stringify([{
|
|
champ: 'import', label: 'Import dossier',
|
|
ancienne_valeur: null, nouvelle_valeur: dossier.exported_at || 'inconnu',
|
|
}]));
|
|
|
|
action = 'created';
|
|
} else {
|
|
/* ────────────── SCÉNARIO UPDATE ──────────────────────── */
|
|
investissementId = existing.id;
|
|
db.prepare(`
|
|
UPDATE investissements SET
|
|
plateforme_id = ?, emetteur = ?,
|
|
date_premiere_echeance = ?, date_cible = ?, date_debut_simul = ?,
|
|
montant_investi = ?, taux_interet = ?, duree_mois = ?,
|
|
type_remb = ?, freq_interets = ?, statut = ?,
|
|
reference = ?, notes = ?
|
|
WHERE id = ?
|
|
`).run(
|
|
plateformeId, inv.emetteur || null,
|
|
inv.date_premiere_echeance || null, inv.date_cible || null, inv.date_debut_simul || null,
|
|
Number(inv.montant_investi), inv.taux_interet ?? null, inv.duree_mois ?? null,
|
|
inv.type_remb || 'in_fine', inv.freq_interets || 'mensuel',
|
|
inv.statut || 'en_cours',
|
|
inv.reference || null, inv.notes || null,
|
|
investissementId,
|
|
);
|
|
|
|
// Remboursements : ajouter les manquants (clé naturelle = date_remb + capital + interets_bruts)
|
|
let rembInserted = 0;
|
|
for (const rb of remboursements) {
|
|
const exists = db.prepare(`
|
|
SELECT id FROM remboursements
|
|
WHERE investissement_id = ? AND date_remb = ?
|
|
AND ABS(capital - ?) < 0.005 AND ABS(interets_bruts - ?) < 0.005
|
|
`).get(investissementId, rb.date_remb, rb.capital || 0, rb.interets_bruts || 0);
|
|
if (!exists) {
|
|
db.prepare(`
|
|
INSERT INTO remboursements
|
|
(investissement_id, date_remb, capital, cashback, interets_bruts,
|
|
prelev_sociaux, prelev_forfaitaire, interets_nets, net_recu, statut, notes, source)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?, 'import_dossier')
|
|
`).run(
|
|
investissementId, rb.date_remb,
|
|
rb.capital || 0, rb.cashback || 0, rb.interets_bruts || 0,
|
|
rb.prelev_sociaux || 0, rb.prelev_forfaitaire || 0,
|
|
rb.interets_nets || 0, rb.net_recu || 0,
|
|
rb.statut || 'paye', rb.notes || null,
|
|
);
|
|
rembInserted++;
|
|
}
|
|
}
|
|
|
|
// Réinvestissements : ajouter les manquants (clé = date + montant)
|
|
let reinvInserted = 0;
|
|
for (const rv of reinvestissements) {
|
|
if (!rv.date_reinvestissement || !rv.montant) continue;
|
|
const rvExists = db.prepare(`
|
|
SELECT id FROM reinvestissements
|
|
WHERE investissement_id = ? AND date_reinvestissement = ? AND ABS(montant - ?) < 0.005
|
|
`).get(investissementId, rv.date_reinvestissement, Number(rv.montant));
|
|
if (!rvExists) {
|
|
db.prepare(`
|
|
INSERT INTO reinvestissements (investissement_id, montant, date_reinvestissement, note, source)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(
|
|
investissementId,
|
|
Number(rv.montant),
|
|
rv.date_reinvestissement,
|
|
rv.note || null,
|
|
rv.source || 'manuel',
|
|
);
|
|
reinvInserted++;
|
|
}
|
|
}
|
|
|
|
// Entrée d'historique de la mise à jour
|
|
db.prepare(`
|
|
INSERT INTO investissement_historique (investissement_id, type_evenement, changements)
|
|
VALUES (?, 'import', ?)
|
|
`).run(investissementId, JSON.stringify([{
|
|
champ: 'import', label: 'Mise à jour dossier',
|
|
ancienne_valeur: null,
|
|
nouvelle_valeur: `${dossier.exported_at || 'inconnu'} — ${rembInserted} remb. ajouté(s), ${reinvInserted} réinvest. ajouté(s)`,
|
|
}]));
|
|
|
|
action = 'updated';
|
|
}
|
|
});
|
|
tx();
|
|
|
|
/* ── 3. Régénérer la simulation ─────────────────────────── */
|
|
const fresh = db.prepare('SELECT * FROM investissements WHERE id = ?').get(investissementId);
|
|
generateSimulWithReinvestissements(db, investissementId);
|
|
|
|
/* ── 4. Log import ──────────────────────────────────────── */
|
|
db.prepare(`
|
|
INSERT INTO imports (user_id, investisseur_id, module, filename, rows_total, rows_inserted, rows_skipped, mapping_json)
|
|
VALUES (?,?,?,?,?,?,?,?)
|
|
`).run(
|
|
req.user.id, req.investisseur.id,
|
|
'dossier_investissement',
|
|
`Dossier_${inv.nom_projet}`,
|
|
1, 1, 0,
|
|
JSON.stringify({ action, investissementId }),
|
|
);
|
|
|
|
res.json({ action, investissementId });
|
|
} catch (e) { next(e); }
|
|
});
|
|
|
|
router.get('/history', (req, res) => {
|
|
const rows = db.prepare(`
|
|
SELECT * FROM imports WHERE user_id=? ORDER BY id DESC LIMIT 100
|
|
`).all(req.user.id);
|
|
res.json(rows);
|
|
});
|
|
|
|
// helpers
|
|
|
|
/** Supprime les accents/diacritiques d'une chaîne (ex. "Dépôt" → "Depot") */
|
|
function stripAccents(s) {
|
|
return String(s).normalize('NFD').replace(/[̀-ͯ]/g, '');
|
|
}
|
|
|
|
/** Convertit une valeur monétaire en nombre (gère "€", espaces, virgule décimale) */
|
|
function num(v) {
|
|
if (v === undefined || v === null || v === '') return 0;
|
|
// Supprimer tout ce qui n'est pas chiffre, virgule, point ou signe moins
|
|
const clean = String(v).replace(/[^\d,.-]/g, '').replace(',', '.');
|
|
const n = Number(clean);
|
|
return isNaN(n) ? 0 : n;
|
|
}
|
|
|
|
function normaliseType(v) {
|
|
// Normalise les accents avant la comparaison : "Dépôt" → "depot"
|
|
const s = stripAccents(String(v || '')).toLowerCase();
|
|
if (s.startsWith('dep') || s === 'versement' || s === 'in') return 'depot';
|
|
if (s.startsWith('ret') || s === 'withdrawal' || s === 'out') return 'retrait';
|
|
return s; // CHECK constraint will reject if invalid
|
|
}
|
|
function normaliseDate(v) {
|
|
if (!v) return null;
|
|
if (v instanceof Date) return v.toISOString().slice(0, 10);
|
|
const s = String(v).trim();
|
|
// Already ISO
|
|
if (/^\d{4}-\d{2}-\d{2}/.test(s)) return s.slice(0, 10);
|
|
// dd/mm/yyyy
|
|
const m = s.match(/^(\d{1,2})[/.-](\d{1,2})[/.-](\d{2,4})$/);
|
|
if (m) {
|
|
const [_, d, mo, y] = m;
|
|
const yy = y.length === 2 ? '20' + y : y;
|
|
return `${yy}-${mo.padStart(2, '0')}-${d.padStart(2, '0')}`;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
export default router;
|