empuno 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/empuno.mjs +166 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -55,6 +55,26 @@ komutu CLI güncellemesi olmadan görünür.
55
55
  `flash.created` ya da `redirect.params` içindedir.
56
56
  - Geçmiş veri aktarımı (`data-migration.*`) davet/e-posta/bildirim göndermez.
57
57
 
58
+ ## Başka sistemden geçiş
59
+
60
+ Eski sistemin geçmişini JSON ya da Excel'den alınmış CSV ile taşıyın. Çalışan her
61
+ satırda TC (`identity_number`), sicil no ya da `employee_id` ile belirtilir;
62
+ tekrar çalıştırmak güvenlidir (aynı kayıt `skipped` döner).
63
+
64
+ ```bash
65
+ empuno migrate employees calisanlar.csv --validate # önce yazmadan doğrula
66
+ empuno migrate employees calisanlar.csv # ayrılmışlar dahil
67
+ empuno migrate leaves izinler.csv
68
+ empuno migrate advances avanslar.csv # devreden bakiye: deducted_amount
69
+ empuno migrate expenses masraflar.csv
70
+ empuno migrate payroll bordrolar.csv --decimal , # geçmiş bordro; 38.000,50 biçimi
71
+ empuno migrate documents manifest.csv # file sütunu: dosya yolu
72
+ ```
73
+
74
+ Geçmiş bordro tutarları yeniden hesaplanmaz, aynen saklanır; empuno sonraki ayı
75
+ aktarılan kümülatif gelir vergisi matrahıyla devralır. Alanlar:
76
+ `empuno describe data-migration.payroll.commit`.
77
+
58
78
  ## Talepler (empuno ekibine)
59
79
 
60
80
  Ajanınız yapamadığı bir işlemi, bir hatayı ya da geliştirme önerisini empuno
package/empuno.mjs CHANGED
@@ -21,7 +21,7 @@ import os from 'node:os';
21
21
  import path from 'node:path';
22
22
  import crypto from 'node:crypto';
23
23
 
24
- const VERSION = '1.2.0'; // EMPUNO_CLI_VERSION — config/agent_api.php cli_version + package.json ile aynı tutulur
24
+ const VERSION = '1.4.0'; // EMPUNO_CLI_VERSION — config/agent_api.php cli_version + package.json ile aynı tutulur
25
25
  const NPM_PACKAGE = 'empuno';
26
26
  const DEFAULT_URL = 'https://empuno.com';
27
27
  const CATALOG_TTL_MS = 10 * 60 * 1000;
@@ -840,6 +840,155 @@ async function cmdTickets(args) {
840
840
  }
841
841
  }
842
842
 
