Implement church career management features
- Added endpoints for church career functionalities including overview, available positions, application submission, and application decision-making. - Enhanced the FalukantController to handle church-related requests. - Updated associations and models to support church office types and requirements. - Integrated new routes in the falukantRouter for church career operations. - Implemented service methods for managing church applications and checking church career status. - Updated frontend components to display current positions, available positions, and manage applications with appropriate UI elements and loading states. - Localized new church-related strings in both English and German.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- Taufe -->
|
||||
<div v-if="activeTab === 'baptism'">
|
||||
<h3>{{ $t('falukant.church.baptism.title') }}</h3>
|
||||
<table>
|
||||
@@ -36,6 +37,124 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Aktuelle kirchliche Positionen -->
|
||||
<div v-else-if="activeTab === 'current'" class="tab-pane">
|
||||
<div v-if="loading.current" class="loading">{{ $t('loading') }}</div>
|
||||
<div v-else class="table-scroll">
|
||||
<table class="church-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('falukant.church.current.office') }}</th>
|
||||
<th>{{ $t('falukant.church.current.region') }}</th>
|
||||
<th>{{ $t('falukant.church.current.holder') }}</th>
|
||||
<th>{{ $t('falukant.church.current.supervisor') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="pos in currentPositions" :key="pos.id" :class="{ 'own-position': isOwnPosition(pos) }">
|
||||
<td>{{ $t(`falukant.church.offices.${pos.officeType.name}`) }}</td>
|
||||
<td>{{ pos.region.name }}</td>
|
||||
<td>
|
||||
<span v-if="pos.character">
|
||||
{{ $t(`falukant.titles.${pos.character.gender}.${pos.character.title || 'noncivil'}`) }}
|
||||
{{ pos.character.name }}
|
||||
</span>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="pos.supervisor">
|
||||
{{ pos.supervisor.name }}
|
||||
</span>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!currentPositions.length">
|
||||
<td colspan="4">{{ $t('falukant.church.current.none') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Verfügbare Positionen -->
|
||||
<div v-else-if="activeTab === 'available'" class="tab-pane">
|
||||
<div v-if="loading.available" class="loading">{{ $t('loading') }}</div>
|
||||
<div v-else class="table-scroll">
|
||||
<table class="church-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('falukant.church.available.office') }}</th>
|
||||
<th>{{ $t('falukant.church.available.region') }}</th>
|
||||
<th>{{ $t('falukant.church.available.supervisor') }}</th>
|
||||
<th>{{ $t('falukant.church.available.seats') }}</th>
|
||||
<th>{{ $t('falukant.church.available.action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="pos in availablePositions" :key="pos.id">
|
||||
<td>{{ $t(`falukant.church.offices.${pos.officeType.name}`) }}</td>
|
||||
<td>{{ pos.region?.name || '—' }}</td>
|
||||
<td>
|
||||
<span v-if="pos.supervisor">
|
||||
{{ pos.supervisor.name }}
|
||||
</span>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
<td>{{ pos.availableSeats }}</td>
|
||||
<td>
|
||||
<button @click="applyForPosition(pos)" :disabled="pos.availableSeats === 0">
|
||||
{{ $t('falukant.church.available.apply') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!availablePositions.length">
|
||||
<td colspan="5">{{ $t('falukant.church.available.none') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bewerbungen (als Vorgesetzter) -->
|
||||
<div v-else-if="activeTab === 'applications'" class="tab-pane">
|
||||
<div v-if="loading.applications" class="loading">{{ $t('loading') }}</div>
|
||||
<div v-else class="table-scroll">
|
||||
<table class="church-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('falukant.church.applications.office') }}</th>
|
||||
<th>{{ $t('falukant.church.applications.region') }}</th>
|
||||
<th>{{ $t('falukant.church.applications.applicant') }}</th>
|
||||
<th>{{ $t('falukant.church.applications.date') }}</th>
|
||||
<th>{{ $t('falukant.church.applications.action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="app in supervisedApplications" :key="app.id">
|
||||
<td>{{ $t(`falukant.church.offices.${app.officeType.name}`) }}</td>
|
||||
<td>{{ app.region.name }}</td>
|
||||
<td>
|
||||
{{ $t(`falukant.titles.${app.applicant.gender}.${app.applicant.title || 'noncivil'}`) }}
|
||||
{{ app.applicant.name }} ({{ app.applicant.age }})
|
||||
</td>
|
||||
<td>{{ formatDate(app.createdAt) }}</td>
|
||||
<td>
|
||||
<button @click="decideOnApplication(app.id, 'approve')" class="approve-button">
|
||||
{{ $t('falukant.church.applications.approve') }}
|
||||
</button>
|
||||
<button @click="decideOnApplication(app.id, 'reject')" class="reject-button">
|
||||
{{ $t('falukant.church.applications.reject') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!supervisedApplications.length">
|
||||
<td colspan="5">{{ $t('falukant.church.applications.none') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,12 +180,36 @@ export default {
|
||||
activeTab: 'baptism',
|
||||
tabs: [
|
||||
{ value: 'baptism', label: 'falukant.church.baptism.title' },
|
||||
{ value: 'current', label: 'falukant.church.tabs.current' },
|
||||
{ value: 'available', label: 'falukant.church.tabs.available' },
|
||||
{ value: 'applications', label: 'falukant.church.tabs.applications' },
|
||||
],
|
||||
baptismList: []
|
||||
baptismList: [],
|
||||
currentPositions: [],
|
||||
availablePositions: [],
|
||||
supervisedApplications: [],
|
||||
ownCharacterId: null,
|
||||
loading: {
|
||||
current: false,
|
||||
available: false,
|
||||
applications: false
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadNotBaptisedChildren()
|
||||
await this.loadNotBaptisedChildren();
|
||||
await this.loadOwnCharacterId();
|
||||
},
|
||||
watch: {
|
||||
activeTab(newTab) {
|
||||
if (newTab === 'current') {
|
||||
this.loadCurrentPositions();
|
||||
} else if (newTab === 'available') {
|
||||
this.loadAvailablePositions();
|
||||
} else if (newTab === 'applications') {
|
||||
this.loadSupervisedApplications();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadNotBaptisedChildren() {
|
||||
@@ -99,6 +242,102 @@ export default {
|
||||
console.error(err)
|
||||
this.$root.$refs.errorDialog.open('tr:falukant.church.baptism.error')
|
||||
}
|
||||
},
|
||||
async loadCurrentPositions() {
|
||||
this.loading.current = true;
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/church/overview');
|
||||
this.currentPositions = data;
|
||||
} catch (err) {
|
||||
console.error('Error loading current positions', err);
|
||||
} finally {
|
||||
this.loading.current = false;
|
||||
}
|
||||
},
|
||||
async loadAvailablePositions() {
|
||||
this.loading.available = true;
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/church/positions/available');
|
||||
this.availablePositions = data;
|
||||
} catch (err) {
|
||||
console.error('Error loading available positions', err);
|
||||
} finally {
|
||||
this.loading.available = false;
|
||||
}
|
||||
},
|
||||
async loadSupervisedApplications() {
|
||||
this.loading.applications = true;
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/church/applications/supervised');
|
||||
this.supervisedApplications = data;
|
||||
} catch (err) {
|
||||
console.error('Error loading supervised applications', err);
|
||||
} finally {
|
||||
this.loading.applications = false;
|
||||
}
|
||||
},
|
||||
async loadOwnCharacterId() {
|
||||
try {
|
||||
const { data } = await apiClient.get('/api/falukant/info');
|
||||
if (data.character && data.character.id) {
|
||||
this.ownCharacterId = data.character.id;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading own character ID', err);
|
||||
}
|
||||
},
|
||||
isOwnPosition(pos) {
|
||||
if (!this.ownCharacterId || !pos.character) {
|
||||
return false;
|
||||
}
|
||||
return pos.character.id === this.ownCharacterId;
|
||||
},
|
||||
async applyForPosition(position) {
|
||||
try {
|
||||
const regionId = position.regionId || position.region?.id;
|
||||
|
||||
if (!regionId) {
|
||||
throw new Error('Region not found');
|
||||
}
|
||||
|
||||
await apiClient.post('/api/falukant/church/positions/apply', {
|
||||
officeTypeId: position.id,
|
||||
regionId: regionId
|
||||
});
|
||||
|
||||
this.$root.$refs.messageDialog?.open('tr:falukant.church.available.applySuccess');
|
||||
await this.loadAvailablePositions();
|
||||
} catch (err) {
|
||||
console.error('Error applying for position', err);
|
||||
const errorMsg = err.response?.data?.message || 'falukant.church.available.applyError';
|
||||
this.$root.$refs.errorDialog?.open(`tr:${errorMsg}`);
|
||||
}
|
||||
},
|
||||
async decideOnApplication(applicationId, decision) {
|
||||
try {
|
||||
await apiClient.post('/api/falukant/church/applications/decide', {
|
||||
applicationId: applicationId,
|
||||
decision: decision
|
||||
});
|
||||
|
||||
const msgKey = decision === 'approve'
|
||||
? 'falukant.church.applications.approveSuccess'
|
||||
: 'falukant.church.applications.rejectSuccess';
|
||||
this.$root.$refs.messageDialog?.open(`tr:${msgKey}`);
|
||||
await this.loadSupervisedApplications();
|
||||
await this.loadCurrentPositions();
|
||||
} catch (err) {
|
||||
console.error('Error deciding on application', err);
|
||||
const errorMsg = err.response?.data?.message || 'falukant.church.applications.decideError';
|
||||
this.$root.$refs.errorDialog?.open(`tr:${errorMsg}`);
|
||||
}
|
||||
},
|
||||
formatDate(date) {
|
||||
return new Date(date).toLocaleDateString(this.$i18n.locale, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,4 +379,75 @@ input[type="text"] {
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.church-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.church-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #FFF;
|
||||
z-index: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.church-table tbody td {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.church-table tbody tr.own-position {
|
||||
background-color: #e0e0e0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.approve-button {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.approve-button:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.reject-button {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.reject-button:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user