diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js index 5369c37..f6919cf 100644 --- a/backend/controllers/adminController.js +++ b/backend/controllers/adminController.js @@ -13,6 +13,9 @@ class AdminController { this.searchUser = this.searchUser.bind(this); this.getFalukantUserById = this.getFalukantUserById.bind(this); this.changeFalukantUser = this.changeFalukantUser.bind(this); + this.adminForceFalukantPregnancy = this.adminForceFalukantPregnancy.bind(this); + this.adminClearFalukantPregnancy = this.adminClearFalukantPregnancy.bind(this); + this.adminForceFalukantBirth = this.adminForceFalukantBirth.bind(this); this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this); this.updateFalukantStock = this.updateFalukantStock.bind(this); this.addFalukantStock = this.addFalukantStock.bind(this); @@ -372,6 +375,50 @@ class AdminController { } } + async adminForceFalukantPregnancy(req, res) { + try { + const { userid: userId } = req.headers; + const { characterId, fatherCharacterId, dueInDays } = req.body; + const response = await AdminService.adminForceFalukantPregnancy(userId, characterId, { + fatherCharacterId, + dueInDays, + }); + res.status(200).json(response); + } catch (error) { + console.log(error); + res.status(400).json({ error: error.message }); + } + } + + async adminClearFalukantPregnancy(req, res) { + try { + const { userid: userId } = req.headers; + const { characterId } = req.body; + const response = await AdminService.adminClearFalukantPregnancy(userId, characterId); + res.status(200).json(response); + } catch (error) { + console.log(error); + res.status(400).json({ error: error.message }); + } + } + + async adminForceFalukantBirth(req, res) { + try { + const { userid: userId } = req.headers; + const { motherCharacterId, fatherCharacterId, birthContext, legitimacy, gender } = req.body; + const response = await AdminService.adminForceFalukantBirth(userId, motherCharacterId, { + fatherCharacterId, + birthContext, + legitimacy, + gender, + }); + res.status(200).json(response); + } catch (error) { + console.log(error); + res.status(400).json({ error: error.message }); + } + } + async getFalukantUserBranches(req, res) { try { const { userid: userId } = req.headers; diff --git a/backend/migrations/20260330000000-add-character-pregnancy.cjs b/backend/migrations/20260330000000-add-character-pregnancy.cjs new file mode 100644 index 0000000..bee7780 --- /dev/null +++ b/backend/migrations/20260330000000-add-character-pregnancy.cjs @@ -0,0 +1,36 @@ +"use strict"; + +/** Schwangerschaft (Admin / Spiel): erwarteter Geburtstermin + optionaler Vater-Charakter */ +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_due_at' + ) THEN + ALTER TABLE falukant_data."character" + ADD COLUMN pregnancy_due_at TIMESTAMPTZ NULL; + END IF; + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_father_character_id' + ) THEN + ALTER TABLE falukant_data."character" + ADD COLUMN pregnancy_father_character_id INTEGER NULL + REFERENCES falukant_data."character"(id) ON DELETE SET NULL; + END IF; + END$$; + `); + }, + + async down(queryInterface) { + await queryInterface.sequelize.query(` + ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_father_character_id; + `); + await queryInterface.sequelize.query(` + ALTER TABLE falukant_data."character" DROP COLUMN IF EXISTS pregnancy_due_at; + `); + }, +}; diff --git a/backend/models/associations.js b/backend/models/associations.js index 978a693..b4b5556 100644 --- a/backend/models/associations.js +++ b/backend/models/associations.js @@ -379,6 +379,8 @@ export default function setupAssociations() { FalukantCharacter.belongsTo(RegionData, { foreignKey: 'regionId', as: 'region' }); RegionData.hasMany(FalukantCharacter, { foreignKey: 'regionId', as: 'charactersInRegion' }); + FalukantCharacter.belongsTo(FalukantCharacter, { foreignKey: 'pregnancyFatherCharacterId', as: 'pregnancyFather' }); + FalukantStock.belongsTo(FalukantStockType, { foreignKey: 'stockTypeId', as: 'stockType' }); FalukantStockType.hasMany(FalukantStock, { foreignKey: 'stockTypeId', as: 'stocks' }); diff --git a/backend/models/falukant/data/character.js b/backend/models/falukant/data/character.js index e76b99b..c4327f6 100644 --- a/backend/models/falukant/data/character.js +++ b/backend/models/falukant/data/character.js @@ -45,6 +45,14 @@ FalukantCharacter.init( min: 0, max: 100 } + }, + pregnancyDueAt: { + type: DataTypes.DATE, + allowNull: true, + }, + pregnancyFatherCharacterId: { + type: DataTypes.INTEGER, + allowNull: true, } }, { diff --git a/backend/routers/adminRouter.js b/backend/routers/adminRouter.js index ebf8984..a8bdec9 100644 --- a/backend/routers/adminRouter.js +++ b/backend/routers/adminRouter.js @@ -43,6 +43,9 @@ router.post('/contacts/answer', authenticate, adminController.answerContact); router.post('/falukant/searchuser', authenticate, adminController.searchUser); router.get('/falukant/getuser/:id', authenticate, adminController.getFalukantUserById); router.post('/falukant/edituser', authenticate, adminController.changeFalukantUser); +router.post('/falukant/character/force-pregnancy', authenticate, adminController.adminForceFalukantPregnancy); +router.post('/falukant/character/clear-pregnancy', authenticate, adminController.adminClearFalukantPregnancy); +router.post('/falukant/character/force-birth', authenticate, adminController.adminForceFalukantBirth); router.get('/falukant/branches/:falukantUserId', authenticate, adminController.getFalukantUserBranches); router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock); router.post('/falukant/stock', authenticate, adminController.addFalukantStock); diff --git a/backend/services/adminService.js b/backend/services/adminService.js index 83ed357..2071b93 100644 --- a/backend/services/adminService.js +++ b/backend/services/adminService.js @@ -28,6 +28,7 @@ import Image from '../models/community/image.js'; import EroticVideo from '../models/community/erotic_video.js'; import EroticContentReport from '../models/community/erotic_content_report.js'; import TitleOfNobility from "../models/falukant/type/title_of_nobility.js"; +import ChildRelation from "../models/falukant/data/child_relation.js"; import { sequelize } from '../utils/sequelize.js'; import npcCreationJobService from './npcCreationJobService.js'; import { v4 as uuidv4 } from 'uuid'; @@ -669,7 +670,6 @@ class AdminService { include: [{ model: FalukantCharacter, as: 'character', - attributes: ['birthdate', 'health', 'title_of_nobility'], include: [{ model: FalukantPredefineFirstname, as: 'definedFirstName', @@ -945,6 +945,145 @@ class AdminService { await character.save(); } + /** + * Admin: Charakter als schwanger markieren (erwarteter Geburtstermin). + * @param {number} fatherCharacterId - optional; Vater-Charakter-ID + * @param {number} dueInDays - Tage bis zur „Geburt“ (Default 21) + */ + async adminForceFalukantPregnancy(userId, characterId, { fatherCharacterId = null, dueInDays = 21 } = {}) { + if (!(await this.hasUserAccess(userId, 'falukantusers'))) { + throw new Error('noaccess'); + } + const mother = await FalukantCharacter.findByPk(characterId); + if (!mother) throw new Error('notfound'); + if (fatherCharacterId != null) { + const father = await FalukantCharacter.findByPk(Number(fatherCharacterId)); + if (!father) throw new Error('fatherNotFound'); + } + const days = Math.max(1, Math.min(365, Number(dueInDays) || 21)); + const due = new Date(); + due.setDate(due.getDate() + days); + await mother.update({ + pregnancyDueAt: due, + pregnancyFatherCharacterId: fatherCharacterId != null ? Number(fatherCharacterId) : null, + }); + const fu = mother.userId ? await FalukantUser.findByPk(mother.userId) : null; + if (fu) { + const u = await User.findByPk(fu.userId); + if (u?.hashedId) await notifyUser(u.hashedId, 'familychanged', {}); + } + return { + success: true, + pregnancyDueAt: due.toISOString(), + pregnancyFatherCharacterId: fatherCharacterId != null ? Number(fatherCharacterId) : null, + }; + } + + async adminClearFalukantPregnancy(userId, characterId) { + if (!(await this.hasUserAccess(userId, 'falukantusers'))) { + throw new Error('noaccess'); + } + const mother = await FalukantCharacter.findByPk(characterId); + if (!mother) throw new Error('notfound'); + await mother.update({ + pregnancyDueAt: null, + pregnancyFatherCharacterId: null, + }); + const fu = mother.userId ? await FalukantUser.findByPk(mother.userId) : null; + if (fu) { + const u = await User.findByPk(fu.userId); + if (u?.hashedId) await notifyUser(u.hashedId, 'familychanged', {}); + } + return { success: true }; + } + + /** + * Admin: Geburt auslösen – Kind-Charakter + child_relation; setzt Schwangerschaft zurück. + */ + async adminForceFalukantBirth(userId, motherCharacterId, { + fatherCharacterId, + birthContext = 'marriage', + legitimacy = 'legitimate', + gender = null, + } = {}) { + if (!(await this.hasUserAccess(userId, 'falukantusers'))) { + throw new Error('noaccess'); + } + if (fatherCharacterId == null || fatherCharacterId === '') { + throw new Error('fatherRequired'); + } + const mother = await FalukantCharacter.findByPk(motherCharacterId); + if (!mother) throw new Error('notfound'); + const father = await FalukantCharacter.findByPk(Number(fatherCharacterId)); + if (!father) throw new Error('fatherNotFound'); + if (Number(fatherCharacterId) === Number(motherCharacterId)) { + throw new Error('invalidParents'); + } + const ctx = ['marriage', 'lover'].includes(birthContext) ? birthContext : 'marriage'; + const leg = ['legitimate', 'acknowledged_bastard', 'hidden_bastard'].includes(legitimacy) + ? legitimacy + : 'legitimate'; + const childGender = gender === 'male' || gender === 'female' + ? gender + : (Math.random() < 0.5 ? 'male' : 'female'); + + const nobility = await TitleOfNobility.findOne({ where: { labelTr: 'noncivil' } }); + if (!nobility) throw new Error('titleNotFound'); + + const fnObj = await FalukantPredefineFirstname.findOne({ + where: { gender: childGender }, + order: Sequelize.fn('RANDOM'), + }); + if (!fnObj) throw new Error('firstNameNotFound'); + + let childCharacterId; + await sequelize.transaction(async (t) => { + const baby = await FalukantCharacter.create({ + userId: null, + regionId: mother.regionId, + firstName: fnObj.id, + lastName: mother.lastName, + gender: childGender, + birthdate: new Date(), + titleOfNobility: nobility.id, + health: 100, + moodId: 1, + }, { transaction: t }); + childCharacterId = baby.id; + + await ChildRelation.create({ + fatherCharacterId: father.id, + motherCharacterId: mother.id, + childCharacterId: baby.id, + nameSet: false, + isHeir: false, + legitimacy: leg, + birthContext: ctx, + publicKnown: ctx === 'marriage', + }, { transaction: t }); + + await mother.update({ + pregnancyDueAt: null, + pregnancyFatherCharacterId: null, + }, { transaction: t }); + }); + + const notifyParent = async (char) => { + if (!char?.userId) return; + const fu = await FalukantUser.findByPk(char.userId); + if (!fu) return; + const u = await User.findByPk(fu.userId); + if (u?.hashedId) { + await notifyUser(u.hashedId, 'familychanged', {}); + await notifyUser(u.hashedId, 'falukantUpdateStatus', {}); + } + }; + await notifyParent(mother); + await notifyParent(father); + + return { success: true, childCharacterId, gender: childGender }; + } + // --- User Administration --- async searchUsers(requestingHashedUserId, query) { if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) { diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 6160ede..62347ac 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -3518,7 +3518,13 @@ class FalukantService extends BaseService { deathPartners: relationships.filter(r => r.relationshipType === 'widowed'), children: children.map(({ _createdAt, ...rest }) => rest), possiblePartners: [], - possibleLovers: [] + possibleLovers: [], + pregnancy: character.pregnancyDueAt + ? { + dueAt: character.pregnancyDueAt, + fatherCharacterId: character.pregnancyFatherCharacterId, + } + : null, }; const ownAge = calcAge(character.birthdate); if (ownAge >= 12) { diff --git a/backend/sql/add_character_pregnancy.sql b/backend/sql/add_character_pregnancy.sql new file mode 100644 index 0000000..0f1edf4 --- /dev/null +++ b/backend/sql/add_character_pregnancy.sql @@ -0,0 +1,6 @@ +-- Optional: manuell ausführen, falls Migration nicht genutzt wird +ALTER TABLE falukant_data."character" + ADD COLUMN IF NOT EXISTS pregnancy_due_at TIMESTAMPTZ NULL; +ALTER TABLE falukant_data."character" + ADD COLUMN IF NOT EXISTS pregnancy_father_character_id INTEGER NULL + REFERENCES falukant_data."character"(id) ON DELETE SET NULL; diff --git a/frontend/src/i18n/locales/de/admin.json b/frontend/src/i18n/locales/de/admin.json index c46dd31..e76d231 100644 --- a/frontend/src/i18n/locales/de/admin.json +++ b/frontend/src/i18n/locales/de/admin.json @@ -168,7 +168,40 @@ "errorLoadingStockTypes": "Fehler beim Laden der Lagertypen.", "errorAddingStock": "Fehler beim Hinzufügen des Lagers.", "stockAdded": "Lager erfolgreich hinzugefügt.", - "invalidStockData": "Bitte gültige Lagertyp- und Mengenangaben eingeben." + "invalidStockData": "Bitte gültige Lagertyp- und Mengenangaben eingeben.", + "pregnancy": { + "title": "Schwangerschaft (Admin)", + "characterId": "Charakter-ID", + "status": "Status", + "statusActive": "Schwanger bis", + "statusNone": "Nicht schwanger", + "fatherId": "Vater-Charakter-ID (optional)", + "dueDays": "Tage bis zum Termin", + "force": "Schwangerschaft setzen", + "clear": "Schwangerschaft entfernen", + "successForce": "Schwangerschaft wurde gesetzt.", + "successClear": "Schwangerschaft wurde entfernt.", + "error": "Aktion fehlgeschlagen." + }, + "birth": { + "title": "Geburt erzwingen (Admin)", + "motherHint": "Es wird der oben genannte Charakter (Mutter) verwendet.", + "fatherId": "Vater-Charakter-ID", + "context": "Kontext", + "contextMarriage": "Ehe", + "contextLover": "Liebschaft", + "legitimacy": "Legitimität", + "legitimate": "Legitim", + "ackBastard": "Anerkannt unehelich", + "hiddenBastard": "Verborgen unehelich", + "gender": "Kind-Geschlecht", + "genderRandom": "Zufällig", + "male": "Männlich", + "female": "Weiblich", + "force": "Geburt auslösen", + "success": "Kind wurde angelegt (Taufe ausstehend).", + "error": "Geburt konnte nicht ausgelöst werden." + } }, "map": { "title": "Falukant Karten-Editor (Regionen)", diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index 7307b93..7c34260 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -540,6 +540,10 @@ "familyWarning": "Anhaltender Kreditverzug belastet Ehe, Haushalt und Liebschaften.", "familyImpact": "Der Schuldturm schadet Ehe, Hausfrieden und der Stabilität von Liebschaften." }, + "pregnancy": { + "banner": "Du erwartest ein Kind.", + "dueHint": "Voraussichtlicher Geburtstermin" + }, "spouse": { "title": "Beziehung", "name": "Name", diff --git a/frontend/src/i18n/locales/en/admin.json b/frontend/src/i18n/locales/en/admin.json index 423f528..3072668 100644 --- a/frontend/src/i18n/locales/en/admin.json +++ b/frontend/src/i18n/locales/en/admin.json @@ -223,7 +223,40 @@ "errorLoadingStockTypes": "Error loading warehouse types.", "errorAddingStock": "Error adding warehouse.", "stockAdded": "Warehouse successfully added.", - "invalidStockData": "Please enter valid warehouse type and quantity." + "invalidStockData": "Please enter valid warehouse type and quantity.", + "pregnancy": { + "title": "Pregnancy (admin)", + "characterId": "Character ID", + "status": "Status", + "statusActive": "Expecting until", + "statusNone": "Not pregnant", + "fatherId": "Father character ID (optional)", + "dueDays": "Days until due date", + "force": "Set pregnancy", + "clear": "Clear pregnancy", + "successForce": "Pregnancy has been set.", + "successClear": "Pregnancy has been cleared.", + "error": "Action failed." + }, + "birth": { + "title": "Force birth (admin)", + "motherHint": "The character listed above is used as the mother.", + "fatherId": "Father character ID", + "context": "Context", + "contextMarriage": "Marriage", + "contextLover": "Affair", + "legitimacy": "Legitimacy", + "legitimate": "Legitimate", + "ackBastard": "Acknowledged bastard", + "hiddenBastard": "Hidden bastard", + "gender": "Child gender", + "genderRandom": "Random", + "male": "Male", + "female": "Female", + "force": "Trigger birth", + "success": "Child created (baptism pending).", + "error": "Could not trigger birth." + } }, "createNPC": { "title": "Create NPCs", diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json index bab028c..86a88b2 100644 --- a/frontend/src/i18n/locales/en/falukant.json +++ b/frontend/src/i18n/locales/en/falukant.json @@ -611,6 +611,10 @@ "familyWarning": "Ongoing debt delinquency puts strain on marriage, household and affairs.", "familyImpact": "Debtors' prison damages marriage, household peace and the stability of affairs." }, + "pregnancy": { + "banner": "You are expecting a child.", + "dueHint": "Expected due date" + }, "children": { "title": "Children", "name": "Name", diff --git a/frontend/src/i18n/locales/es/admin.json b/frontend/src/i18n/locales/es/admin.json index 79313d6..4780dca 100644 --- a/frontend/src/i18n/locales/es/admin.json +++ b/frontend/src/i18n/locales/es/admin.json @@ -168,7 +168,40 @@ "errorLoadingStockTypes": "Error al cargar los tipos de almacén.", "errorAddingStock": "Error al añadir el almacén.", "stockAdded": "Almacén añadido correctamente.", - "invalidStockData": "Por favor, introduce un tipo de almacén y una cantidad válidos." + "invalidStockData": "Por favor, introduce un tipo de almacén y una cantidad válidos.", + "pregnancy": { + "title": "Embarazo (admin)", + "characterId": "ID de personaje", + "status": "Estado", + "statusActive": "Embarazo hasta", + "statusNone": "No embarazada", + "fatherId": "ID del padre (opcional)", + "dueDays": "Días hasta el parto previsto", + "force": "Establecer embarazo", + "clear": "Quitar embarazo", + "successForce": "Embarazo establecido.", + "successClear": "Embarazo eliminado.", + "error": "La acción ha fallado." + }, + "birth": { + "title": "Forzar nacimiento (admin)", + "motherHint": "Se usa el personaje indicado arriba como madre.", + "fatherId": "ID del padre", + "context": "Contexto", + "contextMarriage": "Matrimonio", + "contextLover": "Amante", + "legitimacy": "Legitimidad", + "legitimate": "Legítimo", + "ackBastard": "Bastardo reconocido", + "hiddenBastard": "Bastardo oculto", + "gender": "Sexo del niño", + "genderRandom": "Aleatorio", + "male": "Masculino", + "female": "Femenino", + "force": "Provocar nacimiento", + "success": "Niño creado (bautizo pendiente).", + "error": "No se pudo provocar el nacimiento." + } }, "map": { "title": "Editor de mapas de Falukant (regiones)", diff --git a/frontend/src/i18n/locales/es/falukant.json b/frontend/src/i18n/locales/es/falukant.json index afc5938..0618596 100644 --- a/frontend/src/i18n/locales/es/falukant.json +++ b/frontend/src/i18n/locales/es/falukant.json @@ -521,6 +521,10 @@ "familyWarning": "La mora continuada perjudica el matrimonio, el hogar y las relaciones.", "familyImpact": "La prisión por deudas daña el matrimonio, la paz del hogar y la estabilidad de las relaciones." }, + "pregnancy": { + "banner": "Esperas un hijo.", + "dueHint": "Fecha prevista de nacimiento" + }, "spouse": { "title": "Relación", "name": "Nombre", diff --git a/frontend/src/views/admin/falukant/EditUserView.vue b/frontend/src/views/admin/falukant/EditUserView.vue index 7e84bb7..eadc376 100644 --- a/frontend/src/views/admin/falukant/EditUserView.vue +++ b/frontend/src/views/admin/falukant/EditUserView.vue @@ -39,6 +39,67 @@ + +
+

{{ $t('admin.falukant.edituser.pregnancy.title') }}

+

+ {{ $t('admin.falukant.edituser.pregnancy.characterId') }}: + {{ editableUser.falukantData[0].character.id }} +

+

+ {{ $t('admin.falukant.edituser.pregnancy.status') }}: + + +

+ + +
+ + +
+ +

{{ $t('admin.falukant.edituser.birth.title') }}

+

{{ $t('admin.falukant.edituser.birth.motherHint') }}

+ + + + +
+ +
+
+
@@ -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; diff --git a/frontend/src/views/falukant/FamilyView.vue b/frontend/src/views/falukant/FamilyView.vue index fc4b181..5975aed 100644 --- a/frontend/src/views/falukant/FamilyView.vue +++ b/frontend/src/views/falukant/FamilyView.vue @@ -11,6 +11,14 @@
+
+ {{ $t('falukant.family.pregnancy.banner') }} +

{{ $t('falukant.family.pregnancy.dueHint') }}: {{ formatPregnancyDue(pregnancy.dueAt) }}

+
+
/dev/null || true - if [ "$(ls -A $TEMP_ENV_DIR 2>/dev/null)" ]; then - echo "✓ .env-Dateien gesichert: $(ls $TEMP_ENV_DIR)" +mkdir -p "$TEMP_ENV_DIR" + +echo "Sichere .env-Dateien aus $CURRENT_BACKEND..." +if [ -d "$CURRENT_BACKEND" ]; then + find "$CURRENT_BACKEND" -maxdepth 1 -name '.env*' -type f -exec cp {} "$TEMP_ENV_DIR/" \; 2>/dev/null || true + if [ "$(ls -A "$TEMP_ENV_DIR" 2>/dev/null)" ]; then + echo "✓ .env-Dateien gesichert: $(ls "$TEMP_ENV_DIR")" + cp "$TEMP_ENV_DIR"/.env* "$BACKEND_DIR"/ 2>/dev/null || true else - echo "⚠ Keine .env-Dateien in $BACKEND_TARGET gefunden" + echo "⚠ Keine .env-Dateien in $CURRENT_BACKEND gefunden" fi fi -# Altes Backend löschen -echo "Lösche altes Backend..." -sudo rm -rf "$BACKEND_TARGET" +sudo -n mkdir -p "$DATA_DIR/adult-verification" -# Erstelle Backend-Verzeichnis -echo "Erstelle Backend-Verzeichnis..." -sudo mkdir -p "$BACKEND_TARGET" -sudo mkdir -p /opt/yourpart-data/adult-verification - -# Kopiere neues Backend (ohne .env-Dateien aus dem Quellverzeichnis) -echo "Kopiere neues Backend..." -if command -v rsync &> /dev/null; then - sudo rsync -av --exclude='.env*' --exclude='node_modules' . "$BACKEND_TARGET/" 2>/dev/null - if [ -d node_modules ]; then - sudo rsync -av --exclude='.env*' node_modules/ "$BACKEND_TARGET/node_modules/" 2>/dev/null || \ - sudo cp -r node_modules "$BACKEND_TARGET/" 2>/dev/null - fi -else - # Fallback: Kopiere alles außer .env - sudo cp -r * "$BACKEND_TARGET/" 2>/dev/null -fi - -# Stelle .env-Dateien wieder her -echo "Stelle .env-Dateien wieder her..." -if [ -d "$TEMP_ENV_DIR" ] && [ "$(ls -A $TEMP_ENV_DIR 2>/dev/null)" ]; then - sudo cp "$TEMP_ENV_DIR"/.env* "$BACKEND_TARGET/" 2>/dev/null || true - echo "✓ .env-Dateien wiederhergestellt" -else - echo "⚠ Keine .env-Dateien zum Wiederherstellen vorhanden" -fi -sudo rm -rf "$TEMP_ENV_DIR" - -# Prüfe ob .env vorhanden ist -if [ -f "$BACKEND_TARGET/.env" ]; then +if [ -f "$BACKEND_DIR/.env" ]; then echo "✓ .env-Datei ist vorhanden" else - echo "⚠ WARNUNG: Keine .env-Datei in $BACKEND_TARGET gefunden!" - echo " Bitte manuell erstellen: sudo nano $BACKEND_TARGET/.env" + echo "⚠ WARNUNG: Keine .env-Datei in $BACKEND_DIR gefunden!" fi -# Berechtigungen setzen echo "Setting permissions..." -sudo chown -R yourpart:yourpart "$BACKEND_TARGET" -sudo chmod -R 755 "$BACKEND_TARGET" -sudo chown -R yourpart:yourpart /opt/yourpart-data -sudo chmod -R 755 /opt/yourpart-data -# Stelle sicher, dass .env-Dateien die richtigen Berechtigungen haben -if [ -f "$BACKEND_TARGET/.env" ]; then - sudo chmod 600 "$BACKEND_TARGET/.env" +sudo -n chown -R yourpart:yourpart "$BACKEND_DIR" +sudo -n chmod -R 755 "$BACKEND_DIR" +sudo -n chown -R yourpart:yourpart "$DATA_DIR" +sudo -n chmod -R 755 "$DATA_DIR" + +if [ -f "$BACKEND_DIR/.env" ]; then + sudo -n chmod 600 "$BACKEND_DIR/.env" fi -# Systemd-Service aktualisieren und neu laden echo "Updating systemd service..." -sudo cp yourpart.service /etc/systemd/system/ -sudo systemctl daemon-reload +sudo -n cp "$TARGET_DIR/yourpart.service" /etc/systemd/system/ +sudo -n systemctl daemon-reload -# Datenbank-Synchronisation durchführen echo "Running database synchronization..." -cd "$BACKEND_TARGET" - -# STAGE für Schema-Updates verwenden -echo "Running database sync with STAGE=$STAGE..." -export STAGE=$STAGE && npm run sync-db - -# Service neu starten -echo "Restarting yourpart service..." -sudo systemctl restart yourpart - -# Kurz warten und Status prüfen -sleep 2 -echo "Checking service status..." -sudo systemctl status yourpart --no-pager +cd "$BACKEND_DIR" +export STAGE="$STAGE" +npm run sync-db echo "Backend update completed!" +rm -rf "$TEMP_ENV_DIR" \ No newline at end of file diff --git a/update-frontend.sh b/update-frontend.sh index 752ebce..d7e71af 100755 --- a/update-frontend.sh +++ b/update-frontend.sh @@ -1,35 +1,33 @@ -#!/bin/bash +#!/usr/bin/env bash set -euo pipefail -TARGET_DIR="${1:?Zielverzeichnis fehlt: ./update-frontend.sh }" -FRONTEND_TARGET="$TARGET_DIR/frontend" +TARGET_DIR="${1:?target dir required}" + +FRONTEND_DIR="$TARGET_DIR/frontend" +CURRENT_LINK="/opt/yourpart" +CURRENT_FRONTEND="$CURRENT_LINK/frontend" echo "=== YourPart Frontend Update ===" -echo "Ziel: $FRONTEND_TARGET" +echo "Ziel: $FRONTEND_DIR" echo "NOTE: .env files will NOT be overwritten" -# 1. Zum Frontend-Verzeichnis wechseln -cd frontend +cd "$FRONTEND_DIR" -# 2. Berechtigungen für dist-Verzeichnis korrigieren (falls vorhanden) if [ -d "dist" ]; then echo "Korrigiere Berechtigungen für dist-Verzeichnis..." - sudo chown -R $USER:$USER dist/ 2>/dev/null || true + sudo -n chown -R "$USER:$USER" dist/ 2>/dev/null || true fi -# 3. Alle generierten Verzeichnisse löschen echo "Lösche alle generierten Verzeichnisse..." rm -rf dist/ rm -rf node_modules/.vite/ rm -rf node_modules/.cache/ -# 4. Verwende bestehende .env-Dateien im Zielverzeichnis für Build -# Kopiere temporär die bestehende .env vom Ziel, falls vorhanden TEMP_ENV="/tmp/yourpart-frontend-env-$$" -if [ -f "$FRONTEND_TARGET/.env" ]; then - echo "Nutze bestehende .env-Datei vom Zielverzeichnis für Build..." - sudo cp "$FRONTEND_TARGET/.env" "$TEMP_ENV" - sudo chown $USER:$USER "$TEMP_ENV" + +if [ -f "$CURRENT_FRONTEND/.env" ]; then + echo "Nutze bestehende .env-Datei vom Live-System für Build..." + cp "$CURRENT_FRONTEND/.env" "$TEMP_ENV" cp "$TEMP_ENV" .env elif [ -f .env.production ]; then echo "Nutze .env.production für Build..." @@ -39,84 +37,35 @@ elif [ -f .env.server ]; then cp .env.server .env fi -# 5. Frontend neu bauen – VITE_* aus Environment übernehmen oder Defaults setzen echo "Baue Frontend neu..." -export VITE_API_BASE_URL=${VITE_API_BASE_URL:-https://www.your-part.de} -# Standard: Daemon direkt auf Port 4551, nicht über Apache-Proxy -export VITE_DAEMON_SOCKET=${VITE_DAEMON_SOCKET:-wss://www.your-part.de:4551} -export VITE_CHAT_WS_URL=${VITE_CHAT_WS_URL:-wss://www.your-part.de:1235} +export VITE_API_BASE_URL="${VITE_API_BASE_URL:-https://www.your-part.de}" +export VITE_DAEMON_SOCKET="${VITE_DAEMON_SOCKET:-wss://www.your-part.de:4551}" +export VITE_CHAT_WS_URL="${VITE_CHAT_WS_URL:-wss://www.your-part.de:1235}" echo "VITE_API_BASE_URL=$VITE_API_BASE_URL" echo "VITE_DAEMON_SOCKET=$VITE_DAEMON_SOCKET" echo "VITE_CHAT_WS_URL=$VITE_CHAT_WS_URL" -# 5a. Dependencies installieren echo "Installiere Dependencies..." npm install -if [ $? -ne 0 ]; then - echo "❌ npm install fehlgeschlagen!" - # Temporäre .env-Datei aufräumen - rm -f "$TEMP_ENV" - exit 1 -fi - -# 5b. Frontend neu bauen +echo "Baue Frontend..." npm run build -if [ $? -ne 0 ]; then - echo "❌ Build fehlgeschlagen!" - # Temporäre .env-Datei aufräumen - rm -f "$TEMP_ENV" - exit 1 -fi - -echo "✅ Build erfolgreich!" - -# Temporäre .env-Datei aufräumen rm -f "$TEMP_ENV" -# 6. Zielverzeichnis erstellen (falls nicht vorhanden) -echo "Erstelle Zielverzeichnis..." -sudo mkdir -p "$FRONTEND_TARGET/dist" - -# 7. Altes Frontend löschen (außer .env-Dateien) -echo "Lösche altes Frontend (außer .env-Dateien)..." -sudo find "$FRONTEND_TARGET/dist" -mindepth 1 -exec rm -rf {} + 2>/dev/null || true - -# 8. Neues Frontend kopieren -echo "Kopiere neues Frontend..." -sudo cp -r dist/* "$FRONTEND_TARGET/dist/" - -# 9. .env-Dateien NICHT überschreiben - bestehende beibehalten -if [ -f "$FRONTEND_TARGET/.env" ]; then - echo "✓ Bestehende .env-Datei wurde beibehalten (nicht überschrieben)" +if [ -f "$FRONTEND_DIR/.env" ]; then + echo "✓ Bestehende .env-Datei wurde beibehalten" else echo "⚠ Keine .env-Datei im Zielverzeichnis gefunden" - # Falls .env im Quellverzeichnis existiert, kopiere sie nur wenn sie im Ziel nicht existiert - if [ -f .env ]; then - echo "Kopiere .env-Datei (nur wenn nicht vorhanden)..." - sudo cp .env "$FRONTEND_TARGET/" - fi fi -# 10. Berechtigungen setzen echo "Setze Berechtigungen..." -sudo chown -R www-data:www-data "$FRONTEND_TARGET/dist" -if [ -f "$FRONTEND_TARGET/.env" ]; then - sudo chown www-data:www-data "$FRONTEND_TARGET/.env" - sudo chmod 644 "$FRONTEND_TARGET/.env" +sudo -n chown -R www-data:www-data "$FRONTEND_DIR/dist" +if [ -f "$FRONTEND_DIR/.env" ]; then + sudo -n chown www-data:www-data "$FRONTEND_DIR/.env" + sudo -n chmod 644 "$FRONTEND_DIR/.env" fi -sudo chmod -R 755 "$FRONTEND_TARGET/dist" +sudo -n chmod -R 755 "$FRONTEND_DIR/dist" -# 11. Apache neu laden -echo "Lade Apache neu..." -sudo systemctl reload apache2 - -echo "" -echo "=== Frontend Update abgeschlossen! ===" -echo "✅ Frontend neu gebaut" -echo "✅ Frontend aktualisiert" -echo "✅ .env-Dateien wurden NICHT überschrieben" -echo "✅ Apache neu geladen" -echo "" +echo "=== Frontend Update abgeschlossen! ===" \ No newline at end of file