Files
yourpart3/frontend/src/views/admin/falukant/EditUserView.vue
Torsten Schulz (local) 061bd4e7f2 refactor(AdminService, EditUserView): streamline character data deletion and enhance confirmation dialog
- Introduced a reusable `deleteIfTableExists` function in AdminService to simplify the deletion of character-related data across multiple tables, improving code maintainability.
- Updated the deletion logic to check for table existence before executing delete queries, enhancing robustness.
- Enhanced the EditUserView by integrating a `ChooseDialog` for user confirmation before proceeding with character death cleanup, improving user experience and interaction clarity.
2026-04-20 16:16:48 +02:00

975 lines
32 KiB
Vue

<template>
<div class="edit-user-view">
<h1>{{ $t('admin.falukant.edituser.title') }}</h1>
<!-- Benutzer-Suche -->
<div class="search-section">
<label class="form-field">{{ $t('admin.falukant.edituser.username') }} <input type="text" v-model="user.username" @keyup.enter="searchUser" /></label>
<label class="form-field">{{ $t('admin.falukant.edituser.characterName') }} <input type="text" v-model="user.characterName" @keyup.enter="searchUser" /></label>
<button @click="searchUser" :disabled="!canSearch">{{ $t('admin.falukant.edituser.search') }}</button>
</div>
<!-- Benutzer-Liste -->
<ul v-for="user in users" class="user-list">
<li @click="selectUser(user)">{{ user.username }} ({{ user.falukantUser[0].character.definedFirstName.name }} {{
user.falukantUser[0].character.definedLastName.name }})</li>
</ul>
<!-- Tab-System für ausgewählten Benutzer -->
<div v-if="editableUser" class="user-edit-section">
<h2>{{ $t('admin.falukant.edituser.user') }}: {{ editableUser.username }}</h2>
<h3>{{ $t('admin.falukant.edituser.characterName') }}: {{ editableUser.falukantData[0].character.definedFirstName.name }} {{
editableUser.falukantData[0].character.definedLastName.name }}</h3>
<SimpleTabs v-model="activeTab" :tabs="tabs" @change="onTabChange" />
<!-- Tab-Inhalt -->
<div class="tab-content">
<!-- Userdaten Tab -->
<div v-if="activeTab === 'userdata'" class="tab-pane">
<div class="edit-form">
<label>{{ $t('falukant.overview.metadata.money') }}: <input type="number" v-model="editableUser.falukantData[0].money" /></label>
<label>{{ $t('falukant.overview.metadata.age') }}: <input type="number" v-model="age" /></label>
<label>{{ $t('falukant.overview.metadata.nobleTitle') }}:
<select v-model="editableUser.falukantData[0].character.title_of_nobility">
<option v-for="title in titles" :value="title.id">{{ $t(`falukant.titles.male.${title.labelTr}`) }}</option>
</select>
</label>
<label>{{ $t('falukant.house.title') }}: <select v-model="editableUser.falukantData[0].house">
<option v-for="house in houses" :value="house.id">{{ $t(`falukant.house.type.${house.labelTr}`) }}</option>
</select>
</label>
<div class="admin-family-tools">
<h4>{{ $t('admin.falukant.edituser.pregnancy.title') }}</h4>
<p class="admin-family-tools__meta">
{{ $t('admin.falukant.edituser.pregnancy.characterId') }}:
<code>{{ editableUser.falukantData[0].character.id }}</code>
</p>
<p class="admin-family-tools__meta">
{{ $t('admin.falukant.edituser.pregnancy.status') }}:
<template v-if="pregnancyDueDisplay">
{{ $t('admin.falukant.edituser.pregnancy.statusActive') }} {{ pregnancyDueDisplay }}
</template>
<template v-else>{{ $t('admin.falukant.edituser.pregnancy.statusNone') }}</template>
</p>
<label class="form-field">
{{ $t('admin.falukant.edituser.pregnancy.fatherSelect') }}
<select v-if="potentialFathers.length" v-model="adminPregnancyFatherSelect">
<option value="">{{ $t('admin.falukant.edituser.pregnancy.fatherNone') }}</option>
<option v-for="p in potentialFathers" :key="p.characterId" :value="String(p.characterId)">
{{ p.displayName }} {{ fatherRelationshipLabel(p.relationshipType) }}
</option>
</select>
<input
v-else
type="number"
v-model.number="adminPregnancyFatherManualId"
min="1"
:placeholder="$t('admin.falukant.edituser.pregnancy.fatherManualPlaceholder')"
/>
</label>
<p v-if="potentialFathers.length" class="admin-family-tools__hint">{{ $t('admin.falukant.edituser.pregnancy.fatherHintList') }}</p>
<p v-else class="admin-family-tools__hint">{{ $t('admin.falukant.edituser.pregnancy.fatherHintManual') }}</p>
<label class="form-field">
{{ $t('admin.falukant.edituser.pregnancy.dueDays') }}
<input type="number" v-model.number="adminDueInDays" min="0" max="365" />
</label>
<p class="admin-family-tools__hint">{{ $t('admin.falukant.edituser.pregnancy.dueDaysHint') }}</p>
<div class="action-buttons">
<button type="button" @click="adminForcePregnancy">{{ $t('admin.falukant.edituser.pregnancy.force') }}</button>
<button type="button" class="button-secondary" @click="adminClearPregnancy">{{ $t('admin.falukant.edituser.pregnancy.clear') }}</button>
</div>
<h4>{{ $t('admin.falukant.edituser.birth.title') }}</h4>
<p class="admin-family-tools__hint">{{ $t('admin.falukant.edituser.birth.motherHint') }}</p>
<label class="form-field">
{{ $t('admin.falukant.edituser.birth.fatherSelect') }} *
<select v-if="potentialFathers.length" v-model="adminBirthFatherSelect">
<option value="">{{ $t('admin.falukant.edituser.birth.fatherChoose') }}</option>
<option v-for="p in potentialFathers" :key="'b-' + p.characterId" :value="String(p.characterId)">
{{ p.displayName }} {{ fatherRelationshipLabel(p.relationshipType) }}
</option>
</select>
<input
v-else
type="number"
v-model.number="adminBirthFatherManualId"
min="1"
required
:placeholder="$t('admin.falukant.edituser.pregnancy.fatherManualPlaceholder')"
/>
</label>
<p v-if="potentialFathers.length" class="admin-family-tools__hint">{{ $t('admin.falukant.edituser.birth.fatherHintList') }}</p>
<p v-else class="admin-family-tools__hint">{{ $t('admin.falukant.edituser.birth.fatherHintManual') }}</p>
<label class="form-field">
{{ $t('admin.falukant.edituser.birth.context') }}
<select v-model="adminBirthContext">
<option value="marriage">{{ $t('admin.falukant.edituser.birth.contextMarriage') }}</option>
<option value="lover">{{ $t('admin.falukant.edituser.birth.contextLover') }}</option>
</select>
</label>
<label class="form-field">
{{ $t('admin.falukant.edituser.birth.legitimacy') }}
<select v-model="adminBirthLegitimacy">
<option value="legitimate">{{ $t('admin.falukant.edituser.birth.legitimate') }}</option>
<option value="acknowledged_bastard">{{ $t('admin.falukant.edituser.birth.ackBastard') }}</option>
<option value="hidden_bastard">{{ $t('admin.falukant.edituser.birth.hiddenBastard') }}</option>
</select>
</label>
<label class="form-field">
{{ $t('admin.falukant.edituser.birth.gender') }}
<select v-model="adminBirthGender">
<option value="">{{ $t('admin.falukant.edituser.birth.genderRandom') }}</option>
<option value="male">{{ $t('admin.falukant.edituser.birth.male') }}</option>
<option value="female">{{ $t('admin.falukant.edituser.birth.female') }}</option>
</select>
</label>
<div class="action-buttons">
<button type="button" @click="adminForceBirth">{{ $t('admin.falukant.edituser.birth.force') }}</button>
</div>
<h4>{{ $t('admin.falukant.edituser.deathCleanup.title') }}</h4>
<p class="admin-family-tools__hint">{{ $t('admin.falukant.edituser.deathCleanup.hint') }}</p>
<div class="action-buttons">
<button type="button" class="button-secondary" @click="adminCleanupDeathArtifacts">
{{ $t('admin.falukant.edituser.deathCleanup.action') }}
</button>
</div>
</div>
<div class="action-buttons">
<button @click="saveUser" :disabled="!hasUserChanges">{{ $t('common.save') }}</button>
<button @click="deleteUser" class="button-secondary">{{ $t('common.delete') }}</button>
</div>
</div>
</div>
<!-- Niederlassungen Tab -->
<div v-else-if="activeTab === 'branches'" class="tab-pane">
<div v-if="loading.branches" class="loading">{{ $t('loading') }}</div>
<div v-else class="branches-section">
<h3>{{ $t('admin.falukant.edituser.branches.title') }}</h3>
<div v-for="branch in userBranches" :key="branch.id" class="branch-card">
<h4>{{ branch.region.name }} - {{ $t(`falukant.branch.types.${branch.branchType.labelTr}`) }}</h4>
<!-- Lager-Übersicht -->
<div class="warehouse-section">
<h5>{{ $t('falukant.branch.storage.title') }}</h5>
<div v-if="branch.stocks && branch.stocks.length" class="stock-list">
<div v-for="stock in branch.stocks" :key="stock.id" class="stock-item">
<span class="stock-name">{{ $t(`falukant.branch.stocktype.${stock.stockType.labelTr}`) }}</span>
<input type="number" v-model="stock.quantity" class="stock-quantity" />
<button @click="updateStock(stock)">{{ $t('common.update') }}</button>
</div>
</div>
<div v-else class="no-stocks">
{{ $t('admin.falukant.edituser.branches.noStocks') }}
</div>
<div v-if="canAddStock(branch)" class="add-stock-section">
<button @click="openAddStockDialog(branch.id)" class="add-stock-btn">
{{ $t('admin.falukant.edituser.branches.addStock') }}
</button>
</div>
<div v-else-if="branch.stocks && branch.stocks.length > 0" class="all-stocks-added">
{{ $t('admin.falukant.edituser.branches.allStocksAdded') }}
</div>
</div>
</div>
<div v-if="!userBranches.length" class="no-branches">
{{ $t('admin.falukant.edituser.branches.noBranches') }}
</div>
</div>
</div>
</div>
</div>
<!-- Add Stock Dialog -->
<div v-if="showAddStockDialog" class="dialog-overlay" @click="closeAddStockDialog">
<div class="dialog" @click.stop>
<h3>{{ $t('admin.falukant.edituser.branches.addStock') }}</h3>
<div class="dialog-content">
<div class="form-group">
<label>{{ $t('admin.falukant.edituser.branches.stockType') }}:</label>
<select v-model="newStock.stockTypeId" class="form-select">
<option value="">{{ $t('admin.falukant.edituser.branches.selectStockType') }}</option>
<option v-for="stockType in availableStockTypes" :key="stockType.id" :value="stockType.id">
{{ $t(`falukant.branch.stocktype.${stockType.labelTr}`) }}
</option>
</select>
</div>
<div class="form-group">
<label>{{ $t('admin.falukant.edituser.branches.quantity') }}:</label>
<input type="number" v-model="newStock.quantity" min="1" class="form-input" />
</div>
</div>
<div class="dialog-actions">
<button @click="closeAddStockDialog" class="btn btn-secondary">
{{ $t('common.cancel') }}
</button>
<button @click="addStock" class="btn btn-primary">
{{ $t('common.add') }}
</button>
</div>
</div>
</div>
<ChooseDialog ref="chooseDialog" />
</div>
</template>
<script lang="js">
import { mapState } from 'vuex';
import { mapActions } from 'vuex';
import SimpleTabs from '@/components/SimpleTabs.vue';
import ChooseDialog from '@/dialogues/standard/ChooseDialog.vue';
import apiClient from '@/utils/axios.js';
import { showApiError, showError, showSuccess } from '@/utils/feedback.js';
export default {
name: 'AdminFalukantEditUserView',
components: {
SimpleTabs,
ChooseDialog
},
data() {
return {
user: {
username: '',
characterName: ''
},
users: [],
editableUser: null,
age: null,
originalAge: null,
originalUser: null,
titles: [],
houses: [],
activeTab: 'userdata',
tabs: [
{ value: 'userdata', label: 'admin.falukant.edituser.tabs.userdata' },
{ value: 'branches', label: 'admin.falukant.edituser.tabs.branches' }
],
userBranches: [],
stockTypes: [],
newStock: {
branchId: null,
stockTypeId: null,
quantity: 0
},
showAddStockDialog: false,
loading: {
branches: false,
stockTypes: false
},
potentialFathers: [],
adminPregnancyFatherSelect: '',
adminPregnancyFatherManualId: null,
adminDueInDays: 21,
adminBirthFatherSelect: '',
adminBirthFatherManualId: null,
adminBirthContext: 'marriage',
adminBirthLegitimacy: 'legitimate',
adminBirthGender: ''
}
},
computed: {
...mapState('falukant', ['user']),
canSearch() {
return this.user.username.trim().length > 0 || this.user.characterName.trim().length > 0;
},
hasUserChanges() {
if (!this.editableUser || !this.originalUser) return false;
return this.editableUser.falukantData[0].money != this.originalUser.falukantData[0].money
|| this.editableUser.falukantData[0].character.title_of_nobility != this.originalUser.falukantData[0].character.title_of_nobility
|| this.originalAge != this.age;
},
pregnancyDueDisplay() {
const c = this.editableUser?.falukantData?.[0]?.character;
if (!c) return null;
const raw = c.pregnancyDueAt ?? c.pregnancy_due_at;
if (!raw) return null;
try {
return new Date(raw).toLocaleString(this.$i18n?.locale || undefined, {
dateStyle: 'medium',
timeStyle: 'short'
});
} catch (_) {
return String(raw);
}
},
availableStockTypes() {
if (!this.newStock.branchId || !this.stockTypes.length) {
return this.stockTypes;
}
// Finde den aktuellen Branch
const currentBranch = this.userBranches.find(branch => branch.id === this.newStock.branchId);
if (!currentBranch || !currentBranch.stocks) {
return this.stockTypes;
}
// Erstelle eine Liste der bereits vorhandenen Stock-Type-IDs für diesen Branch
const existingStockTypeIds = currentBranch.stocks.map(stock => stock.stockTypeId);
// Filtere die Stock-Types, die noch nicht in diesem Branch vorhanden sind
return this.stockTypes.filter(stockType =>
!existingStockTypeIds.includes(stockType.id)
);
}
},
async mounted() {
const titlesResult = await apiClient.get('/api/falukant/nobility/titels');
this.titles = titlesResult.data;
// Lade Stock-Types
await this.loadStockTypes();
},
methods: {
async searchUser() {
if (!this.canSearch) {
showError(this, 'Bitte Benutzername oder Charaktername eingeben.');
return;
}
const userResult = await apiClient.post('/api/admin/falukant/searchuser', {
userName: this.user.username,
characterName: this.user.characterName
});
this.users = userResult.data;
},
async selectUser(user) {
const userResult = await apiClient.get(`/api/admin/falukant/getuser/${user.id}`);
this.editableUser = userResult.data;
this.originalUser = JSON.parse(JSON.stringify(this.editableUser));
this.age = Math.floor((Date.now() - new Date(this.editableUser.falukantData[0].character.birthdate)) / (24 * 60 * 60 * 1000));
this.originalAge = this.age;
this.users = [];
this.activeTab = 'userdata';
this.adminPregnancyFatherSelect = '';
this.adminPregnancyFatherManualId = null;
this.adminBirthFatherSelect = '';
this.adminBirthFatherManualId = null;
await this.loadPotentialFathers();
this.syncFatherSelectionsFromCharacter();
},
async saveUser() {
const dataToChange = {
id: this.editableUser.falukantData[0].id,
};
if (this.editableUser.falukantData[0].money != this.originalUser.falukantData[0].money) {
dataToChange.money = this.editableUser.falukantData[0].money;
}
if (this.editableUser.falukantData[0].character.title_of_nobility != this.originalUser.falukantData[0].character.title_of_nobility) {
dataToChange.title_of_nobility = this.editableUser.falukantData[0].character.title_of_nobility;
}
if (this.originalAge != this.age) {
dataToChange.age = this.age;
}
try {
await apiClient.post(`/api/admin/falukant/edituser`, dataToChange);
showSuccess(this, 'tr:admin.falukant.edituser.success');
} catch (error) {
showApiError(this, error, 'tr:admin.falukant.edituser.error');
}
},
async deleteUser() {
const dataToChange = {
id: this.editableUser.falukantData[0].id,
};
},
onTabChange(tab) {
if (tab === 'branches' && this.editableUser && !this.userBranches.length) {
this.loadUserBranches();
}
},
async loadUserBranches() {
if (!this.editableUser) return;
this.loading.branches = true;
try {
const branchesResult = await apiClient.get(`/api/admin/falukant/branches/${this.editableUser.falukantData[0].id}`);
this.userBranches = branchesResult.data;
} catch (error) {
console.error('Error loading user branches:', error);
showApiError(this, error, 'tr:admin.falukant.edituser.errorLoadingBranches');
} finally {
this.loading.branches = false;
}
},
async updateStock(stock) {
try {
await apiClient.put(`/api/admin/falukant/stock/${stock.id}`, {
quantity: stock.quantity
});
showSuccess(this, 'tr:admin.falukant.edituser.stockUpdated');
} catch (error) {
console.error('Error updating stock:', error);
this.$root.$refs.errorDialog.open('tr:admin.falukant.edituser.errorUpdatingStock');
}
},
async loadStockTypes() {
this.loading.stockTypes = true;
try {
const stockTypesResult = await apiClient.get('/api/admin/falukant/stock-types');
this.stockTypes = stockTypesResult.data;
} catch (error) {
console.error('Error loading stock types:', error);
this.$root.$refs.errorDialog.open('tr:admin.falukant.edituser.errorLoadingStockTypes');
} finally {
this.loading.stockTypes = false;
}
},
openAddStockDialog(branchId) {
this.newStock.branchId = branchId;
this.newStock.stockTypeId = null;
this.newStock.quantity = 0;
this.showAddStockDialog = true;
},
closeAddStockDialog() {
this.showAddStockDialog = false;
this.newStock = {
branchId: null,
stockTypeId: null,
quantity: 0
};
},
async addStock() {
if (!this.newStock.stockTypeId || this.newStock.quantity <= 0) {
this.$root.$refs.errorDialog.open('tr:admin.falukant.edituser.invalidStockData');
return;
}
try {
await apiClient.post('/api/admin/falukant/stock', {
branchId: this.newStock.branchId,
stockTypeId: this.newStock.stockTypeId,
quantity: this.newStock.quantity
});
this.$root.$refs.messageDialog.open('tr:admin.falukant.edituser.stockAdded');
this.closeAddStockDialog();
// Lade Branches neu um den neuen Stock anzuzeigen
await this.loadUserBranches();
} catch (error) {
console.error('Error adding stock:', error);
this.$root.$refs.errorDialog.open('tr:admin.falukant.edituser.errorAddingStock');
}
},
canAddStock(branch) {
// Wenn keine Stock-Types geladen sind, zeige den Button nicht
if (!this.stockTypes || this.stockTypes.length === 0) {
return false;
}
// Wenn keine Stocks vorhanden sind, kann immer hinzugefügt werden
if (!branch.stocks || branch.stocks.length === 0) {
return true;
}
// Erstelle eine Liste der bereits vorhandenen Stock-Type-IDs für diesen Branch
const existingStockTypeIds = branch.stocks.map(stock => stock.stockTypeId);
// Prüfe ob es noch Stock-Types gibt, die nicht in diesem Branch vorhanden sind
const availableStockTypes = this.stockTypes.filter(stockType =>
!existingStockTypeIds.includes(stockType.id)
);
return availableStockTypes.length > 0;
},
async refreshEditableUser() {
if (!this.editableUser?.hashedId) return;
const userResult = await apiClient.get(`/api/admin/falukant/getuser/${this.editableUser.hashedId}`);
this.editableUser = userResult.data;
this.originalUser = JSON.parse(JSON.stringify(this.editableUser));
this.age = Math.floor((Date.now() - new Date(this.editableUser.falukantData[0].character.birthdate)) / (24 * 60 * 60 * 1000));
this.originalAge = this.age;
await this.loadPotentialFathers();
this.syncFatherSelectionsFromCharacter();
},
fatherRelationshipLabel(relationshipType) {
const key = `admin.falukant.edituser.pregnancy.relationship.${relationshipType}`;
const t = this.$t(key);
return t === key ? relationshipType : t;
},
async loadPotentialFathers() {
const cid = this.editableUser?.falukantData?.[0]?.character?.id;
if (!cid) {
this.potentialFathers = [];
return;
}
try {
const r = await apiClient.get(`/api/admin/falukant/character/${cid}/potential-fathers`);
this.potentialFathers = Array.isArray(r.data?.options) ? r.data.options : [];
} catch (e) {
console.error('loadPotentialFathers', e);
this.potentialFathers = [];
}
},
syncFatherSelectionsFromCharacter() {
const c = this.editableUser?.falukantData?.[0]?.character;
if (!c) return;
const pf = c.pregnancyFatherCharacterId ?? c.pregnancy_father_character_id;
if (pf != null && this.potentialFathers.some((x) => x.characterId === Number(pf))) {
this.adminPregnancyFatherSelect = String(pf);
}
},
resolvePregnancyFatherCharacterId() {
if (this.potentialFathers.length) {
return this.adminPregnancyFatherSelect ? Number(this.adminPregnancyFatherSelect) : null;
}
return this.adminPregnancyFatherManualId ? Number(this.adminPregnancyFatherManualId) : null;
},
resolveBirthFatherCharacterId() {
if (this.potentialFathers.length) {
return this.adminBirthFatherSelect ? Number(this.adminBirthFatherSelect) : null;
}
return this.adminBirthFatherManualId ? Number(this.adminBirthFatherManualId) : null;
},
resolveDueInDays() {
const raw = this.adminDueInDays;
if (raw === '' || raw === null || raw === undefined) {
return 21;
}
const n = Number(raw);
return Number.isFinite(n) ? n : 21;
},
async adminForcePregnancy() {
const characterId = this.editableUser.falukantData[0].character.id;
const payload = { characterId, dueInDays: this.resolveDueInDays() };
const fatherId = this.resolvePregnancyFatherCharacterId();
if (fatherId != null) payload.fatherCharacterId = fatherId;
try {
await apiClient.post('/api/admin/falukant/character/force-pregnancy', payload);
showSuccess(this, 'tr:admin.falukant.edituser.pregnancy.successForce');
await this.refreshEditableUser();
} catch (error) {
showApiError(this, error, 'tr:admin.falukant.edituser.pregnancy.error');
}
},
async adminClearPregnancy() {
const characterId = this.editableUser.falukantData[0].character.id;
try {
await apiClient.post('/api/admin/falukant/character/clear-pregnancy', { characterId });
showSuccess(this, 'tr:admin.falukant.edituser.pregnancy.successClear');
await this.refreshEditableUser();
} catch (error) {
showApiError(this, error, 'tr:admin.falukant.edituser.pregnancy.error');
}
},
async adminForceBirth() {
const motherCharacterId = this.editableUser.falukantData[0].character.id;
const fatherCharacterId = this.resolveBirthFatherCharacterId();
if (fatherCharacterId == null || Number.isNaN(fatherCharacterId)) {
showError(this, this.$t('admin.falukant.edituser.birth.fatherRequired'));
return;
}
const body = {
motherCharacterId,
fatherCharacterId,
birthContext: this.adminBirthContext,
legitimacy: this.adminBirthLegitimacy
};
if (this.adminBirthGender === 'male' || this.adminBirthGender === 'female') {
body.gender = this.adminBirthGender;
}
try {
await apiClient.post('/api/admin/falukant/character/force-birth', body);
showSuccess(this, 'tr:admin.falukant.edituser.birth.success');
await this.refreshEditableUser();
} catch (error) {
showApiError(this, error, 'tr:admin.falukant.edituser.birth.error');
}
},
async adminCleanupDeathArtifacts() {
const characterId = this.editableUser?.falukantData?.[0]?.character?.id;
if (!characterId) {
return;
}
const confirmed = await this.$refs.chooseDialog.open({
title: this.$t('common.confirm'),
message: this.$t('admin.falukant.edituser.deathCleanup.confirm')
});
if (!confirmed) {
return;
}
try {
await apiClient.post(`/api/admin/falukant/character/${characterId}/death-cleanup`);
showSuccess(this, 'tr:admin.falukant.edituser.deathCleanup.success');
await this.refreshEditableUser();
} catch (error) {
showApiError(this, error, 'tr:admin.falukant.edituser.deathCleanup.error');
}
}
}
}
</script>
<style lang="scss" scoped>
.edit-user-view {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
height: 100%;
overflow-y: auto;
}
.admin-family-tools {
margin-top: 1.25rem;
padding: 1rem 1rem 0.5rem;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 8px;
background: #fafafa;
}
.admin-family-tools h4 {
margin: 0.75rem 0 0.5rem;
font-size: 1rem;
}
.admin-family-tools h4:first-child {
margin-top: 0;
}
.admin-family-tools__meta {
margin: 0.35rem 0;
font-size: 0.9rem;
}
.admin-family-tools__hint {
margin: 0 0 0.75rem;
font-size: 0.85rem;
color: #666;
}
.admin-family-tools .form-field {
display: block;
margin-bottom: 0.5rem;
}
.admin-family-tools .action-buttons {
margin-bottom: 1rem;
}
.search-section {
margin-bottom: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 5px;
}
.search-section label {
display: inline-block;
margin-right: 15px;
margin-bottom: 10px;
}
.search-section input {
margin-left: 5px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
.search-section button {
padding: 8px 15px;
background: #1976d2;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.user-list {
list-style: none;
padding: 0;
margin: 20px 0;
}
.user-list > li {
cursor: pointer;
color: #0066ff;
padding: 8px;
border: 1px solid #ddd;
margin-bottom: 5px;
border-radius: 3px;
background: #f9f9f9;
}
.user-list > li:hover {
background: #e9e9e9;
}
.user-edit-section {
margin-top: 30px;
border: 1px solid #ddd;
border-radius: 5px;
padding: 20px;
background: white;
max-height: calc(100vh - 200px);
overflow-y: auto;
}
.user-edit-section h2, .user-edit-section h3 {
margin-top: 0;
color: #333;
}
.tab-content {
margin-top: 20px;
max-height: calc(100vh - 300px);
overflow-y: auto;
}
.tab-pane {
padding: 20px 0;
min-height: 400px;
}
.edit-form label {
display: block;
margin-bottom: 15px;
}
.edit-form input, .edit-form select {
margin-left: 10px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
.action-buttons {
margin-top: 20px;
}
.action-buttons button {
margin-right: 10px;
padding: 8px 15px;
border: none;
border-radius: 3px;
cursor: pointer;
}
.action-buttons button:first-child {
background: #4caf50;
color: white;
}
.action-buttons button:last-child {
background: #f44336;
color: white;
}
.branches-section {
max-height: calc(100vh - 400px);
overflow-y: auto;
}
.branches-section h3 {
margin-bottom: 20px;
color: #333;
position: sticky;
top: 0;
background: white;
padding: 10px 0;
z-index: 1;
}
.branch-card {
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
margin-bottom: 20px;
background: #f9f9f9;
}
.branch-card h4 {
margin-top: 0;
color: #1976d2;
}
.warehouse-section {
margin-top: 15px;
}
.warehouse-section h5 {
margin-bottom: 10px;
color: #666;
}
.stock-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.stock-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
background: white;
border: 1px solid #ddd;
border-radius: 3px;
}
.stock-name {
flex: 1;
font-weight: bold;
}
.stock-quantity {
width: 80px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
.stock-item button {
padding: 5px 10px;
background: #4caf50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.no-stocks, .no-branches {
text-align: center;
color: #666;
font-style: italic;
padding: 20px;
background: #f5f5f5;
border-radius: 3px;
}
.loading {
text-align: center;
color: #666;
font-style: italic;
padding: 20px;
}
/* Add Stock Dialog Styles */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
background: white;
border-radius: 8px;
padding: 20px;
min-width: 400px;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.dialog h3 {
margin: 0 0 20px 0;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.dialog-content {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
.form-select, .form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-select:focus, .form-input:focus {
outline: none;
border-color: #1976d2;
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.2);
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.btn-primary {
background: #1976d2;
color: white;
}
.btn-primary:hover {
background: #1565c0;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
/* Add Stock Button */
.add-stock-section {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
}
.add-stock-btn {
background: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.add-stock-btn:hover {
background: #218838;
}
.all-stocks-added {
margin-top: 15px;
padding: 10px;
background: #e8f5e8;
border: 1px solid #c3e6c3;
border-radius: 4px;
color: #2d5a2d;
font-size: 14px;
text-align: center;
}
</style>