Initial Kimai 2.61.0 setup with Kimai 1 migration
Some checks failed
Frontend / Frontend verification (push) Has been cancelled
Lint PHP / Linting (8.2) (push) Has been cancelled
Check .lock files / Verify lock file integrity (push) Has been cancelled
Release Drafter / Verify repository (push) Has been cancelled
Release Drafter / Draft next release (push) Has been cancelled
Tests / Integration (8.2) (push) Has been cancelled
Tests / Integration (8.3) (push) Has been cancelled
Tests / Integration (8.4) (push) Has been cancelled
Tests / Integration (8.5) (push) Has been cancelled
Actions Security Analysis / Scan workflows (push) Has been cancelled

- PHP 8.3, Symfony 6.4, MariaDB 10.11
- Migrated from Kimai 1.x: 908K timesheets, 2,154 users, 427 projects
- Custom migration script at migrate.php
- All users login with: changeme
This commit is contained in:
root
2026-06-18 21:54:38 +00:00
parent 0bedb7a877
commit 553c87d7ea
2 changed files with 661 additions and 0 deletions

View File

@@ -105,6 +105,11 @@
"symfony/web-profiler-bundle": "^6.0"
},
"repositories": [
{
"name": "import-plugin",
"type": "vcs",
"url": "https://github.com/kevinpapst/ImportBundle.git"
},
{
"type": "artifact",
"url": "var/packages/"

656
migrate.php Normal file
View File

@@ -0,0 +1,656 @@
#!/usr/bin/env php
<?php
/**
* Kimai v1 → Kimai v2 Migration Script
*
* Reads from `kimai` database (prefix: kimai15_)
* Writes to `kimai2` database (prefix: kimai2_)
*
* Usage: php8.3 /root/workspace/kimai2/migrate.php
*/
declare(strict_types=1);
// ── Config ──────────────────────────────────────────────────────────────────
const SOURCE_DSN = 'mysql:host=localhost;dbname=kimai;charset=utf8mb4';
const TARGET_DSN = 'mysql:host=localhost;dbname=kimai2;charset=utf8mb4';
const DB_USER = 'root';
const DB_PASS = '';
const DEFAULT_PASSWORD = 'changeme';
const DEFAULT_TIMEZONE = 'UTC';
const DEFAULT_CURRENCY = 'EUR';
const DEFAULT_COUNTRY = '';
const BATCH_SIZE = 2000; // rows per INSERT batch
// ── Bootstrap ───────────────────────────────────────────────────────────────
$src = new PDO(SOURCE_DSN, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$dst = new PDO(TARGET_DSN, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
// Disable foreign key checks and autocommit for speed
$dst->exec('SET FOREIGN_KEY_CHECKS = 0');
$dst->exec('SET AUTOCOMMIT = 0');
// ── Helpers ─────────────────────────────────────────────────────────────────
function progress(string $label, int $done, int $total): void {
$pct = $total > 0 ? round($done / $total * 100, 1) : 100.0;
printf("\r %s: %d / %d (%s%%)", str_pad($label, 20), $done, $total, $pct);
}
function done(string $label, int $count, float $elapsed): void {
printf("\r %s: %d rows (%.2f s)\n", str_pad($label, 20), $count, $elapsed);
}
function batchInsert(PDO $dst, string $table, array $columns, array $rows, int $batchSize = BATCH_SIZE): int {
if (empty($rows)) {
return 0;
}
$total = 0;
$placeholders = '(' . implode(',', array_fill(0, count($columns), '?')) . ')';
$sql = sprintf(
'INSERT INTO `%s` (`%s`) VALUES %s',
$table,
implode('`, `', $columns),
implode(',', array_fill(0, min($batchSize, count($rows)), $placeholders))
);
foreach (array_chunk($rows, $batchSize) as $chunk) {
// Rebuild SQL if chunk is smaller than batchSize (last chunk)
if (count($chunk) < $batchSize) {
$sql = sprintf(
'INSERT INTO `%s` (`%s`) VALUES %s',
$table,
implode('`, `', $columns),
implode(',', array_fill(0, count($chunk), $placeholders))
);
}
$flat = [];
foreach ($chunk as $row) {
foreach ($row as $val) {
$flat[] = $val;
}
}
$stmt = $dst->prepare($sql);
$stmt->execute($flat);
$total += count($chunk);
}
return $total;
}
function safeString(?string $val, int $maxLen = 255): ?string {
if ($val === null || $val === '') {
return null;
}
$val = trim($val);
if ($val === '') {
return null;
}
return mb_substr($val, 0, $maxLen);
}
function timestampToDateTime(int $ts): ?string {
if ($ts <= 0) {
return null;
}
return date('Y-m-d H:i:s', $ts);
}
// ── Truncate target tables first ────────────────────────────────────────────
echo "Clearing target tables...\n";
$tables = [
'kimai2_timesheet_tags', 'kimai2_timesheet_meta', 'kimai2_timesheet',
'kimai2_activities_rates', 'kimai2_activities_teams', 'kimai2_activities_meta', 'kimai2_activities',
'kimai2_projects_rates', 'kimai2_projects_teams', 'kimai2_projects_comments', 'kimai2_projects_meta', 'kimai2_projects',
'kimai2_customers_rates', 'kimai2_customers_teams', 'kimai2_customers_comments', 'kimai2_customers_meta', 'kimai2_customers',
'kimai2_users_teams', 'kimai2_user_preferences', 'kimai2_users',
'kimai2_teams',
];
foreach ($tables as $table) {
try {
$dst->exec("DELETE FROM `$table`");
echo " Cleared: $table\n";
} catch (PDOException $e) {
echo " Skipped (doesn't exist): $table\n";
}
}
$dst->exec('COMMIT');
echo "\n";
// ── Migration order: master data first, then transactions ───────────────────
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 1. Groups → Teams ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
echo "--- Migrating Groups → Teams ---\n";
$t0 = microtime(true);
$groups = $src->query('SELECT grp_ID, grp_name FROM kimai15_grp WHERE grp_trash = 0')->fetchAll();
$rows = [];
foreach ($groups as $g) {
$rows[] = [
'id' => (int) $g['grp_ID'],
'name' => safeString($g['grp_name'], 100) ?? '',
'color' => null,
];
}
$count = batchInsert($dst, 'kimai2_teams', ['id', 'name', 'color'], $rows);
$dst->exec('COMMIT');
done('Teams', $count, microtime(true) - $t0);
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 2. Customers ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
echo "--- Migrating Customers ---\n";
$t0 = microtime(true);
$customers = $src->query('SELECT * FROM kimai15_knd WHERE knd_trash = 0')->fetchAll();
$total = count($customers);
$count = 0;
$rows = [];
foreach ($customers as $c) {
$rows[] = [
'id' => (int) $c['knd_ID'],
'name' => safeString($c['knd_name'], 150) ?: '',
'number' => null,
'comment' => $c['knd_comment'] ?: null,
'visible' => (int) $c['knd_visible'],
'company' => safeString($c['knd_company'], 100),
'contact' => safeString($c['knd_contact'], 100),
'address' => $c['knd_street'] ?: null,
'country' => DEFAULT_COUNTRY,
'currency' => DEFAULT_CURRENCY,
'phone' => safeString($c['knd_tel'], 30),
'fax' => safeString($c['knd_fax'], 30),
'mobile' => safeString($c['knd_mobile'], 30),
'email' => safeString($c['knd_mail'], 75),
'homepage' => safeString($c['knd_homepage'], 100),
'timezone' => DEFAULT_TIMEZONE,
'color' => null,
'time_budget' => 0,
'budget' => 0.0,
'vat_id' => safeString($c['knd_vat'], 50),
'budget_type' => null,
'billable' => 1,
'invoice_template_id' => null,
'invoice_text' => null,
'created_at' => date('Y-m-d H:i:s'),
'address_line1' => safeString($c['knd_street'], 150),
'address_line2' => null,
'address_line3' => null,
'postcode' => safeString($c['knd_zipcode'], 20),
'city' => safeString($c['knd_city'], 50),
'buyer_reference' => null,
];
$count++;
if ($count % 100 === 0) {
progress('Customers', $count, $total);
}
}
$imported = batchInsert($dst, 'kimai2_customers', [
'id', 'name', 'number', 'comment', 'visible', 'company', 'contact', 'address',
'country', 'currency', 'phone', 'fax', 'mobile', 'email', 'homepage',
'timezone', 'color', 'time_budget', 'budget', 'vat_id', 'budget_type',
'billable', 'invoice_template_id', 'invoice_text', 'created_at',
'address_line1', 'address_line2', 'address_line3', 'postcode', 'city', 'buyer_reference',
], $rows);
$dst->exec('COMMIT');
done('Customers', $imported, microtime(true) - $t0);
// Reset auto-increment after inserting with explicit IDs
$dst->exec("ALTER TABLE kimai2_customers AUTO_INCREMENT = " . ((int) $src->query('SELECT MAX(knd_ID) FROM kimai15_knd')->fetchColumn() + 1));
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 3. Projects ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
echo "--- Migrating Projects ---\n";
$t0 = microtime(true);
$projects = $src->query('SELECT * FROM kimai15_pct WHERE pct_trash = 0')->fetchAll();
$total = count($projects);
$count = 0;
$rows = [];
foreach ($projects as $p) {
$rows[] = [
'id' => (int) $p['pct_ID'],
'customer_id' => (int) $p['pct_kndID'],
'name' => safeString($p['pct_name'], 150) ?: '',
'order_number'=> null,
'comment' => $p['pct_comment'] ?: null,
'visible' => (int) $p['pct_visible'],
'budget' => (float) $p['pct_budget'],
'color' => null,
'time_budget' => 0,
'order_date' => null,
'start' => null,
'end' => null,
'timezone' => null,
'budget_type' => null,
'billable' => (int) $p['pct_billable'],
'invoice_text'=> null,
'global_activities' => 1,
'number' => null,
'created_at' => date('Y-m-d H:i:s'),
];
$count++;
if ($count % 100 === 0) {
progress('Projects', $count, $total);
}
}
$imported = batchInsert($dst, 'kimai2_projects', [
'id', 'customer_id', 'name', 'order_number', 'comment', 'visible',
'budget', 'color', 'time_budget', 'order_date', 'start', 'end',
'timezone', 'budget_type', 'billable', 'invoice_text',
'global_activities', 'number', 'created_at',
], $rows);
$dst->exec('COMMIT');
done('Projects', $imported, microtime(true) - $t0);
$dst->exec("ALTER TABLE kimai2_projects AUTO_INCREMENT = " . ((int) $src->query('SELECT MAX(pct_ID) FROM kimai15_pct')->fetchColumn() + 1));
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 4. Activities (global) ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
echo "--- Migrating Activities ---\n";
$t0 = microtime(true);
$activities = $src->query('SELECT * FROM kimai15_evt WHERE evt_trash = 0')->fetchAll();
$total = count($activities);
$count = 0;
$rows = [];
foreach ($activities as $a) {
$rows[] = [
'id' => (int) $a['evt_ID'],
'project_id' => null, // global activities
'name' => safeString($a['evt_name'], 150) ?: '',
'comment' => $a['evt_comment'] ?: null,
'visible' => (int) $a['evt_visible'],
'color' => null,
'time_budget'=> 0,
'budget' => 0.0,
'budget_type'=> null,
'billable' => 1,
'invoice_text'=> null,
'number' => null,
'created_at' => date('Y-m-d H:i:s'),
];
$count++;
if ($count % 100 === 0) {
progress('Activities', $count, $total);
}
}
$imported = batchInsert($dst, 'kimai2_activities', [
'id', 'project_id', 'name', 'comment', 'visible', 'color',
'time_budget', 'budget', 'budget_type', 'billable', 'invoice_text',
'number', 'created_at',
], $rows);
$dst->exec('COMMIT');
done('Activities', $imported, microtime(true) - $t0);
$dst->exec("ALTER TABLE kimai2_activities AUTO_INCREMENT = " . ((int) $src->query('SELECT MAX(evt_ID) FROM kimai15_evt')->fetchColumn() + 1));
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 5. Users ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
echo "--- Migrating Users ---\n";
$t0 = microtime(true);
$users = $src->query('SELECT * FROM kimai15_usr ORDER BY usr_ID')->fetchAll();
$total = count($users);
$count = 0;
$now = date('Y-m-d H:i:s');
$passwordHash = password_hash(DEFAULT_PASSWORD, PASSWORD_BCRYPT);
$userRows = [];
$prefRows = [];
$teamRows = [];
$seenEmails = [];
$seenUsernames = [];
foreach ($users as $u) {
$userId = (int) $u['usr_ID'];
$username = safeString($u['usr_name'], 180) ?: 'user_' . $userId;
$alias = safeString($u['usr_alias'], 60);
$email = safeString($u['usr_mail'], 180) ?: ($username . '@migrated.local');
$enabled = (int) $u['usr_active'];
$v1group = (int) $u['usr_grp'];
// Deduplicate username (case-insensitive)
$origUsername = $username;
$suffix = 1;
while (isset($seenUsernames[mb_strtolower($username)])) {
$username = mb_substr($origUsername, 0, 170) . '_' . $suffix;
$suffix++;
}
$seenUsernames[mb_strtolower($username)] = true;
// Deduplicate email
$origEmail = $email;
$suffix = 1;
$emailLower = mb_strtolower($email);
while (isset($seenEmails[$emailLower])) {
$atPos = strpos($origEmail, '@');
if ($atPos !== false) {
$email = mb_substr($origEmail, 0, min($atPos, 170)) . '+' . $suffix . mb_substr($origEmail, $atPos);
} else {
$email = mb_substr($origEmail, 0, 170) . '+' . $suffix . '@migrated.local';
}
$email = mb_substr($email, 0, 180);
$emailLower = mb_strtolower($email);
$suffix++;
}
$seenEmails[$emailLower] = true;
// Role mapping: admin user gets SUPER_ADMIN, others get USER
if ($origUsername === 'admin' || $origUsername === 'administrator') {
$roles = serialize(['ROLE_SUPER_ADMIN']);
} else {
$roles = serialize(['ROLE_USER']);
}
$userRows[] = [
'id' => $userId,
'username' => $username,
'email' => $email,
'password' => $passwordHash,
'alias' => $alias,
'enabled' => $enabled,
'registration_date' => $now,
'title' => null,
'avatar' => null,
'roles' => $roles,
'last_login' => null,
'confirmation_token' => null,
'password_requested_at' => null,
'api_token' => null,
'auth' => 'kimai',
'color' => null,
'account' => null,
'totp_secret' => null,
'totp_enabled' => 0,
'system_account' => 0,
'supervisor_id' => null,
'signature_date' => null,
];
// User preferences: hourly_rate
$prefRows[] = [
'user_id' => $userId,
'name' => 'hourly_rate',
'value' => '0',
];
$prefRows[] = [
'user_id' => $userId,
'name' => 'timezone',
'value' => DEFAULT_TIMEZONE,
];
// Team membership
if ($v1group > 0) {
$teamRows[] = [
'user_id' => $userId,
'team_id' => $v1group,
'teamlead' => 0,
];
}
$count++;
if ($count % 100 === 0) {
progress('Users', $count, $total);
}
}
// Verify no duplicate emails before insert
$emailCheck = [];
$dupFound = false;
foreach ($userRows as $r) {
$em = mb_strtolower($r['email']);
if (isset($emailCheck[$em])) {
echo "\n *** DUPLICATE EMAIL: $em (IDs: {$emailCheck[$em]} and {$r['id']})\n";
$dupFound = true;
}
$emailCheck[$em] = $r['id'];
}
echo " Verified: " . count($userRows) . " users, " . count($emailCheck) . " unique emails\n";
if ($dupFound) {
echo " ERROR: Duplicate emails detected, aborting.\n";
exit(1);
}
$imported = batchInsert($dst, 'kimai2_users', [
'id', 'username', 'email', 'password', 'alias', 'enabled',
'registration_date', 'title', 'avatar', 'roles', 'last_login',
'confirmation_token', 'password_requested_at', 'api_token', 'auth',
'color', 'account', 'totp_secret', 'totp_enabled', 'system_account',
'supervisor_id', 'signature_date',
], $userRows);
$dst->exec('COMMIT');
done('Users', $imported, microtime(true) - $t0);
$dst->exec("ALTER TABLE kimai2_users AUTO_INCREMENT = " . ((int) $src->query('SELECT MAX(usr_ID) FROM kimai15_usr')->fetchColumn() + 1));
// User preferences
echo "--- Migrating User Preferences ---\n";
$t0 = microtime(true);
$imported = batchInsert($dst, 'kimai2_user_preferences', ['user_id', 'name', 'value'], $prefRows);
$dst->exec('COMMIT');
done('User Preferences', $imported, microtime(true) - $t0);
// User-Team mappings
echo "--- Migrating User-Team Mappings ---\n";
$t0 = microtime(true);
$imported = batchInsert($dst, 'kimai2_users_teams', ['user_id', 'team_id', 'teamlead'], $teamRows);
$dst->exec('COMMIT');
done('User-Team Mappings', $imported, microtime(true) - $t0);
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 6. Rates ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
echo "--- Migrating Rates ---\n";
$t0 = microtime(true);
$rates = $src->query('SELECT * FROM kimai15_rates')->fetchAll();
$projectRates = [];
$activityRates = [];
foreach ($rates as $r) {
if (!empty($r['project_id'])) {
$projectRates[] = [
'user_id' => $r['user_id'] ? (int) $r['user_id'] : null,
'project_id' => (int) $r['project_id'],
'rate' => (float) $r['rate'],
'fixed' => 0,
'internal_rate' => null,
];
}
if (!empty($r['event_id'])) {
$activityRates[] = [
'user_id' => $r['user_id'] ? (int) $r['user_id'] : null,
'activity_id' => (int) $r['event_id'],
'rate' => (float) $r['rate'],
'fixed' => 0,
'internal_rate' => null,
];
}
}
$count1 = batchInsert($dst, 'kimai2_projects_rates', ['user_id', 'project_id', 'rate', 'fixed', 'internal_rate'], $projectRates);
$count2 = batchInsert($dst, 'kimai2_activities_rates', ['user_id', 'activity_id', 'rate', 'fixed', 'internal_rate'], $activityRates);
$dst->exec('COMMIT');
done('Rates (projects)', $count1, microtime(true) - $t0);
done('Rates (activities)', $count2, microtime(true) - $t0);
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 7. Timesheets (the big one: ~908K rows) ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
echo "--- Migrating Timesheets (~908K rows) ---\n";
$t0 = microtime(true);
// Count total
$total = (int) $src->query('SELECT COUNT(*) FROM kimai15_zef')->fetchColumn();
$count = 0;
// Stream with unbuffered query
$stmt = $src->prepare('SELECT zef_ID, zef_in, zef_out, zef_time, zef_usrID, zef_pctID, zef_evtID, zef_comment, zef_rate FROM kimai15_zef ORDER BY zef_ID');
$stmt->execute();
$rows = [];
while ($row = $stmt->fetch()) {
$startTime = timestampToDateTime((int) $row['zef_in']);
// Skip entries with no valid start time
if ($startTime === null) {
$count++;
continue;
}
$endTime = timestampToDateTime((int) $row['zef_out']);
$duration = (int) $row['zef_time']; // seconds
// date_tz: extract date portion from start_time
$dateTz = $startTime ? substr($startTime, 0, 10) : null;
$rows[] = [
'id' => (int) $row['zef_ID'],
'user' => (int) $row['zef_usrID'],
'activity_id' => (int) $row['zef_evtID'],
'project_id' => (int) $row['zef_pctID'],
'start_time' => $startTime,
'end_time' => $endTime,
'duration' => $duration > 0 ? $duration : null,
'description' => $row['zef_comment'] ?: null,
'rate' => (float) $row['zef_rate'],
'fixed_rate' => null,
'hourly_rate' => null,
'exported' => 0,
'timezone' => DEFAULT_TIMEZONE,
'internal_rate'=> null,
'billable' => 1,
'category' => 'work',
'modified_at' => null,
'date_tz' => $dateTz,
'break' => null,
];
$count++;
if ($count % BATCH_SIZE === 0) {
batchInsert($dst, 'kimai2_timesheet', [
'id', 'user', 'activity_id', 'project_id', 'start_time', 'end_time',
'duration', 'description', 'rate', 'fixed_rate', 'hourly_rate',
'exported', 'timezone', 'internal_rate', 'billable', 'category',
'modified_at', 'date_tz', 'break',
], $rows, BATCH_SIZE);
$dst->exec('COMMIT');
$rows = [];
progress('Timesheets', $count, $total);
}
}
// Insert final batch
if (!empty($rows)) {
batchInsert($dst, 'kimai2_timesheet', [
'id', 'user', 'activity_id', 'project_id', 'start_time', 'end_time',
'duration', 'description', 'rate', 'fixed_rate', 'hourly_rate',
'exported', 'timezone', 'internal_rate', 'billable', 'category',
'modified_at', 'date_tz', 'break',
], $rows, BATCH_SIZE);
$dst->exec('COMMIT');
}
done('Timesheets', $count, microtime(true) - $t0);
$dst->exec("ALTER TABLE kimai2_timesheet AUTO_INCREMENT = " . ((int) $src->query('SELECT MAX(zef_ID) FROM kimai15_zef')->fetchColumn() + 1));
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ 8. Expenses (optional) ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
echo "--- Migrating Expenses ---\n";
$t0 = microtime(true);
$expenses = $src->query('SELECT * FROM kimai15_exp')->fetchAll();
$total = count($expenses);
// Expenses are stored as timesheet entries with category='expense' in Kimai2
// Expenses: need a fallback activity_id since Kimai2 requires it NOT NULL
$fallbackActivityId = (int) ($dst->query('SELECT MIN(id) FROM kimai2_activities')->fetchColumn() ?: 1);
$rows = [];
foreach ($expenses as $e) {
$startTime = timestampToDateTime((int) $e['exp_timestamp']);
$dateTz = $startTime ? substr($startTime, 0, 10) : null;
$rows[] = [
'user' => (int) $e['exp_usrID'],
'activity_id' => $fallbackActivityId,
'project_id' => (int) $e['exp_pctID'],
'start_time' => $startTime,
'end_time' => $startTime, // expenses: same as start
'duration' => null,
'description' => ($e['exp_designation'] ?: '') . ($e['exp_comment'] ? ' — ' . $e['exp_comment'] : ''),
'rate' => (float) $e['exp_value'],
'fixed_rate' => null,
'hourly_rate' => null,
'exported' => (int) $e['exp_cleared'],
'timezone' => DEFAULT_TIMEZONE,
'internal_rate'=> null,
'billable' => (int) $e['exp_refundable'],
'category' => 'expense',
'modified_at' => null,
'date_tz' => $dateTz,
'break' => null,
];
}
$imported = batchInsert($dst, 'kimai2_timesheet', [
'user', 'activity_id', 'project_id', 'start_time', 'end_time',
'duration', 'description', 'rate', 'fixed_rate', 'hourly_rate',
'exported', 'timezone', 'internal_rate', 'billable', 'category',
'modified_at', 'date_tz', 'break',
], $rows);
$dst->exec('COMMIT');
done('Expenses', $imported, microtime(true) - $t0);
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ Re-enable foreign keys ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
$dst->exec('SET FOREIGN_KEY_CHECKS = 1');
$dst->exec('SET AUTOCOMMIT = 1');
// ╔══════════════════════════════════════════════════════════════════════════════╗
// ║ Summary ║
// ╚══════════════════════════════════════════════════════════════════════════════╝
echo "\n";
echo str_repeat('=', 60) . "\n";
echo "MIGRATION SUMMARY\n";
echo str_repeat('=', 60) . "\n";
$summaryTables = [
'Teams' => 'kimai2_teams',
'Customers' => 'kimai2_customers',
'Projects' => 'kimai2_projects',
'Activities' => 'kimai2_activities',
'Users' => 'kimai2_users',
'User Preferences' => 'kimai2_user_preferences',
'User-Team Mappings'=> 'kimai2_users_teams',
'Timesheets' => 'kimai2_timesheet',
];
foreach ($summaryTables as $label => $table) {
$cnt = $dst->query("SELECT COUNT(*) FROM `$table`")->fetchColumn();
printf(" %-20s → %s: %d rows\n", $label, $table, $cnt);
}
echo "\nDefault password for all users: " . DEFAULT_PASSWORD . "\n";
echo "Password hash algorithm: BCRYPT\n";
echo "\nDone.\n";