diff --git a/application/User/UserController.php b/application/User/UserController.php
index 474a12468..0b4a6f934 100644
--- a/application/User/UserController.php
+++ b/application/User/UserController.php
@@ -1,5 +1,8 @@
self::getUrl("User", "Form"),
"EDIT_URL" => self::getUrl("User", "Form"),
"IMPERSONATE_URL" => self::getUrl("User", "impersonate"),
+ "SEND_LOGIN_EMAIL_URL" => self::getUrl("User", "sendLoginEmail"),
]);
}
@@ -583,4 +587,159 @@ class UserController extends mfBaseController
$_SESSION[MFAPPNAME.'_impersonate'] = $this->request->username;
$this->redirect("Dashboard");
}
+
+ protected function sendLoginEmailAction()
+ {
+ $id = $this->request->id;
+ if (!$id || !is_numeric($id)) {
+ self::sendError("Benutzer-ID fehlt oder ist ungültig.");
+ }
+
+ $user = new User($id);
+ if (!$user->id || !$user->email) {
+ self::sendError("Benutzer nicht gefunden oder keine E-Mail-Adresse hinterlegt.");
+ }
+
+ $userFullName = htmlspecialchars($user->name);
+ $userUsername = htmlspecialchars($user->username);
+ $firstName = htmlspecialchars(explode(' ', $user->name)[0]);
+
+ $subject = "Ihr Zugang zu TheTOOL by XINON, {$firstName}!";
+
+ $logoToolPath = LIBDIR . '/../public/assets/images/the-tool-logo.png';
+ $logoXinonPath = LIBDIR . '/../public/assets/images/xinon-full.png';
+
+ $logoToolTag = file_exists($logoToolPath) ? '
' : '';
+ $logoXinonTag = file_exists($logoXinonPath) ? '
' : '';
+
+ $currentYear = date("Y");
+ $xinonBlue = '#005384';
+ $loginLink = 'https://thetool.xinon.at';
+ $passwordResetLink = 'https://thetool.xinon.at/UserPasswordReset/forgotPassword';
+
+ $html = <<
+
+
+
+
+ {$subject}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | {$logoToolTag} |
+ {$logoXinonTag} |
+
+
+ |
+
+
+
+ Willkommen an Bord, {$firstName}!
+ Wir freuen uns, Sie im Team zu haben. Ihr Zugang zu einer smarteren Arbeitsweise ist freigeschaltet.
+
+ Ihr persönlicher Benutzername lautet:
+
+
+ |
+ {$userUsername}
+ |
+
+
+
+ Um zu starten, legen Sie bitte Ihr persönliches Passwort fest.
+
+
+
+
+ |
+
+
+ |
+
+ © {$currentYear} XINON GmbH | Impressum
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+HTML;
+
+ $altBody = "Willkommen an Bord, {$firstName}!\n\n" .
+ "Wir freuen uns, Sie im Team zu haben. Ihr Zugang zu einer smarteren Arbeitsweise ist freigeschaltet.\n\n" .
+ "Ihr persönlicher Benutzername lautet: {$userUsername}\n\n" .
+ "Um zu starten, legen Sie bitte Ihr persönliches Passwort fest. Besuchen Sie dazu folgende Seite:\n" .
+ "{$passwordResetLink}\n\n" .
+ "Oder besitzen Sie bereits ein Passwort? Hier geht es direkt zum Login:\n" .
+ "{$loginLink}\n\n" .
+ "© {$currentYear} XINON GmbH | Impressum: https://xinon.at/impressum/";
+
+ $mail = new PHPMailer(true);
+ try {
+ $mail->isSMTP();
+ $mail->Host = TT_PIPEWORK_SMTP_HOST;
+ $mail->SMTPAuth = true;
+ $mail->Username = TT_PIPEWORK_SMTP_USER;
+ $mail->Password = TT_PIPEWORK_SMTP_PASS;
+ $mail->CharSet = PHPMailer::CHARSET_UTF8;
+ $mail->Encoding = 'base64';
+ $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
+ $mail->Port = 587;
+
+ if (file_exists($logoToolPath)) $mail->addEmbeddedImage($logoToolPath, 'logo_thetool');
+ if (file_exists($logoXinonPath)) $mail->addEmbeddedImage($logoXinonPath, 'logo_xinon');
+
+ $mail->setFrom('thetool@xinon.at', 'TheTOOL by XINON');
+ $mail->addReplyTo('office@xinon.at', 'XINON Office');
+ $mail->addAddress($user->email, $user->name);
+
+ $mail->isHTML(true);
+ $mail->Subject = $subject;
+ $mail->Body = $html;
+ $mail->AltBody = $altBody;
+
+ $mail->send();
+ self::returnJson(['success' => true, 'message' => 'Willkommens-E-Mail wurde erfolgreich an ' . htmlspecialchars($user->email) . ' gesendet.']);
+
+ } catch (Exception $e) {
+ error_log("Mailer Error in sendLoginEmailAction for user ID {$id}: " . $mail->ErrorInfo);
+ self::sendError("E-Mail konnte nicht gesendet werden. Bitte kontaktieren Sie den Support.");
+ }
+ }
}
diff --git a/public/js/pages/User/User.js b/public/js/pages/User/User.js
index 343267058..b39a9b265 100644
--- a/public/js/pages/User/User.js
+++ b/public/js/pages/User/User.js
@@ -1,64 +1,109 @@
Vue.component("User", {
template: `
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
- `, data: () => ({
- window: window, UserTableConfig: {
- key: "UserTable",
- tableHeader: "Benutzer",
+
+
+
+ Sind Sie sicher, dass Sie die Login-Informationen an {{ selectedUserForMail.email }} senden möchten?
+
+
+ `,
+ data: () => ({
+ window: window,
+ showSendMailModal: false,
+ selectedUserForMail: null,
+ isSendingMail: false,
+ UserTableConfig: {
+ key: "UserTable",
+ tableHeader: "Benutzer",
defaultPageSize: 25,
- headers: [{text: "Username", key: "username", class: "text-center", sortable: false, priority: 20},
- {text: "Name", key: "name", class: "text-center", sortable: false, priority: 18},
- {text: "Firma", key: "address", class: "text-center", priority: 19},
- {text: "E-Mail", key: "email", priority: 14},
- {text: "Tel. Nr.", key: "mobile", priority: 17, filter: false, sortable: false},
- {
- text: "2FA", key: "twofactor", class: "text-center", priority: 16, filter: 'iconSelect', sortable: false,
- filterOptions: [{value: "N/A", text: "N/A", icon: "fa fa-exclamation-triangle text-danger"},
- {value: "Mail", text: "Mail", icon: "fa-light fa-envelope text-primary"},
- {value: "SMS", text: "SMS", icon: "fa-light fa-mobile-retro text-info"}]
- },
- {
- text: "Admin", key: "isAdmin", class: "text-center", priority: 13, filter: 'iconSelect', sortable: false,
- filterOptions: [{value: true, text: "Ist Admin", icon: "fa-regular fa-circle-check text-success"},
- {value: false, text: "Ist kein Admin", icon: "fa-regular fa-circle-xmark text-danger"}]
- },
- {
- text: "Techniker", key: "isTechnician", class: "text-center", priority: 12, filter: 'iconSelect', sortable: false,
- filterOptions: [{value: true, text: "Ist Techniker", icon: "fa-regular fa-circle-check text-success"},
- {value: false, text: "Ist kein Techniker", icon: "fa-regular fa-circle-xmark text-danger"}],
- },
- {
- text: "Aktiv", key: "isActive", class: "text-center", priority: 12, filter: 'iconSelect', sortable: false,
- filterOptions: [{value: "1", text: "Ist Aktiv", icon: "fa-regular fa-circle-check text-success"},
- {value: "0", text: "Ist nicht aktiv", icon: "fa-regular fa-circle-xmark text-danger"}],
- },
- {text: "Aktionen", key: "actions", class: "text-center", sortable: false, priority: 21, filter: false}]
+ headers: [{text: "Username", key: "username", class: "text-center", sortable: false, priority: 20},
+ {text: "Name", key: "name", class: "text-center", sortable: false, priority: 18},
+ {text: "Firma", key: "address", class: "text-center", priority: 19},
+ {text: "E-Mail", key: "email", priority: 14},
+ {text: "Tel. Nr.", key: "mobile", priority: 17, filter: false, sortable: false},
+ {
+ text: "2FA", key: "twofactor", class: "text-center", priority: 16, filter: 'iconSelect', sortable: false,
+ filterOptions: [{value: "N/A", text: "N/A", icon: "fa fa-exclamation-triangle text-danger"},
+ {value: "Mail", text: "Mail", icon: "fa-light fa-envelope text-primary"},
+ {value: "SMS", text: "SMS", icon: "fa-light fa-mobile-retro text-info"}]
+ },
+ {
+ text: "Admin", key: "isAdmin", class: "text-center", priority: 13, filter: 'iconSelect', sortable: false,
+ filterOptions: [{value: true, text: "Ist Admin", icon: "fa-regular fa-circle-check text-success"},
+ {value: false, text: "Ist kein Admin", icon: "fa-regular fa-circle-xmark text-danger"}]
+ },
+ {
+ text: "Techniker", key: "isTechnician", class: "text-center", priority: 12, filter: 'iconSelect', sortable: false,
+ filterOptions: [{value: true, text: "Ist Techniker", icon: "fa-regular fa-circle-check text-success"},
+ {value: false, text: "Ist kein Techniker", icon: "fa-regular fa-circle-xmark text-danger"}],
+ },
+ {
+ text: "Aktiv", key: "isActive", class: "text-center", priority: 12, filter: 'iconSelect', sortable: false,
+ filterOptions: [{value: "1", text: "Ist Aktiv", icon: "fa-regular fa-circle-check text-success"},
+ {value: "0", text: "Ist nicht aktiv", icon: "fa-regular fa-circle-xmark text-danger"}],
+ },
+ {text: "Aktionen", key: "actions", class: "text-center", sortable: false, priority: 21, filter: false}]
}
- })
-});
+ }),
+ methods: {
+ openSendMailModal(user) {
+ this.selectedUserForMail = user;
+ this.showSendMailModal = true;
+ },
+ async sendLoginEmail() {
+ if (!this.selectedUserForMail) return;
+ this.isSendingMail = true;
+ try {
+ const response = await axios.get(window.TT_CONFIG.SEND_LOGIN_EMAIL_URL + '?id=' + this.selectedUserForMail.id);
+ if (response.data.success) {
+ window.notify('success', response.data.message);
+ } else {
+ window.notify('error', response.data.message || 'E-Mail konnte nicht gesendet werden.');
+ }
+ } catch (error) {
+ window.notify('error', 'Ein Fehler ist aufgetreten.');
+ console.error(error);
+ }
+ this.isSendingMail = false;
+ this.showSendMailModal = false;
+ this.selectedUserForMail = null;
+ }
+ }
+});
\ No newline at end of file
diff --git a/public/js/pages/UserEdit/UserEdit.css b/public/js/pages/UserEdit/UserEdit.css
index ffbe45d53..bdfa333f4 100644
--- a/public/js/pages/UserEdit/UserEdit.css
+++ b/public/js/pages/UserEdit/UserEdit.css
@@ -1,5 +1,5 @@
.user-edit-container {
- padding-bottom: 60px; /* Space for the bottom save button */
+ padding-bottom: 60px
}
.user-edit-header {
@@ -8,136 +8,146 @@
align-items: center;
margin-bottom: 1rem;
margin-top: 1.5rem;
- padding: 0.5rem;
+ padding: .5rem
+}
+
+.user-edit-header .header-actions {
+ display: flex;
+ gap: .5rem
}
.user-edit-header.sticky-header {
position: sticky;
- top: 60px; /* Adjust based on your main header height */
- background-color: #f8f9fa; /* Match card background */
+ top: 60px;
+ background-color: #f8f9fa;
z-index: 10;
- padding: 0.75rem 1rem;
+ padding: .75rem 1rem;
border-bottom: 1px solid #dee2e6;
- margin: -1.25rem -1.25rem 1rem -1.25rem; /* Make it span the card width */
+ margin: -1.25rem -1.25rem 1rem -1.25rem
}
.user-edit-header.collapsible {
cursor: pointer;
border-bottom: 1px solid #dee2e6;
- padding-bottom: 0.75rem;
- transition: background-color 0.2s;
+ padding-bottom: .75rem;
+ transition: background-color .2s
}
+
.user-edit-header.collapsible:hover {
- background-color: #f1f3f5;
+ background-color: #f1f3f5
}
.user-edit-header h3 {
- margin: 0;
+ margin: 0
}
.user-form-grid {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 0.5rem 1.5rem;
+ grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));
+ gap: .5rem 1.5rem
}
.user-form-grid-half {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
- gap: 1rem;
+ grid-template-columns:repeat(auto-fit, minmax(400px, 1fr));
+ gap: 1rem
}
.user-form-grid-toggles {
display: flex;
gap: 2rem;
- margin-top: 1rem;
+ margin-top: 1rem
}
.user-form-grid-toggles .form-group {
display: flex;
align-items: center;
- gap: 0.75rem;
- margin-bottom: 0;
+ gap: .75rem;
+ margin-bottom: 0
}
.permission-template-section {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
- align-items: end;
+ align-items: end
}
.permissions-grid {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 1.5rem 2rem;
+ grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1.5rem 2rem
}
.permission-group {
border: 1px solid #dee2e6;
- border-radius: 0.3rem;
+ border-radius: .3rem;
padding: 1rem;
- background-color: #fff;
+ background-color: #fff
}
.permission-group h5 {
font-size: 1.1rem;
margin-bottom: 1rem;
- color: #0056b3;
+ color: #0056b3
}
.form-check-label {
- user-select: none;
+ user-select: none
}
.password-generation-grid {
display: grid;
- grid-template-columns: 1fr auto auto;
- gap: 0.5rem;
- align-items: end;
+ grid-template-columns:1fr auto auto;
+ gap: .5rem;
+ align-items: end
}
.password-generation-grid .form-group {
- margin-bottom: 0;
+ margin-bottom: 0
}
.selected-items-viewer {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
- margin-top: 1.5rem;
+ margin-top: 1.5rem
}
.selection-list-container {
border: 1px solid #e9ecef;
- padding: 0.75rem;
- border-radius: 0.25rem;
+ padding: .75rem;
+ border-radius: .25rem
}
.selection-list {
list-style-type: none;
padding-left: 0;
- margin-top: 0.5rem;
+ margin-top: .5rem;
max-height: 200px;
- overflow-y: auto;
+ overflow-y: auto
}
+
.selection-list li {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 0.25rem 0.5rem;
- border-radius: 0.2rem;
+ padding: .25rem .5rem;
+ border-radius: .2rem
}
+
.selection-list li:hover {
- background-color: #f8f9fa;
+ background-color: #f8f9fa
}
+
.selection-list li .fa-times-circle {
color: #dc3545;
cursor: pointer;
- opacity: 0.7;
+ opacity: .7
}
+
.selection-list li .fa-times-circle:hover {
- opacity: 1;
+ opacity: 1
}
.user-edit-footer {
@@ -145,26 +155,36 @@
bottom: 0;
left: 0;
right: 0;
- padding: 0.75rem;
- background-color: #ffffff;
+ padding: .75rem;
+ background-color: #fff;
border-top: 1px solid #dee2e6;
display: flex;
justify-content: flex-end;
- z-index: 1000;
+ z-index: 1000
}
-/* Slide-fade transition */
.slide-fade-enter-active, .slide-fade-leave-active {
- transition: all .3s ease;
+ transition: all .3s ease
}
+
.slide-fade-enter, .slide-fade-leave-to {
transform: translateY(-10px);
- opacity: 0;
+ opacity: 0
}
+
.slide-fade-fast-enter-active, .slide-fade-fast-leave-active {
- transition: all .2s ease;
+ transition: all .2s ease
}
+
.slide-fade-fast-enter, .slide-fade-fast-leave-to {
transform: translateY(-5px);
- opacity: 0;
+ opacity: 0
+}
+
+.loading-overlay {
+ position: relative;
+ padding: 20px;
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)
}
\ No newline at end of file
diff --git a/public/js/pages/UserEdit/UserEdit.js b/public/js/pages/UserEdit/UserEdit.js
index 0d8622d34..296594535 100644
--- a/public/js/pages/UserEdit/UserEdit.js
+++ b/public/js/pages/UserEdit/UserEdit.js
@@ -5,9 +5,12 @@ Vue.component("UserEdit", {
+
+
+
+
+
+ {{ Math.round(bulkProgress) }}%
+
+
+
Benutzer {{ bulkCurrentRow + 1 }} von {{ bulkTotalRows }} wird erstellt...
+
+
+
+ Hinweis: Die Berechtigungen, Zugriffe und allgemeinen Einstellungen (Firma, 2FA, etc.) werden aus dem Hauptformular übernommen. Tragen Sie hier nur die individuellen Benutzerdaten wie Name und E-Mail ein.
+
+
+
+
`,
components: {
@@ -160,6 +189,27 @@ Vue.component("UserEdit", {
employeeSpecific: true,
projects: true,
security: true,
+ },
+ showBulkCreateModal: false,
+ isBulkSaving: false,
+ bulkUsers: [],
+ bulkProgress: 0,
+ bulkCurrentRow: 0,
+ bulkTotalRows: 0,
+ bulkCreateConfig: {
+ fields: {
+ username: { type: 'input', label: 'Username' },
+ name: { type: 'input', label: 'Name' },
+ email: { type: 'input', label: 'Email', inputType: 'email' },
+ mobile: { type: 'input', label: 'Handy Nr.' }
+ },
+ validateForm(formData) {
+ if (!formData.username || !formData.name || !formData.email) {
+ window.notify('error', 'Username, Name und Email sind Pflichtfelder.');
+ return false;
+ }
+ return true;
+ }
}
}
},
@@ -320,12 +370,23 @@ Vue.component("UserEdit", {
(this.user.constructionconsent_projects || []).forEach(val => formData.append('constructionconsent_projects[]', val));
axios.post(window.TT_CONFIG.SAVE_URL, formData)
- .then(() => {
- window.notify('success', 'Benutzer erfolgreich gespeichert.');
- setTimeout(() => window.location.href = '/User', 150);
+ .then(response => {
+ // A successful save redirects. The final URL will be different from the POST URL.
+ const wasRedirected = response.request.responseURL && !response.request.responseURL.endsWith(window.TT_CONFIG.SAVE_URL);
+
+ if (response.status === 200 && wasRedirected) {
+ window.notify('success', 'Benutzer erfolgreich gespeichert.');
+ setTimeout(() => window.location.href = '/User', 150);
+ } else if (response.data && response.data.success === false) { // Explicit JSON error from backend
+ const errorMsg = response.data.errors ? Object.values(response.data.errors).join('
') : response.data.message;
+ window.notify('error', errorMsg || 'Fehler beim Speichern des Benutzers.');
+ } else { // Fallback
+ window.notify('error', 'Eine unerwartete Antwort vom Server wurde empfangen.');
+ }
})
.catch(error => {
- window.notify('error', 'Fehler beim Speichern des Benutzers.');
+ const errorMsg = error.response?.data?.errors ? Object.values(error.response.data.errors).join('
') : (error.response?.data?.message || 'Fehler beim Speichern des Benutzers.');
+ window.notify('error', errorMsg);
console.error(error);
})
.finally(() => {
@@ -341,13 +402,16 @@ Vue.component("UserEdit", {
window.notify('error', 'API Key konnte nicht generiert werden.');
}
},
- generatePassword() {
+ generatePassword(returnOnly = false) {
const length = 14;
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let retVal = "";
for (let i = 0, n = charset.length; i < length; ++i) {
retVal += charset.charAt(Math.floor(Math.random() * n));
}
+ if(returnOnly) {
+ return retVal;
+ }
this.password.new = retVal;
this.password.repeat = retVal;
window.notify('info', 'Neues Passwort generiert.');
@@ -360,6 +424,103 @@ Vue.component("UserEdit", {
if (index > -1) {
this.user[arrayName].splice(index, 1);
}
+ },
+ async handleBulkCreate() {
+ if (this.bulkUsers.length === 0) {
+ window.notify('error', 'Bitte fügen Sie mindestens einen Benutzer hinzu.');
+ return;
+ }
+ this.isBulkSaving = true;
+ this.bulkTotalRows = this.bulkUsers.length;
+ this.bulkCurrentRow = 0;
+ this.bulkProgress = 0;
+
+ const createdUsersForCSV = [];
+ const failedUsers = [];
+
+ for (const [index, user] of this.bulkUsers.entries()) {
+ this.bulkCurrentRow = index;
+ this.bulkProgress = ((index + 1) / this.bulkTotalRows) * 100;
+
+ const password = this.generatePassword(true);
+ const formData = new FormData();
+
+ // Append base data from main form
+ formData.append('active', this.user.active ? 'true' : 'false');
+ formData.append('twofactorrequired', this.user.twofactorrequired ? 'true' : 'false');
+ for (const key in this.user.permissions) {
+ if (this.user.permissions[key]) {
+ if (key.startsWith('can')) {
+ formData.append(`can[${key.replace('can', '')}]`, 'true');
+ } else {
+ formData.append(key, 'true');
+ }
+ }
+ }
+ const baseFields = ['address_id', 'employee_number', 'project_api_key', 'vodia_identity_domain', 'vodia_identity_username', 'vodia_identity_default'];
+ baseFields.forEach(field => formData.append(field, this.user[field] || ''));
+ (this.user.preorder_networks || []).forEach(val => formData.append('preorder_networks[]', val));
+ (this.user.constructionconsent_projects || []).forEach(val => formData.append('constructionconsent_projects[]', val));
+
+ // Append user-specific data from loop
+ formData.append('username', user.username);
+ formData.append('name', user.name);
+ formData.append('email', user.email);
+ formData.append('mobile', user.mobile || '');
+ formData.append('password', password);
+ formData.append('password2', password);
+
+ try {
+ const response = await axios.post(window.TT_CONFIG.SAVE_URL, formData);
+ const wasRedirected = response.request.responseURL && !response.request.responseURL.endsWith(window.TT_CONFIG.SAVE_URL);
+
+ if (response.status === 200 && wasRedirected) {
+ createdUsersForCSV.push({ ...user, password: password });
+ } else {
+ const errorMsg = response.data.errors ? Object.values(response.data.errors).join(', ') : (response.data.message || 'Unbekannter Serverfehler');
+ failedUsers.push({ ...user, error: errorMsg });
+ }
+ } catch (error) {
+ const errorMsg = error.response?.data?.errors ? Object.values(error.response.data.errors).join(', ') : (error.response?.data?.message || error.message);
+ failedUsers.push({ ...user, error: errorMsg });
+ }
+ }
+
+ this.isBulkSaving = false;
+
+ if (createdUsersForCSV.length > 0) {
+ this.downloadCSV(createdUsersForCSV);
+ window.notify('success', `${createdUsersForCSV.length} von ${this.bulkTotalRows} Benutzern erfolgreich erstellt.`);
+ }
+
+ if (failedUsers.length > 0) {
+ let errorMessage = `${failedUsers.length} Benutzer konnten nicht erstellt werden:\n` + failedUsers.map(u => `- ${u.username}: ${u.error}`).join('\n');
+ alert(errorMessage);
+ }
+
+ if (createdUsersForCSV.length > 0 && failedUsers.length === 0) {
+ this.showBulkCreateModal = false;
+ setTimeout(() => window.location.href = '/User', 500);
+ }
+ },
+ downloadCSV(data) {
+ if (!data || data.length === 0) return;
+
+ const headers = ['Username', 'Password', 'Name', 'Email'];
+ const rows = data.map(user => [
+ `"${user.username}"`, `"${user.password}"`, `"${user.name}"`, `"${user.email}"`
+ ]);
+
+ let csvContent = headers.join(";") + "\n" + rows.map(e => e.join(";")).join("\n");
+
+ const blob = new Blob([`\uFEFF${csvContent}`], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement("a");
+ const url = URL.createObjectURL(blob);
+ link.setAttribute("href", url);
+ link.setAttribute("download", "new_users_credentials.csv");
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
}
},
created() {