843
+ // ─── geçiş (başka sistemden geçmiş verinin aktarımı) ─────────────────────────
844
+
845
+ const MIGRATE_KINDS = {
846
+ employees: { path: '/manager/data-migration/employees/commit', chunk: 200 },
847
+ leaves: { path: '/manager/data-migration/leaves/commit', chunk: 500 },
848
+ advances: { path: '/manager/data-migration/advances/commit', chunk: 200 },
849
+ expenses: { path: '/manager/data-migration/expenses/commit', chunk: 200 },
850
+ documents: { path: '/manager/data-migration/documents/commit', chunk: 10 },
851
+ // Dönem sırasıyla gönderilir: aynı ayın bordroları aynı istekte kalır, kümülatif zincir aydan aya kurulur.
852
+ payroll: { path: '/manager/data-migration/payroll/commit', chunk: 300, sortBy: 'period' },
853
+ };
854
+
855
+ /** Basit CSV ayrıştırıcı: tırnak, "" kaçışı, ; ya da , ayırıcı (başlıktan seçilir), BOM. */
856
+ function parseCsv(text) {
857
+ const src = text.replace(/^/, '');
858
+ const firstLine = src.split(/\r?\n/, 1)[0] || '';
859
+ const delim = (firstLine.match(/;/g) || []).length > (firstLine.match(/,/g) || []).length ? ';' : ',';
860
+ const rows = [];
861
+ let row = [];
862
+ let field = '';
863
+ let quoted = false;
864
+ for (let i = 0; i < src.length; i++) {
865
+ const c = src[i];
866
+ if (quoted) {
867
+ if (c === '"' && src[i + 1] === '"') { field += '"'; i++; }
868
+ else if (c === '"') quoted = false;
869
+ else field += c;
870
+ continue;
871
+ }
872
+ if (c === '"') quoted = true;
873
+ else if (c === delim) { row.push(field); field = ''; }
874
+ else if (c === '\n' || c === '\r') {
875
+ if (c === '\r' && src[i + 1] === '\n') i++;
876
+ row.push(field); field = '';
877
+ if (row.some((v) => v !== '')) rows.push(row);
878
+ row = [];
879
+ } else field += c;
880
+ }
881
+ row.push(field);
882
+ if (row.some((v) => v !== '')) rows.push(row);
883
+ const [header, ...data] = rows;
884
+ if (!header) return [];
885
+ const keys = header.map((h) => h.trim());
886
+ return data.map((cells) => {
887
+ const obj = {};
888
+ keys.forEach((k, idx) => {
889
+ const v = (cells[idx] ?? '').trim();
890
+ if (v === '' || !k) return;
891
+ // CSV değerleri metin kalır (TC, sicil no, telefon); yalnız mantıksal değerler çevrilir.
892
+ obj[k] = ['true', 'false'].includes(v.toLowerCase()) ? v.toLowerCase() === 'true' : v;
893
+ });
894
+ return obj;
895
+ });
896
+ }
897
+
898
+ /** --decimal , : Türk dışa aktarımlarındaki 38.000,50 → 38000.50 (TC, tarih, sicil no dokunulmaz). */
899
+ function commaDecimals(record) {
900
+ for (const [k, v] of Object.entries(record)) {
901
+ if (typeof v === 'string' && /^-?(\d{1,3}(\.\d{3})+|\d+)(,\d+)?$/.test(v) && /[.,]/.test(v)) record[k] = v.replace(/\./g, '').replace(',', '.');
902
+ }
903
+ }
904
+
905
+ function readRecordsFile(file) {
906
+ if (!fs.existsSync(file)) throw new UsageError(`dosya bulunamadı: ${file}`);
907
+ const text = fs.readFileSync(file, 'utf8');
908
+ if (/\.csv$/i.test(file)) return parseCsv(text);
909
+ let data;
910
+ try { data = JSON.parse(text); } catch (e) { throw new UsageError(`geçersiz JSON (${file}): ${e.message}`); }
911
+ const records = Array.isArray(data) ? data : data?.records;
912
+ if (!Array.isArray(records)) throw new UsageError('JSON bir dizi ya da {"records": [...]} olmalı');
913
+ return records;
914
+ }
915
+
916
+ async function cmdMigrate(args) {
917
+ const { flags, fields, positional } = args;
918
+ const [kind, file] = positional;
919
+ if (!kind || !MIGRATE_KINDS[kind] || !file) {
920
+ throw new UsageError(`kullanım: empuno migrate <${Object.keys(MIGRATE_KINDS).join('|')}> <kayitlar.json|.csv> [--validate] [--on_existing update] [--employee_id N] [--chunk N] [--decimal ,]
921
+ alanlar: empuno describe data-migration.${kind && MIGRATE_KINDS[kind] ? kind : 'employees'}.commit · rehber: https://empuno.com/api-docs/agent#gecis`);
922
+ }
923
+ const auth = resolveAuth(flags);
924
+ requireKey(auth);
925
+ const opts = fieldMap(fields);
926
+ const spec = MIGRATE_KINDS[kind];
927
+ const raw = readRecordsFile(file);
928
+ if (opts.decimal === ',') raw.forEach(commaDecimals);
929
+ // Sıralanan türlerde sonuçtaki index yine dosyadaki sırayı gösterir.
930
+ const order = raw.map((_, i) => i);
931
+ if (spec.sortBy) order.sort((a, b) => String(raw[a][spec.sortBy] ?? '').localeCompare(String(raw[b][spec.sortBy] ?? '')) || a - b);
932
+ const records = order.map((i) => raw[i]);
933
+ const chunkSize = Math.max(1, Math.min(Number(opts.chunk) || spec.chunk, spec.chunk));
934
+ const validate = Boolean(flags.validate);
935
+ const baseDir = path.dirname(path.resolve(file));
936
+
937
+ const summary = { kind, file, dry_run: validate, records: records.length, created: 0, updated: 0, skipped: 0, failed: 0, results: [] };
938
+
939
+ for (let offset = 0; offset < records.length; offset += chunkSize) {
940
+ const chunk = records.slice(offset, offset + chunkSize);
941
+ let response;
942
+
943
+ if (kind === 'documents') {
944
+ const files = [];
945
+ const manifest = chunk.map((entry, i) => {
946
+ const filePath = path.resolve(baseDir, String(entry.file ?? ''));
947
+ files.push({ field: 'files[]', file: filePath });
948
+ return { ...entry, file: i };
949
+ });
950
+ for (const f of files) if (!fs.existsSync(f.file)) throw new UsageError(`dosya bulunamadı: ${f.file}`);
951
+ response = await request(auth, 'POST', spec.path, {
952
+ body: { manifest: JSON.stringify(manifest), ...(validate ? { dry_run: 1 } : {}) },
953
+ files,
954
+ dryRun: flags['dry-run'],
955
+ });
956
+ } else {
957
+ const body = { records: chunk, ...(validate ? { dry_run: true } : {}) };
958
+ if (opts.on_existing) body.on_existing = String(opts.on_existing);
959
+ if (opts.employee_id) body.employee_id = Number(opts.employee_id);
960
+ response = await request(auth, 'POST', spec.path, { body, dryRun: flags['dry-run'] });
961
+ }
962
+
963
+ if (response.dryRun) {
964
+ print(response.result);
965
+ return 0;
966
+ }
967
+ const r = response.result || {};
968
+ if (!response.ok) {
969
+ // Doğrulama hatası (422) tüm parçayı etkiler: sunucunun errors'ını kayıt indeksine çevir.
970
+ summary.failed += chunk.length;
971
+ summary.results.push({ chunk_offset: offset, indexes: order.slice(offset, offset + chunk.length), action: 'failed', status: response.status, message: r.message, errors: r.errors });
972
+ continue;
973
+ }
974
+ for (const key of ['created', 'updated', 'skipped', 'failed']) summary[key] += Number(r[key] || 0);
975
+ for (const row of r.results || []) summary.results.push({ ...row, index: order[offset + Number(row.index ?? 0)] });
976
+ if (r.periods) (summary.periods ??= []).push(...r.periods);
977
+ if (isTTY) warn(`${Math.min(offset + chunkSize, records.length)}/${records.length} işlendi`);
978
+ }
979
+
980
+ summary.message = `${summary.created} ${validate ? 'eklenecek' : 'eklendi'}, ${summary.updated} güncellen${validate ? 'ecek' : 'di'}, ${summary.skipped} atlandı, ${summary.failed} hatalı${validate ? ' (doğrulama — hiçbir şey yazılmadı)' : ''}`;
981
+ if (wantsText()) {
982
+ process.stdout.write(`${summary.message}\n`);
983
+ for (const row of summary.results.filter((x) => x.action === 'failed')) {
984
+ process.stdout.write(` ✗ #${row.index ?? row.chunk_offset}: ${row.reason || row.message || ''}${row.errors ? ' ' + JSON.stringify(row.errors) : ''}\n`);
985
+ }
986
+ } else {
987
+ print(summary);
988
+ }
989
+ return summary.failed > 0 ? 1 : 0;
990
+ }
991
+
843
992
  // ─── destek (yalnız empuno ekibinin ajanları — emp_staff_ jetonu) ────────────
