added bulk creation and e-mail sending
This commit is contained in:
@@ -5,9 +5,12 @@ Vue.component("UserEdit", {
|
||||
<tt-loader v-if="isSaving"/>
|
||||
<div class="user-edit-header sticky-header">
|
||||
<h3>Allgemeine Informationen</h3>
|
||||
<tt-tooltip :text="permissionChangesTooltip" position="left">
|
||||
<tt-button text="Speichern" icon="fas fa-save" additional-class="btn-primary" @click="saveUser" :loading="isSaving"/>
|
||||
</tt-tooltip>
|
||||
<div class="header-actions">
|
||||
<tt-button v-if="isNewUser" text="Mehrere User erstellen" icon="fas fa-users" additional-class="btn-info" @click="showBulkCreateModal = true"/>
|
||||
<tt-tooltip :text="permissionChangesTooltip" position="left">
|
||||
<tt-button text="Speichern" icon="fas fa-save" additional-class="btn-primary" @click="saveUser" :loading="isSaving"/>
|
||||
</tt-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-form-grid">
|
||||
<tt-input label="Username" v-model="user.username" sm :disabled="!isNewUser"/>
|
||||
@@ -94,7 +97,7 @@ Vue.component("UserEdit", {
|
||||
<tt-card>
|
||||
<div class="password-generation-grid">
|
||||
<tt-input label="Neues Passwort" v-model="password.new" :type="passwordFieldType" sm/>
|
||||
<tt-button icon="fas fa-sync-alt" @click="generatePassword" additional-class="btn-outline-secondary" sm title="Passwort generieren"/>
|
||||
<tt-button icon="fas fa-sync-alt" @click="generatePassword()" additional-class="btn-outline-secondary" sm title="Passwort generieren"/>
|
||||
<tt-button :icon="passwordFieldType === 'password' ? 'fas fa-eye' : 'fas fa-eye-slash'" @click="togglePasswordVisibility" additional-class="btn-outline-secondary" sm title="Passwort anzeigen"/>
|
||||
</div>
|
||||
<tt-input label="Passwort wiederholen" v-model="password.repeat" type="password" sm/>
|
||||
@@ -116,6 +119,32 @@ Vue.component("UserEdit", {
|
||||
</tt-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<tt-modal
|
||||
v-if="showBulkCreateModal"
|
||||
:show.sync="showBulkCreateModal"
|
||||
title="Mehrere Benutzer erstellen"
|
||||
save-text="Erstellen & CSV herunterladen"
|
||||
:delete="false"
|
||||
:save-loading="isBulkSaving"
|
||||
@submit="handleBulkCreate"
|
||||
>
|
||||
<div v-if="isBulkSaving" class="loading-overlay mt-4">
|
||||
<div class="progress" style="height: 30px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
:style="{ width: bulkProgress + '%' }">
|
||||
{{ Math.round(bulkProgress) }}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-2">Benutzer {{ bulkCurrentRow + 1 }} von {{ bulkTotalRows }} wird erstellt...</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="alert alert-info">
|
||||
<strong>Hinweis:</strong> 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.
|
||||
</p>
|
||||
<tt-positions-manager v-model="bulkUsers" :config="bulkCreateConfig" />
|
||||
</div>
|
||||
</tt-modal>
|
||||
</tt-card>
|
||||
`,
|
||||
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('<br>') : 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('<br>') : (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() {
|
||||
|
||||
Reference in New Issue
Block a user