Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion lib/core/database/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1772,7 +1772,12 @@ class Certifications extends Table {
TextColumn get diverId => text().nullable().references(Divers, #id)();
TextColumn get name => text()(); // e.g., "Open Water Diver"
TextColumn get agency => text()(); // PADI, SSI, etc.
// Free-text agency when `agency` == 'other' and the diver's agency isn't in
// the list (issue #806-style escape hatch).
TextColumn get agencyCustom => text().nullable()();
TextColumn get level => text().nullable()(); // For more specific level info
// Free-text level/certification when `level` == 'other'.
TextColumn get levelCustom => text().nullable()();
TextColumn get cardNumber => text().nullable()();
IntColumn get issueDate => integer().nullable()();
IntColumn get expiryDate => integer().nullable()(); // For certs that expire
Expand Down Expand Up @@ -2952,7 +2957,7 @@ class AppDatabase extends _$AppDatabase {

/// The current schema version as a static constant so that pre-open checks
/// (e.g. version-mismatch guard) can reference it without an instance.
static const int currentSchemaVersion = 148;
static const int currentSchemaVersion = 149;

/// Every schema version that has a migration block in onUpgrade.
/// Used to calculate progress step counts. When adding a new migration,
Expand Down Expand Up @@ -3153,6 +3158,8 @@ class AppDatabase extends _$AppDatabase {
// index plus the site-side dedupe cleanup and partial unique index
// mirroring the dive-side v38 pair.
148,
// v149: free-text agency/level escape hatch for certifications.
149,
];

/// Idempotent DDL for the v106 connector-suggestion columns (Lightroom
Expand Down Expand Up @@ -4386,6 +4393,27 @@ class AppDatabase extends _$AppDatabase {
}
}

/// Idempotent DDL for the v149 certifications free-text agency/level columns.
/// Called from the v149 onUpgrade step and the beforeOpen backstop, and
/// self-guarding when the table is absent (minimal migration-test fixtures).
Future<void> _assertCertificationCustomColumns() async {
final cols = await customSelect(
"PRAGMA table_info('certifications')",
).get();
if (cols.isEmpty) return;
final names = cols.map((c) => c.read<String>('name')).toSet();
if (!names.contains('agency_custom')) {
await customStatement(
'ALTER TABLE certifications ADD COLUMN agency_custom TEXT',
);
}
if (!names.contains('level_custom')) {
await customStatement(
'ALTER TABLE certifications ADD COLUMN level_custom TEXT',
);
}
}

/// Idempotent DDL for the v142 return-flight column. Called from the v142
/// onUpgrade step and the beforeOpen backstop, matching the
/// _assertWeatherCodeColumn pattern so a schema-version collision cannot
Expand Down Expand Up @@ -7771,6 +7799,11 @@ class AppDatabase extends _$AppDatabase {
}
}
if (from < 148) await reportProgress();
if (from < 149) {
// Free-text agency/level escape hatch for certifications.
await _assertCertificationCustomColumns();
}
if (from < 149) await reportProgress();
},
beforeOpen: (details) async {
// Enable foreign keys
Expand All @@ -7795,6 +7828,9 @@ class AppDatabase extends _$AppDatabase {
// v141 backstop: re-assert diver_settings.default_currency.
await _assertDefaultCurrencyColumn();

// v148 backstop: re-assert certifications.agency_custom/level_custom.
await _assertCertificationCustomColumns();

// v106 backstop: re-assert connector-suggestion columns (the helper
// is self-guarding when the suggestions table is absent).
await _assertConnectorSuggestionColumns();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ class CertificationRepository {
buddyId: Value(cert.buddyId),
name: Value(cert.name),
agency: Value(cert.agency.name),
agencyCustom: Value(cert.agencyCustom),
level: Value(cert.level?.name),
levelCustom: Value(cert.levelCustom),
cardNumber: Value(cert.cardNumber),
issueDate: Value(cert.issueDate?.millisecondsSinceEpoch),
expiryDate: Value(cert.expiryDate?.millisecondsSinceEpoch),
Expand Down Expand Up @@ -248,7 +250,9 @@ class CertificationRepository {
CertificationsCompanion(
name: Value(cert.name),
agency: Value(cert.agency.name),
agencyCustom: Value(cert.agencyCustom),
level: Value(cert.level?.name),
levelCustom: Value(cert.levelCustom),
cardNumber: Value(cert.cardNumber),
issueDate: Value(cert.issueDate?.millisecondsSinceEpoch),
expiryDate: Value(cert.expiryDate?.millisecondsSinceEpoch),
Expand Down Expand Up @@ -375,7 +379,9 @@ class CertificationRepository {
buddyId: row.data['buddy_id'] as String?,
name: row.data['name'] as String,
agency: _parseCertificationAgency(row.data['agency'] as String),
agencyCustom: row.data['agency_custom'] as String?,
level: _parseCertificationLevel(row.data['level'] as String?),
levelCustom: row.data['level_custom'] as String?,
cardNumber: row.data['card_number'] as String?,
issueDate: _parseDateTime(row.data['issue_date'] as int?),
expiryDate: _parseDateTime(row.data['expiry_date'] as int?),
Expand All @@ -401,7 +407,9 @@ class CertificationRepository {
buddyId: row.buddyId,
name: row.name,
agency: _parseCertificationAgency(row.agency),
agencyCustom: row.agencyCustom,
level: _parseCertificationLevel(row.level),
levelCustom: row.levelCustom,
cardNumber: row.cardNumber,
issueDate: _parseDateTime(row.issueDate),
expiryDate: _parseDateTime(row.expiryDate),
Expand Down
53 changes: 43 additions & 10 deletions lib/features/certifications/domain/certification_title.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,36 @@ import 'package:submersion/features/certifications/domain/entities/certification
/// detail page's Agency row, the picker's subtitle, the PDF's agency line, the
/// list's Agency column -- so prefixing here would just trade one duplication
/// for another.
/// The agency label to show: the free-text custom agency when [agency] is
/// [CertificationAgency.other] and a custom name was entered, otherwise the
/// enum's display name.
String effectiveAgencyLabel(CertificationAgency agency, String? agencyCustom) {
if (agency == CertificationAgency.other) {
final custom = agencyCustom?.trim();
if (custom != null && custom.isNotEmpty) return custom;
}
return agency.displayName;
}

/// The level label to show: the free-text custom level when [level] is
/// [CertificationLevel.other] and a custom name was entered, otherwise the
/// enum's display name. Null when there is no level.
String? effectiveLevelLabel(CertificationLevel? level, String? levelCustom) {
if (level == CertificationLevel.other) {
final custom = levelCustom?.trim();
if (custom != null && custom.isNotEmpty) return custom;
}
return level?.displayName;
}

String derivedCertificationTitle(
CertificationAgency agency,
CertificationLevel? level,
) => level?.displayName ?? agency.displayName;
CertificationLevel? level, {
String? agencyCustom,
String? levelCustom,
}) =>
effectiveLevelLabel(level, levelCustom) ??
effectiveAgencyLabel(agency, agencyCustom);

String _normalized(String value) =>
value.trim().toLowerCase().replaceAll(RegExp(r'\s+'), ' ');
Expand All @@ -33,15 +59,15 @@ bool hasDerivedName(Certification cert) {
final stored = _normalized(cert.name);
if (stored.isEmpty) return true;

final agencyName = cert.agency.displayName;
final level = cert.level;
final agencyName = effectiveAgencyLabel(cert.agency, cert.agencyCustom);
final level = effectiveLevelLabel(cert.level, cert.levelCustom);
final candidates = <String>[
agencyName,
if (level != null) ...[
'$agencyName ${level.displayName}',
'$agencyName: ${level.displayName}',
'$agencyName : ${level.displayName}',
level.displayName,
'$agencyName $level',
'$agencyName: $level',
'$agencyName : $level',
level,
],
];
return candidates.map(_normalized).contains(stored);
Expand All @@ -55,11 +81,18 @@ String? customNameOrNull(Certification cert) =>
/// The title to show for [cert] anywhere one is needed. Never empty.
String certificationTitle(Certification cert) =>
customNameOrNull(cert) ??
derivedCertificationTitle(cert.agency, cert.level);
derivedCertificationTitle(
cert.agency,
cert.level,
agencyCustom: cert.agencyCustom,
levelCustom: cert.levelCustom,
);

/// The secondary line beneath [certificationTitle]: the level, but only when
/// the title is a custom name. When the title is derived it already contains
/// the level, and showing it again is the duplication this module exists to
/// remove.
String? certificationSubtitle(Certification cert) =>
customNameOrNull(cert) == null ? null : cert.level?.displayName;
customNameOrNull(cert) == null
? null
: effectiveLevelLabel(cert.level, cert.levelCustom);
18 changes: 18 additions & 0 deletions lib/features/certifications/domain/entities/certification.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ class Certification extends Equatable {
final String? buddyId;
final String name;
final CertificationAgency agency;

/// Free-text agency name when [agency] is [CertificationAgency.other] and the
/// diver's agency isn't in the list. Null otherwise.
final String? agencyCustom;
final CertificationLevel? level;

/// Free-text level/certification name when [level] is
/// [CertificationLevel.other]. Null otherwise.
final String? levelCustom;
final String? cardNumber;
final DateTime? issueDate;
final DateTime? expiryDate;
Expand All @@ -34,7 +42,9 @@ class Certification extends Equatable {
this.buddyId,
required this.name,
required this.agency,
this.agencyCustom,
this.level,
this.levelCustom,
this.cardNumber,
this.issueDate,
this.expiryDate,
Expand Down Expand Up @@ -88,7 +98,9 @@ class Certification extends Equatable {
String? buddyId,
String? name,
CertificationAgency? agency,
String? agencyCustom,
CertificationLevel? level,
String? levelCustom,
String? cardNumber,
DateTime? issueDate,
DateTime? expiryDate,
Expand All @@ -107,7 +119,9 @@ class Certification extends Equatable {
buddyId: buddyId ?? this.buddyId,
name: name ?? this.name,
agency: agency ?? this.agency,
agencyCustom: agencyCustom ?? this.agencyCustom,
level: level ?? this.level,
levelCustom: levelCustom ?? this.levelCustom,
cardNumber: cardNumber ?? this.cardNumber,
issueDate: issueDate ?? this.issueDate,
expiryDate: expiryDate ?? this.expiryDate,
Expand All @@ -130,7 +144,9 @@ class Certification extends Equatable {
buddyId: buddyId,
name: name,
agency: agency,
agencyCustom: agencyCustom,
level: level,
levelCustom: levelCustom,
cardNumber: cardNumber,
issueDate: issueDate,
expiryDate: expiryDate,
Expand Down Expand Up @@ -164,7 +180,9 @@ class Certification extends Equatable {
buddyId,
name,
agency,
agencyCustom,
level,
levelCustom,
cardNumber,
issueDate,
expiryDate,
Expand Down
Loading
Loading