feat(admin): implement pregnancy and birth management features
Some checks failed
Deploy to production / deploy (push) Failing after 2m6s

- Added new admin functionalities to force pregnancy, clear pregnancy, and trigger birth for characters.
- Introduced corresponding routes and controller methods in adminRouter and adminController.
- Enhanced the FalukantCharacter model to include pregnancy-related fields.
- Created database migration for adding pregnancy columns to the character table.
- Updated frontend views and internationalization files to support new pregnancy and birth management features.
- Improved user feedback and error handling for these new actions.
This commit is contained in:
Torsten Schulz (local)
2026-03-30 13:44:43 +02:00
parent b2591da428
commit c52d4b60f9
18 changed files with 628 additions and 160 deletions

View File

@@ -39,6 +39,67 @@
<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.fatherId') }}
<input type="number" v-model.number="adminPregnancyFatherId" min="1" placeholder="—" />
</label>
<label class="form-field">
{{ $t('admin.falukant.edituser.pregnancy.dueDays') }}
<input type="number" v-model.number="adminDueInDays" min="1" max="365" />
</label>
<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.fatherId') }} *
<input type="number" v-model.number="adminBirthFatherId" min="1" required />
</label>
<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>
</div>
<div class="action-buttons">
<button @click="saveUser" :disabled="!hasUserChanges">{{ $t('common.save') }}</button>
<button @click="deleteUser" class="button-secondary">{{ $t('common.delete') }}</button>
@@ -158,7 +219,13 @@ export default {
loading: {
branches: false,
stockTypes: false
}
},
adminPregnancyFatherId: null,
adminDueInDays: 21,
adminBirthFatherId: null,
adminBirthContext: 'marriage',
adminBirthLegitimacy: 'legitimate',
adminBirthGender: ''
}
},
computed: {
@@ -172,6 +239,20 @@ export default {
|| 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;
@@ -343,6 +424,59 @@ export default {
!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;
},
async adminForcePregnancy() {
const characterId = this.editableUser.falukantData[0].character.id;
const payload = { characterId, dueInDays: Number(this.adminDueInDays) || 21 };
if (this.adminPregnancyFatherId) payload.fatherCharacterId = Number(this.adminPregnancyFatherId);
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;
if (!this.adminBirthFatherId) {
showError(this, this.$t('admin.falukant.edituser.birth.fatherId'));
return;
}
const body = {
motherCharacterId,
fatherCharacterId: Number(this.adminBirthFatherId),
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');
}
}
}
}
@@ -357,6 +491,43 @@ export default {
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;

View File

@@ -11,6 +11,14 @@
</div>
</section>
<section
v-if="pregnancy"
class="family-pregnancy surface-card"
>
<strong>{{ $t('falukant.family.pregnancy.banner') }}</strong>
<p>{{ $t('falukant.family.pregnancy.dueHint') }}: {{ formatPregnancyDue(pregnancy.dueAt) }}</p>
</section>
<section
v-if="debtorsPrison.active"
class="family-debt-warning surface-card"
@@ -407,6 +415,7 @@ export default {
active: false,
inDebtorsPrison: false
},
pregnancy: null,
selectedChild: null,
pendingFamilyRefresh: null
}
@@ -563,11 +572,24 @@ export default {
active: false,
inDebtorsPrison: false
};
this.pregnancy = response.data.pregnancy || null;
} catch (error) {
console.error('Error loading family data:', error);
}
},
formatPregnancyDue(iso) {
if (!iso) return '—';
try {
return new Date(iso).toLocaleString(this.$i18n?.locale || undefined, {
dateStyle: 'medium',
timeStyle: 'short',
});
} catch (_) {
return String(iso);
}
},
async spendTimeWithSpouse() {
try {
await apiClient.post('/api/falukant/family/marriage/spend-time');
@@ -904,6 +926,18 @@ export default {
color: var(--color-text-secondary);
}
.family-pregnancy {
margin-bottom: 16px;
padding: 16px 18px;
border: 1px solid rgba(120, 140, 200, 0.35);
background: linear-gradient(180deg, rgba(235, 240, 255, 0.95), rgba(248, 250, 255, 0.98));
}
.family-pregnancy p {
margin: 6px 0 0;
color: var(--color-text-secondary);
}
.family-debt-warning {
margin-bottom: 16px;
padding: 16px 18px;