This commit was merged in pull request #191.
This commit is contained in:
@@ -54,6 +54,10 @@ use OCP\AppFramework\Db\Entity;
|
||||
* @method void setDeletedAt(?string $deletedAt)
|
||||
* @method string|null getEinwilligungDatum()
|
||||
* @method void setEinwilligungDatum(?string $einwilligungDatum)
|
||||
* @method string|null getJuleicaNummer()
|
||||
* @method void setJuleicaNummer(?string $juleicaNummer)
|
||||
* @method string|null getJuleicaAblaufdatum()
|
||||
* @method void setJuleicaAblaufdatum(?string $juleicaAblaufdatum)
|
||||
*/
|
||||
class Member extends Entity implements JsonSerializable {
|
||||
|
||||
@@ -79,6 +83,8 @@ class Member extends Entity implements JsonSerializable {
|
||||
protected string $updatedAt = '';
|
||||
protected ?string $deletedAt = null;
|
||||
protected ?string $einwilligungDatum = null;
|
||||
protected ?string $juleicaNummer = null;
|
||||
protected ?string $juleicaAblaufdatum = null;
|
||||
|
||||
public function __construct() {
|
||||
$this->addType('id', 'integer');
|
||||
@@ -111,6 +117,8 @@ class Member extends Entity implements JsonSerializable {
|
||||
'updatedAt' => $this->updatedAt,
|
||||
'deletedAt' => $this->deletedAt,
|
||||
'einwilligungDatum' => $this->einwilligungDatum,
|
||||
'juleicaNummer' => $this->juleicaNummer,
|
||||
'juleicaAblaufdatum' => $this->juleicaAblaufdatum,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Mitgliederverwaltung\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
/**
|
||||
* Migration: Add Juleica fields to mv_members.
|
||||
*
|
||||
* Adds juleica_nummer (varchar, nullable) and juleica_ablaufdatum (date, nullable)
|
||||
* for tracking youth leader card data.
|
||||
*
|
||||
* Part of Issue #160.
|
||||
*/
|
||||
class Version000016Date20260410000001 extends SimpleMigrationStep {
|
||||
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
if ($schema->hasTable('mv_members')) {
|
||||
$table = $schema->getTable('mv_members');
|
||||
|
||||
if (!$table->hasColumn('juleica_nummer')) {
|
||||
$table->addColumn('juleica_nummer', Types::STRING, [
|
||||
'notnull' => false,
|
||||
'length' => 100,
|
||||
'default' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$table->hasColumn('juleica_ablaufdatum')) {
|
||||
$table->addColumn('juleica_ablaufdatum', Types::DATE, [
|
||||
'notnull' => false,
|
||||
'default' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -390,4 +390,105 @@ class CalendarSyncService {
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($memberId, IQueryBuilder::PARAM_INT)));
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
// ── Juleica reminder sync (Issue #160) ──────────────────────────
|
||||
|
||||
/**
|
||||
* Sync a single member's Juleica expiry reminder.
|
||||
*
|
||||
* Creates a one-time reminder 2 months before juleica_ablaufdatum.
|
||||
* Removes the reminder if the date is cleared.
|
||||
* Updates the reminder if the date changes.
|
||||
*
|
||||
* @param Member $member The member to sync
|
||||
*/
|
||||
public function syncJuleicaReminder(Member $member): void {
|
||||
$this->assertWritePermission();
|
||||
|
||||
$juleicaUri = $this->generateJuleicaEventUri($member);
|
||||
$hasExpiry = $member->getJuleicaAblaufdatum() !== null
|
||||
&& $member->getJuleicaAblaufdatum() !== '';
|
||||
$hasEvent = $this->juleicaEventExists($juleicaUri);
|
||||
|
||||
if ($hasExpiry && $member->getDeletedAt() === null) {
|
||||
$vcalendar = $this->buildJuleicaVEvent($member);
|
||||
$this->storeCalendarEvent($juleicaUri, $vcalendar);
|
||||
|
||||
$this->logger->debug('Synced Juleica reminder', [
|
||||
'memberId' => $member->getId(),
|
||||
'eventUri' => $juleicaUri,
|
||||
'app' => 'mitgliederverwaltung',
|
||||
]);
|
||||
} elseif ($hasEvent) {
|
||||
$this->removeCalendarEvent($juleicaUri);
|
||||
|
||||
$this->logger->debug('Removed Juleica reminder', [
|
||||
'memberId' => $member->getId(),
|
||||
'eventUri' => $juleicaUri,
|
||||
'app' => 'mitgliederverwaltung',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a VCalendar event for a member's Juleica expiry reminder.
|
||||
* The event is placed 2 months before the Ablaufdatum.
|
||||
*/
|
||||
private function buildJuleicaVEvent(Member $member): string {
|
||||
$expiry = new \DateTime($member->getJuleicaAblaufdatum());
|
||||
$reminderDate = (clone $expiry)->modify('-2 months');
|
||||
|
||||
$summary = sprintf(
|
||||
'Juleica-Erneuerung: %s %s',
|
||||
$member->getVorname(),
|
||||
$member->getNachname()
|
||||
);
|
||||
|
||||
$description = sprintf(
|
||||
'Die Juleica von %s %s laeuft am %s ab. Bitte rechtzeitig die Erneuerung einleiten.',
|
||||
$member->getVorname(),
|
||||
$member->getNachname(),
|
||||
$expiry->format('d.m.Y')
|
||||
);
|
||||
|
||||
if ($member->getJuleicaNummer()) {
|
||||
$description .= "\nJuleica-Nummer: " . $member->getJuleicaNummer();
|
||||
}
|
||||
|
||||
$vcal = new VCalendar();
|
||||
$vevent = $vcal->add('VEVENT', [
|
||||
'SUMMARY' => $summary,
|
||||
'DESCRIPTION' => $description,
|
||||
'DTSTART' => $reminderDate,
|
||||
'TRANSP' => 'TRANSPARENT',
|
||||
]);
|
||||
|
||||
// All-day event (non-recurring)
|
||||
$vevent->DTSTART['VALUE'] = 'DATE';
|
||||
|
||||
return $vcal->serialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique event URI for a member's Juleica reminder.
|
||||
*/
|
||||
private function generateJuleicaEventUri(Member $member): string {
|
||||
return 'mv-juleica-' . $member->getId() . '.ics';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a Juleica reminder event exists in the calendar staging table.
|
||||
*/
|
||||
private function juleicaEventExists(string $eventUri): bool {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('id')
|
||||
->from('mv_calendar_events')
|
||||
->where($qb->expr()->eq('event_uri', $qb->createNamedParameter($eventUri)));
|
||||
|
||||
$result = $qb->executeQuery();
|
||||
$exists = $result->fetch() !== false;
|
||||
$result->closeCursor();
|
||||
|
||||
return $exists;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ class EntityExportService {
|
||||
*/
|
||||
public function getColumnHeaders(string $type): array {
|
||||
return match ($type) {
|
||||
'mitglieder' => ['ID', 'Vorname', 'Nachname', 'Geburtsdatum', 'Geschlecht', 'Rolle', 'Stufe-ID', 'Stufenname', 'Eintritt', 'Austritt', 'Status', 'Allergien', 'Notizen', 'Zusaetzliche Notizen', 'KV-Typ', 'KV-Name', 'Familien-ID', 'Familienname', 'Eingefrorener Beitragssatz', 'Einwilligungsdatum'],
|
||||
'mitglieder' => ['ID', 'Vorname', 'Nachname', 'Geburtsdatum', 'Geschlecht', 'Rolle', 'Stufe-ID', 'Stufenname', 'Eintritt', 'Austritt', 'Status', 'Allergien', 'Notizen', 'Zusaetzliche Notizen', 'KV-Typ', 'KV-Name', 'Familien-ID', 'Familienname', 'Eingefrorener Beitragssatz', 'Einwilligungsdatum', 'Juleica-Nummer', 'Juleica-Ablaufdatum'],
|
||||
'familien' => ['ID', 'Name', 'Kontoinhaber', 'IBAN', 'BIC', 'Kreditinstitut'],
|
||||
'adressen' => ['ID', 'Mitglied-ID', 'Mitgliedername', 'Label', 'Strasse', 'PLZ', 'Ort', 'Land', 'Primaer'],
|
||||
'telefonnummern' => ['ID', 'Mitglied-ID', 'Mitgliedername', 'Label', 'Nummer'],
|
||||
@@ -228,6 +228,8 @@ class EntityExportService {
|
||||
$this->resolveFamily($m->getFamilyId()),
|
||||
(string)($m->getFrozenFeeRate() ?? ''),
|
||||
$m->getEinwilligungDatum() ?? '',
|
||||
$m->getJuleicaNummer() ?? '',
|
||||
$m->getJuleicaAblaufdatum() ?? '',
|
||||
];
|
||||
}
|
||||
return $rows;
|
||||
|
||||
@@ -447,6 +447,8 @@ class EntityImportService {
|
||||
if (isset($fields['allergien']) && $fields['allergien'] !== '[verschluesselt]') {
|
||||
$entity->setAllergienEncrypted($fields['allergien'] ? $this->encryptionService->encrypt($fields['allergien']) : null);
|
||||
}
|
||||
if (isset($fields['juleica_nummer'])) $entity->setJuleicaNummer($fields['juleica_nummer'] ?: null);
|
||||
if (isset($fields['juleica_ablaufdatum'])) $entity->setJuleicaAblaufdatum($fields['juleica_ablaufdatum'] ? $this->normalizeDate($fields['juleica_ablaufdatum']) : null);
|
||||
$entity->setUpdatedAt($now);
|
||||
$this->memberMapper->update($entity);
|
||||
$this->auditService->logUpdate($entity->jsonSerialize(), 'member', $id);
|
||||
@@ -527,6 +529,8 @@ class EntityImportService {
|
||||
['key' => 'familienname', 'label' => 'Familienname', 'required' => false, 'fk_resolve' => 'family'],
|
||||
['key' => 'frozen_fee_rate', 'label' => 'Eingefrorener Beitragssatz', 'required' => false],
|
||||
['key' => 'einwilligung_datum', 'label' => 'Einwilligungsdatum', 'required' => false, 'type' => 'date'],
|
||||
['key' => 'juleica_nummer', 'label' => 'Juleica-Nummer', 'required' => false],
|
||||
['key' => 'juleica_ablaufdatum', 'label' => 'Juleica-Ablaufdatum', 'required' => false, 'type' => 'date'],
|
||||
],
|
||||
'familien' => [
|
||||
['key' => 'id', 'label' => 'ID', 'required' => false],
|
||||
@@ -991,6 +995,8 @@ class EntityImportService {
|
||||
if (!empty($d['family_id'])) $m->setFamilyId((int)$d['family_id']);
|
||||
if (!empty($d['frozen_fee_rate'])) $m->setFrozenFeeRate($d['frozen_fee_rate']);
|
||||
if (!empty($d['einwilligung_datum'])) $m->setEinwilligungDatum($this->normalizeDate($d['einwilligung_datum']));
|
||||
if (!empty($d['juleica_nummer'])) $m->setJuleicaNummer($d['juleica_nummer']);
|
||||
if (!empty($d['juleica_ablaufdatum'])) $m->setJuleicaAblaufdatum($this->normalizeDate($d['juleica_ablaufdatum']));
|
||||
$m->setCreatedAt($now);
|
||||
$m->setUpdatedAt($now);
|
||||
$m = $this->memberMapper->insert($m);
|
||||
|
||||
@@ -102,6 +102,8 @@ class MemberService {
|
||||
$member->setFrozenFeeRate($data['frozenFeeRate'] ?? null);
|
||||
$member->setCalendarEventUri($data['calendarEventUri'] ?? null);
|
||||
$member->setContactVcardUri($data['contactVcardUri'] ?? null);
|
||||
$member->setJuleicaNummer($data['juleicaNummer'] ?? null);
|
||||
$member->setJuleicaAblaufdatum($data['juleicaAblaufdatum'] ?? null);
|
||||
$member->setCreatedAt($now);
|
||||
$member->setUpdatedAt($now);
|
||||
|
||||
@@ -352,6 +354,7 @@ class MemberService {
|
||||
'stufeId', 'eintritt', 'austritt', 'status', 'allergienEncrypted',
|
||||
'notizen', 'zusatzNotizen', 'kvTyp', 'kvName', 'familyId',
|
||||
'frozenFeeRate', 'calendarEventUri', 'contactVcardUri', 'einwilligungDatum',
|
||||
'juleicaNummer', 'juleicaAblaufdatum',
|
||||
];
|
||||
$data = array_intersect_key($data, array_flip($editableFields));
|
||||
|
||||
@@ -418,6 +421,12 @@ class MemberService {
|
||||
if (array_key_exists('einwilligungDatum', $data)) {
|
||||
$member->setEinwilligungDatum($data['einwilligungDatum']);
|
||||
}
|
||||
if (array_key_exists('juleicaNummer', $data)) {
|
||||
$member->setJuleicaNummer($data['juleicaNummer']);
|
||||
}
|
||||
if (array_key_exists('juleicaAblaufdatum', $data)) {
|
||||
$member->setJuleicaAblaufdatum($data['juleicaAblaufdatum']);
|
||||
}
|
||||
|
||||
$member->setUpdatedAt((new DateTime())->format('Y-m-d H:i:s'));
|
||||
|
||||
|
||||
@@ -102,6 +102,33 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Juleica section -->
|
||||
<div class="member-form__section">
|
||||
<h4>Juleica</h4>
|
||||
<div class="member-form__grid">
|
||||
<div class="member-form__field">
|
||||
<label>Juleica-Nummer</label>
|
||||
<NcTextField :model-value="member.juleicaNummer || ''"
|
||||
:disabled="!editing"
|
||||
placeholder="Kartennummer"
|
||||
@update:model-value="$emit('update', 'juleicaNummer', $event || null)" />
|
||||
</div>
|
||||
<div class="member-form__field">
|
||||
<label>Ablaufdatum</label>
|
||||
<NcTextField :model-value="member.juleicaAblaufdatum || ''"
|
||||
type="date"
|
||||
:disabled="!editing"
|
||||
@update:model-value="$emit('update', 'juleicaAblaufdatum', $event || null)" />
|
||||
<span v-if="!editing && juleicaStatus === 'expired'" class="member-form__juleica-warning member-form__juleica-warning--expired">
|
||||
abgelaufen
|
||||
</span>
|
||||
<span v-else-if="!editing && juleicaStatus === 'expiring'" class="member-form__juleica-warning member-form__juleica-warning--expiring">
|
||||
laeuft bald ab
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full-width text areas -->
|
||||
<div class="member-form__field member-form__field--full">
|
||||
<label>Allergien</label>
|
||||
@@ -133,9 +160,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { NcTextField, NcSelect } from '@nextcloud/vue'
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
member: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@@ -156,6 +184,24 @@ defineProps({
|
||||
|
||||
defineEmits(['update'])
|
||||
|
||||
/**
|
||||
* Compute Juleica status: 'expired', 'expiring', or null.
|
||||
* 'expired' = Ablaufdatum is in the past
|
||||
* 'expiring' = Ablaufdatum is within the next 2 months
|
||||
*/
|
||||
const juleicaStatus = computed(() => {
|
||||
const dateStr = props.member.juleicaAblaufdatum
|
||||
if (!dateStr) return null
|
||||
const expiry = new Date(dateStr)
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
if (expiry < now) return 'expired'
|
||||
const twoMonths = new Date(now)
|
||||
twoMonths.setMonth(twoMonths.getMonth() + 2)
|
||||
if (expiry <= twoMonths) return 'expiring'
|
||||
return null
|
||||
})
|
||||
|
||||
const geschlechtOptions = [
|
||||
{ value: 'maennlich', label: 'Männlich' },
|
||||
{ value: 'weiblich', label: 'Weiblich' },
|
||||
@@ -233,4 +279,34 @@ const kvTypOptions = [
|
||||
border-color: var(--color-primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.member-form__section {
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.member-form__section h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 1em;
|
||||
color: var(--color-text-lighter);
|
||||
}
|
||||
|
||||
.member-form__juleica-warning {
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-pill);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.member-form__juleica-warning--expired {
|
||||
color: var(--color-error);
|
||||
background-color: var(--color-error-hover, rgba(204, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.member-form__juleica-warning--expiring {
|
||||
color: var(--color-warning);
|
||||
background-color: var(--color-warning-hover, rgba(204, 153, 0, 0.1));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -363,6 +363,8 @@ async function save() {
|
||||
familyId: d.familyId,
|
||||
frozenFeeRate: d.frozenFeeRate,
|
||||
einwilligungDatum: d.einwilligungDatum,
|
||||
juleicaNummer: d.juleicaNummer,
|
||||
juleicaAblaufdatum: d.juleicaAblaufdatum,
|
||||
}
|
||||
await store.updateMember(Number(props.id), data)
|
||||
await loadMember()
|
||||
|
||||
Reference in New Issue
Block a user