844
993
 
845
994
  function requireStaff(auth) {
@@ -968,6 +1117,16 @@ ALANLAR
968
1117
  --alan @./dosya.pdf | --file alan=./dosya.pdf # multipart yükleme
969
1118
  --query anahtar=değer # yazma isteğinde ek sorgu parametresi
970
1119
 
1120
+ GEÇİŞ (başka sistemden geçmiş veri — JSON ya da CSV; davet/e-posta/bildirim gitmez; tekrar çalıştırmak güvenli)
1121
+ empuno migrate employees calisanlar.csv --validate # önce yazmadan doğrula
1122
+ empuno migrate employees calisanlar.csv [--on_existing update] # ayrılmışlar dahil; TC ile eşler
1123
+ empuno migrate leaves izinler.json # her satırda identity_number|employee_number + leave_type
1124
+ empuno migrate advances avanslar.csv # geçmiş avans + devreden bakiye (deducted_amount)
1125
+ empuno migrate expenses masraflar.csv # ödenmiş geçmiş masraf (kategori adla)
1126
+ empuno migrate documents manifest.csv # file sütunu: dosya yolu; ≤10'arlı gönderilir
1127
+ empuno migrate payroll bordrolar.csv # geçmiş bordro (müşavir değerleri aynen); kilitli 'ödendi' dönem + kümülatif GV matrahı
1128
+ alanlar: empuno describe data-migration.<tür>.commit · sıra: çalışanlar → izin → avans → masraf → evrak
1129
+
971
1130
  TALEPLER (geliştirme isteği, hata, eleştiri, soru → empuno ekibi)
972
1131
  empuno tickets [--status active|open|answered|closed|all] [--unread] [--mine]
973
1132
  empuno tickets open "Toplu izin onayı" --kind feature --body "…"|@not.md [--priority high] [--command leave-requests.approve]
@@ -994,8 +1153,9 @@ BELGE: https://empuno.com/api-docs/agent
994
1153
 
995
1154
  async function main() {
996
1155
  const argv = process.argv.slice(2);
997
- const ticketish = ['tickets', 'ticket', 'support'].includes(argv.find((a) => !a.startsWith('--')) ?? '');
998
- const args = parseArgs(argv, ticketish ? ['mine', 'unread', 'close', 'token-stdin'] : []);
1156
+ const sub0 = argv.find((a) => !a.startsWith('--')) ?? '';
1157
+ const ticketish = ['tickets', 'ticket', 'support'].includes(sub0);
1158
+ const args = parseArgs(argv, ticketish ? ['mine', 'unread', 'close', 'token-stdin'] : sub0 === 'migrate' ? ['validate'] : []);
999
1159
  const [sub, ...rest] = args.positional;
1000
1160
  args.positional = rest;
1001
1161
 
@@ -1004,7 +1164,7 @@ async function main() {
1004
1164
  return 0;
1005
1165
  }
1006
1166
  if (args.flags.help) {
1007
- if (['tickets', 'ticket', 'support', 'login', 'commands', 'describe', 'call', 'api'].includes(sub)) {
1167
+ if (['tickets', 'ticket', 'support', 'login', 'commands', 'describe', 'call', 'api', 'migrate', 'reference'].includes(sub)) {
1008
1168
  process.stdout.write(HELP);
1009
1169
  return 0;
1010
1170
  }
@@ -1041,6 +1201,8 @@ async function main() {
1041
1201
  case 'reference':
1042
1202
  case 'ref':
1043
1203
  return cmdReference(args);
1204
+ case 'migrate':
1205
+ return cmdMigrate(args);
1044
1206
  case 'support':
1045
1207
  return cmdSupport(args);
1046
1208
  case 'run':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "empuno",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "empuno İK platformunun komut satırı: yönetim panelindeki tüm işlemleri (izin, bordro, vardiya, PDKS, zimmet, masraf, işe alım) yapay zekâ ajanlarına ve otomasyonlara açar.",
5
5
  "type": "module",
6
6
  "bin": {