From 92578ba62b6c2525700e2b47ae67d6c762c469d7 Mon Sep 17 00:00:00 2001 From: Betsy Castro <5490820+betsyecastro@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:07:26 -0500 Subject: [PATCH 01/25] Read, import and display patents data --- .../ImportPatentsToProfilesCommand.php | 156 +++++++ app/Http/Livewire/ProfileDataCard.php | 1 + app/Profile.php | 11 + app/ProfileData.php | 179 ++++++++ app/Services/PatentFamilyGrouper.php | 235 +++++++++++ app/Traits/ReadsPatentSpreadsheets.php | 70 ++++ composer.json | 1 + composer.lock | 382 ++++++++++++++---- .../profile-data-cards/patents.blade.php | 11 + resources/views/profiles/show.blade.php | 2 + 10 files changed, 965 insertions(+), 83 deletions(-) create mode 100644 app/Console/Commands/ImportPatentsToProfilesCommand.php create mode 100644 app/Services/PatentFamilyGrouper.php create mode 100644 app/Traits/ReadsPatentSpreadsheets.php create mode 100644 resources/views/livewire/profile-data-cards/patents.blade.php diff --git a/app/Console/Commands/ImportPatentsToProfilesCommand.php b/app/Console/Commands/ImportPatentsToProfilesCommand.php new file mode 100644 index 00000000..f5f2c1e0 --- /dev/null +++ b/app/Console/Commands/ImportPatentsToProfilesCommand.php @@ -0,0 +1,156 @@ +resolvePath($this->argument('input')); + $dry_run = (bool) $this->option('dry-run'); + + if (! is_file($input_path)) { + $this->error("Input file not found: {$input_path}"); + return self::FAILURE; + } + + $this->info("Reading: {$input_path}"); + $rows = $this->readSpreadsheetRows($input_path); + $this->info('Rows read: ' . count($rows)); + + if (empty($rows)) { + $this->error('No rows to process.'); + return self::FAILURE; + } + + $curated = $grouper->groupStrict($rows); + $this->info('Curated groups: ' . count($curated)); + + $net_ids_in_csv = array_values(array_unique(array_filter(array_map( + fn ($r) => trim((string) ($r['NetID'] ?? '')), + $rows + )))); + $this->info('Unique NetIDs in CSV: ' . count($net_ids_in_csv)); + + $profiles = Profile::with('user') + ->withWhereHas('user', fn ($q) => $q->whereIn('name', $net_ids_in_csv)) + ->get() + ->keyBy(fn ($p) => $p->user->name); + + $this->info('Profiles matched: ' . $profiles->count()); + + $unmatched = array_diff($net_ids_in_csv, $profiles->keys()->all()); + if (! empty($unmatched)) { + $this->warn('NetIDs in CSV with no matching profile: ' . count($unmatched)); + foreach ($unmatched as $netId) { + $this->warn(" - {$netId}"); + Log::warning("[patents:import] No profile for NetID '{$netId}'"); + } + } + + $curated_by_net_id = []; + foreach ($curated as $entry) { + $curated_by_net_id[$entry['net_id']][] = $entry; + } + + $stats = ['profiles_processed' => 0, 'rows_wiped' => 0, 'rows_inserted' => 0]; + + if ($dry_run) { + $this->warn('Dry run — no changes persisted.'); + } + + foreach ($profiles as $netId => $profile) { + $entries = $curated_by_net_id[$netId] ?? []; + + if (empty($entries)) { + continue; + } + + $stats['profiles_processed']++; + $this->line("Profile #{$profile->id} ({$netId}): " . count($entries) . ' familie(s)'); + + if ($dry_run) { + $stats['rows_inserted'] += count($entries); + continue; + } + + DB::transaction(function () use ($profile, $entries, &$stats) { + $wiped = ProfileData::where('profile_id', $profile->id) + ->where('type', 'patents') + ->delete(); + $stats['rows_wiped'] += $wiped; + + foreach ($entries as $entry) { + ProfileData::create([ + 'profile_id' => $profile->id, + 'type' => 'patents', + 'data' => $this->buildJsonPayload($entry), + ]); + $stats['rows_inserted']++; + } + }); + } + + $this->info('---'); + $this->info('Profiles processed: ' . $stats['profiles_processed']); + $this->info('Existing patents rows ' . ($dry_run ? 'would be ' : '') . 'wiped: ' . $stats['rows_wiped']); + $this->info('profile_data rows ' . ($dry_run ? 'would be ' : '') . 'inserted: ' . $stats['rows_inserted']); + + return self::SUCCESS; + } + + private function buildJsonPayload(array $entry): array + { + $members = []; + $i = 1; + foreach ($entry['rows'] as $r) { + $patent_no = trim((string) ($r['Patent No.'] ?? '')) ?: null; + + $members['patent_' . $i] = [ + 'patent_internal_id' => trim((string) ($r['Patent Internal ID'] ?? '')), + 'patent_title' => trim((string) ($r['Patent Title'] ?? '')), + 'country' => trim((string) ($r['Country'] ?? '')) ?: null, + 'patent_no' => $patent_no, + 'status' => 'granted', + 'filed_date' => null, + 'issued_date' => $this->parseDate((string) ($r['Filed Date'] ?? '')), + 'department' => trim((string) ($r['Department'] ?? '')) ?: null, + ]; + $i++; + } + + return [ + 'title' => $entry['canonical_title'], + 'co_inventors' => $entry['co_inventors'], + 'family_prefix' => $entry['family_prefix'], + 'total_filings' => $entry['total_filings'], + 'countries' => $entry['countries'], + 'patent_numbers' => $entry['patent_numbers'], + 'members' => $members, + ]; + } + + private function resolvePath(string $path): string + { + if (str_starts_with($path, '/') || preg_match('/^[A-Z]:\\\\/', $path)) { + return $path; + } + return base_path($path); + } +} \ No newline at end of file diff --git a/app/Http/Livewire/ProfileDataCard.php b/app/Http/Livewire/ProfileDataCard.php index 0e087898..7f0067e0 100644 --- a/app/Http/Livewire/ProfileDataCard.php +++ b/app/Http/Livewire/ProfileDataCard.php @@ -15,6 +15,7 @@ class ProfileDataCard extends Component 'publications' => 8, 'appointments' => 10, 'awards' => 10, + 'patents' => 8, 'news' => 5, 'support' => 5, 'presentations' => 5, diff --git a/app/Profile.php b/app/Profile.php index d8f6f0e7..ed071e5c 100644 --- a/app/Profile.php +++ b/app/Profile.php @@ -767,6 +767,17 @@ public function awards() { return $this->data()->awards(); } + + /** + * This has many patents. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + + public function patents() + { + return $this->data()->patents(); + } /** * This has many areas. diff --git a/app/ProfileData.php b/app/ProfileData.php index 9685d6b7..48a5c67c 100644 --- a/app/ProfileData.php +++ b/app/ProfileData.php @@ -181,6 +181,174 @@ public function getImageAttribute() return $this->getFirstMedia('images'); } + /** + * Build a citation string from the patent's title and members. + * + * Format: "Title - Country - Number (mm/dd/yyyy), Country - pending - Number (mm/dd/yyyy), ..." + * + * - Granted members: "Country - Number (issued_date)" + * - Pending members: "Country - pending - Number (filed_date)" + * - European countries sharing a patent number collapse into one "EPO - Number" entry + * using the earliest issued_date among them + */ + public function getCitationAttribute(): ?string + { + if ($this->type !== 'patents') { + return null; + } + + $data = $this->data; + $title = trim((string) ($data['title'] ?? '')); + $co_inventors = trim((string) ($data['co_inventors'] ?? '')); + $members = $data['members'] ?? []; + + if (!is_array($members) || empty($members)) { + $base = $title !== '' ? $title : null; + if ($base !== null && $co_inventors !== '') { + return "{$base} - co-inventors: {$co_inventors}"; + } + return $base; + } + + $entries = []; + $ep_buckets = []; + + $european_countries = [ + 'European', 'Belgium', 'Switzerland', 'Germany', 'Spain', 'France', + 'United Kingdom', 'Ireland', 'Italy', 'The Netherlands', + ]; + + foreach ($members as $member) { + $country = trim((string) ($member['country'] ?? '')); + $status = $member['status'] ?? 'granted'; + $patent_no = trim((string) ($member['patent_no'] ?? '')); + $internal_id = trim((string) ($member['patent_internal_id'] ?? '')); + + $identifier = $patent_no !== '' ? $patent_no : $internal_id; + if ($country === '' || $identifier === '') { + continue; + } + + if ($status === 'granted' + && $patent_no !== '' + && in_array($country, $european_countries, true)) { + + $date_str = trim((string) ($member['issued_date'] ?? '')); + $date_sortable = $this->dateSortable($date_str); + + if (!isset($ep_buckets[$patent_no])) { + $ep_buckets[$patent_no] = [ + 'patent_no' => $patent_no, + 'date_display' => $this->formatCitationDate($date_str), + 'date_sortable' => $date_sortable, + ]; + } elseif ($date_sortable < $ep_buckets[$patent_no]['date_sortable']) { + $ep_buckets[$patent_no]['date_display'] = $this->formatCitationDate($date_str); + $ep_buckets[$patent_no]['date_sortable'] = $date_sortable; + } + continue; + } + + $entries[] = $this->formatCitationMember($member); + } + + foreach ($ep_buckets as $bucket) { + $entry = "EPO - {$bucket['patent_no']}"; + if ($bucket['date_display'] !== '') { + $entry .= " ({$bucket['date_display']})"; + } + $entries[] = $entry; + } + + $entries = array_filter($entries, fn ($e) => $e !== null && $e !== ''); + + if (empty($entries)) { + $base = $title !== '' ? $title : null; + if ($base !== null && $co_inventors !== '') { + return "{$base} - co-inventors: {$co_inventors}"; + } + return $base; + } + + $citation = $title !== '' + ? "{$title} - " . implode(', ', $entries) + : implode(', ', $entries); + + if ($co_inventors !== '') { + $citation .= " - co-inventors: {$co_inventors}"; + } + + return $citation; + } + + /** + * Format a single member as a citation entry. + */ + private function formatCitationMember(array $member): ?string + { + $country = trim((string) ($member['country'] ?? '')); + $status = $member['status'] ?? 'granted'; + $patent_no = trim((string) ($member['patent_no'] ?? '')); + $internal_id = trim((string) ($member['patent_internal_id'] ?? '')); + + $identifier = $patent_no !== '' ? $patent_no : $internal_id; + if ($country === '' || $identifier === '') { + return null; + } + + $country_display = match ($country) { + 'European' => 'EPO', + 'United States' => 'US', + 'Korea (Republic of)' => 'Korea', + default => $country, + }; + + $date_field = $status === 'pending' ? 'filed_date' : 'issued_date'; + $date_str = trim((string) ($member[$date_field] ?? '')); + $date_display = $this->formatCitationDate($date_str); + + $entry = $country_display; + if ($status === 'pending') { + $entry .= ' - pending'; + } + $entry .= " - {$identifier}"; + if ($date_display !== '') { + $entry .= " ({$date_display})"; + } + + return $entry; + } + + /** + * Format a date string as mm/dd/yyyy. Returns empty string if unparseable. + */ + private function formatCitationDate(string $date_str): string + { + if ($date_str === '') { + return ''; + } + try { + return \Carbon\Carbon::parse($date_str)->format('m/d/Y'); + } catch (\Throwable) { + return ''; + } + } + + /** + * Return a sortable integer (YYYYMMDD) from a date string. Returns PHP_INT_MAX if unparseable. + */ + private function dateSortable(string $date_str): int + { + if ($date_str === '') { + return PHP_INT_MAX; + } + try { + return (int) \Carbon\Carbon::parse($date_str)->format('Ymd'); + } catch (\Throwable) { + return PHP_INT_MAX; + } + } + /** * Get the image URL. ($this->image_url) * @@ -250,6 +418,17 @@ public function scopeAwards($query) return $query->where('type', 'awards'); } + /** + * Query scope for patents + * + * @param \Illuminate\Database\Query\Builder $query + * @return \Illuminate\Database\Query\Builder + */ + public function scopePatents($query) + { + return $query->where('type', 'patents'); + } + /** * Query scope for * diff --git a/app/Services/PatentFamilyGrouper.php b/app/Services/PatentFamilyGrouper.php new file mode 100644 index 00000000..ea6636cd --- /dev/null +++ b/app/Services/PatentFamilyGrouper.php @@ -0,0 +1,235 @@ +familyPrefix($patent_internal_id), false]; + } + + public function normalizeTitle(string $title): string + { + $t = strtolower(trim($title)); + $t = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $t); + $t = preg_replace('/\s+/', ' ', $t); + return trim($t); + } + + public function groupStrict(array $rows): array + { + $co_inventor_index = $this->buildCoInventorIndex($rows); + + $groups = []; + + foreach ($rows as $row) { + $net_id = trim((string) ($row['NetID'] ?? '')); + $pid = (string) ($row['Patent Internal ID'] ?? ''); + $title = (string) ($row['Patent Title'] ?? ''); + + if ($net_id === '' || $pid === '') { + continue; + } + + $normalized_title = $this->normalizeTitle($title); + [$grouping_prefix, $include_country] = $this->resolveGroupingKey($pid); + + $country_part = ''; + + if ($include_country) { + $country = trim((string) ($row['Country'] ?? '')); + if (in_array($country, self::EUROPEAN_COUNTRIES, true)) { + $country = 'EP'; + } + $country_part = '||' . $country; + } + + $key = $net_id . '||' . $normalized_title . '||' . $grouping_prefix . $country_part; + + if (! isset($groups[$key])) { + $groups[$key] = [ + 'net_id' => $net_id, + 'inventor' => (string) ($row['Inventor'] ?? ''), + 'family_prefix' => $grouping_prefix, + 'normalized_title' => $normalized_title, + 'canonical_title' => trim($title), + 'rows' => [], + ]; + } + + $clean_title = preg_replace('/\s+/', ' ', trim($title)); + if (strlen($clean_title) > strlen($groups[$key]['canonical_title'])) { + $groups[$key]['canonical_title'] = $clean_title; + } + + $groups[$key]['rows'][] = $row; + } + + $curated = []; + foreach ($groups as $group) { + $curated[] = $this->buildEntry($group, $co_inventor_index); + } + + usort($curated, fn ($a, $b) => + [$a['inventor'], $a['canonical_title'], $a['family_prefix']] + <=> [$b['inventor'], $b['canonical_title'], $b['family_prefix']] + ); + + return $curated; + } + + private function buildCoInventorIndex(array $rows): array + { + $by_patent = []; + + foreach ($rows as $row) { + $prefix = $this->familyPrefix((string) ($row['Patent Internal ID'] ?? '')); + $country = trim((string) ($row['Country'] ?? '')); + $patent_no = trim((string) ($row['Patent No.'] ?? '')); + $inventor = trim((string) ($row['Inventor'] ?? '')); + + if ($patent_no === '' || $inventor === '') { + continue; + } + + $key = $prefix . '|' . $country . '|' . $patent_no; + if (! isset($by_patent[$key])) { + $by_patent[$key] = []; + } + if (! in_array($inventor, $by_patent[$key], true)) { + $by_patent[$key][] = $inventor; + } + } + + return $by_patent; + } + + private function formatInventorName(string $full_name): string + { + $parts = explode(',', $full_name, 2); + if (count($parts) !== 2) { + return trim($full_name); + } + + $last = trim($parts[0]); + $first = trim($parts[1]); + + if ($first === '') { + return $last; + } + + $initial = mb_strtoupper(mb_substr($first, 0, 1)); + return $initial . '. ' . $last; + } + + private function buildEntry(array $group, array $co_inventor_index): array + { + $countries = []; + $patent_numbers = []; + $co_inventor_names = []; + + foreach ($group['rows'] as $r) { + $c = trim((string) ($r['Country'] ?? '')); + if ($c !== '' && ! in_array($c, $countries, true)) { + $countries[] = $c; + } + + $p = trim((string) ($r['Patent No.'] ?? '')); + if ($p !== '' && ! in_array($p, $patent_numbers, true)) { + $patent_numbers[] = $p; + } + + $prefix = $this->familyPrefix((string) ($r['Patent Internal ID'] ?? '')); + $co_key = $prefix . '|' . $c . '|' . $p; + $this_inventor = trim((string) ($r['Inventor'] ?? '')); + + foreach ($co_inventor_index[$co_key] ?? [] as $name) { + if ($name !== $this_inventor && ! in_array($name, $co_inventor_names, true)) { + $co_inventor_names[] = $name; + } + } + } + + $co_inventors_formatted = implode(', ', array_map( + fn ($name) => $this->formatInventorName($name), + $co_inventor_names + )); + + return [ + 'net_id' => $group['net_id'], + 'inventor' => $group['inventor'], + 'canonical_title' => $this->toTitleCase($group['canonical_title']), + 'normalized_title' => $group['normalized_title'], + 'family_prefix' => $group['family_prefix'], + 'total_filings' => count($group['rows']), + 'countries' => $countries, + 'patent_numbers' => $patent_numbers, + 'co_inventors' => $co_inventors_formatted, + 'rows' => $group['rows'], + ]; + } + + private function toTitleCase(string $title): string + { + $words = preg_split('/(\s+)/', trim($title), -1, PREG_SPLIT_DELIM_CAPTURE); + $result = ''; + $word_index = 0; + + foreach ($words as $part) { + if (preg_match('/^\s+$/', $part)) { + $result .= $part; + continue; + } + + $lower = mb_strtolower($part); + + if ($word_index === 0 || mb_strlen($part) > 4) { + $result .= mb_strtoupper(mb_substr($lower, 0, 1)) . mb_substr($lower, 1); + } else { + $result .= $lower; + } + + $word_index++; + } + + return $result; + } +} \ No newline at end of file diff --git a/app/Traits/ReadsPatentSpreadsheets.php b/app/Traits/ReadsPatentSpreadsheets.php new file mode 100644 index 00000000..1ace7790 --- /dev/null +++ b/app/Traits/ReadsPatentSpreadsheets.php @@ -0,0 +1,70 @@ +> + */ + protected function readSpreadsheetRows(string $path, ?string $sheetName = null): array + { + $spreadsheet = IOFactory::load($path); + + if ($sheetName) { + $sheet = null; + foreach ($spreadsheet->getAllSheets() as $s) { + if (strcasecmp($s->getTitle(), $sheetName) === 0) { + $sheet = $s; + break; + } + } + $sheet ??= $spreadsheet->getActiveSheet(); + } else { + $sheet = $spreadsheet->getActiveSheet(); + } + + $data = $sheet->toArray(null, true, true, false); + + if (count($data) < 2) { + return []; + } + + $headers = array_map(fn ($h) => trim((string) $h), array_shift($data)); + $rows = []; + + foreach ($data as $raw) { + if (count(array_filter($raw, fn ($v) => $v !== null && $v !== '')) === 0) { + continue; + } + $assoc = []; + foreach ($headers as $i => $header) { + $assoc[$header] = $raw[$i] ?? null; + } + $rows[] = $assoc; + } + + return $rows; + } + + protected function parseDate(?string $value): ?string + { + if ($value === null || trim($value) === '') { + return null; + } + try { + return \Carbon\Carbon::createFromFormat('n/j/y', trim($value))->format('Y-m-d'); + } catch (\Throwable) { + try { + return \Carbon\Carbon::parse($value)->format('Y-m-d'); + } catch (\Throwable) { + return null; + } + } + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index cbffc480..2d77ed5f 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "league/flysystem-aws-s3-v3": "^3.0", "livewire/livewire": "^2.10", "owen-it/laravel-auditing": "^13.0", + "phpoffice/phpspreadsheet": "^5.7", "predis/predis": "^1.1", "sentry/sentry-laravel": "^4.15.0", "spatie/browsershot": "^5.1.0", diff --git a/composer.lock b/composer.lock index a690b868..1274b929 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8ff5298aa8683b4eae7f0de259ddc9c2", + "content-hash": "8410e85979806935951a534cebe4c5a8", "packages": [ { "name": "adldap2/adldap2", @@ -411,6 +411,85 @@ ], "time": "2023-12-11T17:09:12+00:00" }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -2963,6 +3042,113 @@ ], "time": "2025-07-17T11:15:13+00:00" }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", @@ -3700,6 +3886,115 @@ }, "time": "2025-02-21T14:58:02+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "5.7.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8", + "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-filter": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^8.1", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^2.0 || ^3.0", + "ext-intl": "*", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.5", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1 || ^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.7.0" + }, + "time": "2026-04-20T02:42:17+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.4", @@ -8825,85 +9120,6 @@ ], "time": "2025-08-20T18:52:43+00:00" }, - { - "name": "composer/pcre", - "version": "3.3.2", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.10" - }, - "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" - }, - "type": "library", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-11-12T16:29:46+00:00" - }, { "name": "composer/semver", "version": "3.4.4", @@ -12527,12 +12743,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.3.0" }, - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/resources/views/livewire/profile-data-cards/patents.blade.php b/resources/views/livewire/profile-data-cards/patents.blade.php new file mode 100644 index 00000000..aa5a7b1f --- /dev/null +++ b/resources/views/livewire/profile-data-cards/patents.blade.php @@ -0,0 +1,11 @@ +
+

Patents @if($editable) Edit@endif

+ @foreach($data as $patent) +
+ {{$patent->title}} - {{ $patent->citation }}.
+
+ @endforeach + @if($paginated) + {{ $data->links() }} + @endif +
\ No newline at end of file diff --git a/resources/views/profiles/show.blade.php b/resources/views/profiles/show.blade.php index 173c69e3..e23a8616 100644 --- a/resources/views/profiles/show.blade.php +++ b/resources/views/profiles/show.blade.php @@ -125,6 +125,7 @@ @if($profile->publications()->exists())
  • Publications
  • @endif @if($profile->appointments()->exists())
  • Appointments
  • @endif @if($profile->awards()->exists())
  • Awards
  • @endif + @if($profile->patents()->exists())
  • Patents
  • @endif @if($profile->projects()->exists())
  • Projects
  • @endif @if($profile->presentations()->exists())
  • Presentations
  • @endif @if($profile->additionals()->exists())
  • Additional Information
  • @endif @@ -139,6 +140,7 @@ + From 8c2c337a818a8f24d4d6572150ad16b101465331 Mon Sep 17 00:00:00 2001 From: Betsy Castro <5490820+betsyecastro@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:08:04 -0500 Subject: [PATCH 02/25] Edit and update grouped patents records --- app/Profile.php | 32 +++ public/js/app.js | 223 ++++++++++++++++ public/mix-manifest.json | 4 +- resources/assets/js/app.js | 246 +++++++++++++++++- .../edit/_patent_member_fields.blade.php | 62 +++++ .../views/profiles/edit/patents.blade.php | 91 +++++++ 6 files changed, 655 insertions(+), 3 deletions(-) create mode 100644 resources/views/profiles/edit/_patent_member_fields.blade.php create mode 100644 resources/views/profiles/edit/patents.blade.php diff --git a/app/Profile.php b/app/Profile.php index ed071e5c..51c923e8 100644 --- a/app/Profile.php +++ b/app/Profile.php @@ -240,6 +240,7 @@ public function updateDatum($section, $request) //iterate over each record foreach($request->data as $entry){ + $entry = $this->normalizeMembers($entry); $should_save = false; //do we have any data in the record and should we save it foreach($entry['data'] as $key => $value){ @@ -311,6 +312,37 @@ public function updateDatum($section, $request) Cache::tags(['profile_data'])->flush(); } + /** + * Renumber the members object so keys are sequential patent_1, patent_2, ... + */ + private function normalizeMembers(array $entry, string $parent_key = 'patent'): array + { + if (!isset($entry['data']['members']) || !is_array($entry['data']['members'])) { + return $entry; + } + + $renumbered = []; + $i = 1; + foreach ($entry['data']['members'] as $member) { + // Skip entirely empty members (all fields blank) + $hasContent = false; + foreach ($member as $value) { + if (!empty($value)) { + $hasContent = true; + break; + } + } + if (!$hasContent) { + continue; + } + $renumbered["{$parent_key}_{$i}"] = $member; + $i++; + } + + $entry['data']['members'] = $renumbered; + return $entry; + } + /** * Strips HTML tags from the specified data field. * diff --git a/public/js/app.js b/public/js/app.js index 07d287a3..b18858ac 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -252,6 +252,19 @@ var profiles = function ($, undefined) { item_container.append(new_item); } $(new_item).slideDown(); + if (new_item.dataset.supportsSubrows === 'true') { + var subrecords_container = new_item.querySelector('.subrecords'); + if (subrecords_container) { + // Remove any cloned subrecords (template stays, real ones get cleared for a fresh row) + subrecords_container.querySelectorAll(':scope > .subrecord:not(.template)').forEach(function (el) { + return el.remove(); + }); + subrecords_container.dataset.nextSubrowId = '0'; + subrecords_container.style.display = 'none'; + } + update_subrecord_count(new_item); + auto_expand_subrecords(new_item); + } } }; @@ -606,20 +619,215 @@ var profiles = function ($, undefined) { icon.className = "fas fa-play"; } }; + + /** + * Toggle expand/collapse of the subrecords section for a parent row. + * + * @param {Event} event + */ + var toggle_subrecords = function toggle_subrecords(event) { + var button = event.currentTarget; + var row = button.closest('.record'); + var subrecords = row === null || row === void 0 ? void 0 : row.querySelector('.subrecords'); + if (!subrecords) return; + var is_expanded = button.getAttribute('aria-expanded') === 'true'; + button.setAttribute('aria-expanded', String(!is_expanded)); + var icon = button.querySelector('i'); + if (icon) { + icon.classList.toggle('fa-chevron-right', is_expanded); + icon.classList.toggle('fa-chevron-down', !is_expanded); + } + if (is_expanded) { + $(subrecords).slideUp(200); + } else { + $(subrecords).slideDown(200); + } + }; + + /** + * Update the badge showing the number of subrecords for a parent row. + * + * @param {HTMLElement} row - the parent .record element + */ + var update_subrecord_count = function update_subrecord_count(row) { + if (!row) return; + var count = row.querySelectorAll('.subrecords > .subrecord:not(.template)').length; + var badge = row.querySelector('.subrecord-count'); + if (badge) { + badge.textContent = String(count); + } + }; + + /** + * Set the initial expanded/collapsed state of a row's subrecords section. + * Expanded if any subrecords exist, collapsed if empty. + * + * @param {HTMLElement} row - the parent .record element + */ + var auto_expand_subrecords = function auto_expand_subrecords(row) { + if (!row || row.dataset.supportsSubrows !== 'true') return; + var subrecords = row.querySelector('.subrecords'); + var button = row.querySelector('[data-toggle-subrecords]'); + if (!subrecords || !button) return; + var count = subrecords.querySelectorAll(':scope > .subrecord:not(.template)').length; + var should_expand = count > 0; + button.setAttribute('aria-expanded', String(should_expand)); + var icon = button.querySelector('i'); + if (icon) { + icon.classList.toggle('fa-chevron-down', should_expand); + icon.classList.toggle('fa-chevron-right', !should_expand); + } + subrecords.style.display = should_expand ? 'block' : 'none'; + }; + + /** + * Replace placeholder strings in a subrecord clone's name/id/for attributes. + * + * @param {HTMLElement} element - the cloned subrecord + * @param {string} parent_id - the parent row's data-row-id + * @param {string} subrow_id - the new subrow's data-subrow-id + */ + var reindex_subrecord = function reindex_subrecord(element, parent_id, subrow_id) { + ['name', 'id', 'for'].forEach(function (attr) { + element.querySelectorAll("[".concat(attr, "]")).forEach(function (field) { + var original = field.getAttribute(attr); + if (original) { + field.setAttribute(attr, original.replace(/__parent__/g, parent_id).replace(/__index__/g, subrow_id)); + } + }); + }); + }; + + /** + * Wire up the click handlers and pickers for a single subrow. + * + * @param {HTMLElement} subrow + */ + var wire_subrow_actions = function wire_subrow_actions(subrow) { + subrow.querySelectorAll('.trash').forEach(function (el) { + $(el).off('click.subrow').on('click.subrow', remove_subrow); + }); + subrow.querySelectorAll('.subrow-duplicate').forEach(function (el) { + $(el).off('click.subrow').on('click.subrow', duplicate_subrow); + }); + subrow.querySelectorAll('.datepicker.year').forEach(function (el) { + $(el).datepicker(config.datepicker.year); + }); + subrow.querySelectorAll('.datepicker.month').forEach(function (el) { + $(el).datepicker(config.datepicker.month); + }); + }; + + /** + * Add a new (empty) subrecord row to a parent row's subrecords container. + * + * @param {Event} event - triggered by clicking [data-toggle="add_subrow"] + */ + var add_subrow = function add_subrow(event) { + var button = event.currentTarget; + var row = button.closest('.record'); + if (!row || row.dataset.supportsSubrows !== 'true') return; + var subrecords = row.querySelector('.subrecords'); + var template = subrecords === null || subrecords === void 0 ? void 0 : subrecords.querySelector('.subrecord.template'); + if (!subrecords || !template) return; + var parent_id = row.dataset.rowId; + var next_id = subrecords.dataset.nextSubrowId || '0'; + subrecords.dataset.nextSubrowId = String(Number(next_id) + 1); + var new_subrow = template.cloneNode(true); + new_subrow.classList.remove('template'); + new_subrow.dataset.subrowId = next_id; + new_subrow.style.display = ''; + reindex_subrecord(new_subrow, parent_id, next_id); + wire_subrow_actions(new_subrow); + $(new_subrow).hide(); + subrecords.appendChild(new_subrow); + $(new_subrow).slideDown(200); + + // Make sure the section is expanded after adding + var toggle = row.querySelector('[data-toggle-subrecords]'); + if (toggle && toggle.getAttribute('aria-expanded') !== 'true') { + toggle.click(); + } + update_subrecord_count(row); + }; + + /** + * Duplicate an existing subrecord (copying its values) and append after it. + * + * @param {Event} event - triggered by clicking .subrow-duplicate + */ + var duplicate_subrow = function duplicate_subrow(event) { + var button = event.currentTarget; + var source = button.closest('.subrecord'); + var row = button.closest('.record'); + var subrecords = row === null || row === void 0 ? void 0 : row.querySelector('.subrecords'); + if (!source || !row || !subrecords) return; + var next_id = subrecords.dataset.nextSubrowId || '0'; + subrecords.dataset.nextSubrowId = String(Number(next_id) + 1); + var new_subrow = source.cloneNode(true); + new_subrow.dataset.subrowId = next_id; + + // Rename name/id/for attributes from source index to next_id + var search_for = new RegExp("\\[members\\]\\[patent_".concat(source.dataset.subrowId, "\\]"), 'g'); + var replace_with = "[members][patent_".concat(next_id, "]"); + ['name', 'id', 'for'].forEach(function (attr) { + new_subrow.querySelectorAll("[".concat(attr, "]")).forEach(function (field) { + var original = field.getAttribute(attr); + if (original) { + field.setAttribute(attr, original.replace(search_for, replace_with)); + } + }); + }); + + // Copy actual input VALUES (cloneNode doesn't copy user-entered values) + var source_inputs = source.querySelectorAll('input, textarea, select'); + var new_inputs = new_subrow.querySelectorAll('input, textarea, select'); + source_inputs.forEach(function (src, i) { + if (new_inputs[i]) new_inputs[i].value = src.value; + }); + wire_subrow_actions(new_subrow); + $(new_subrow).hide(); + source.insertAdjacentElement('afterend', new_subrow); + $(new_subrow).slideDown(200); + update_subrecord_count(row); + }; + + /** + * Remove a subrecord row. + * + * @param {Event} event - triggered by clicking .trash inside a subrecord + */ + var remove_subrow = function remove_subrow(event) { + var button = event.currentTarget; + var subrow = button.closest('.subrecord'); + var row = button.closest('.record'); + if (!subrow || !row) return; + $(subrow).slideUp(200, function () { + subrow.remove(); + update_subrecord_count(row); + }); + }; return { add_row: add_row, + add_subrow: add_subrow, + auto_expand_subrecords: auto_expand_subrecords, clear_row: clear_row, config: config, deobfuscate_mail_links: deobfuscate_mail_links, + duplicate_subrow: duplicate_subrow, on_list_updated: on_list_updated, preview_selected_image: preview_selected_image, + remove_subrow: remove_subrow, replace_icon: replace_icon, registerTagEditors: registerTagEditors, registerProfilePickers: registerProfilePickers, toast: toast, toggle_class: toggle_class, toggle_show: toggle_show, + toggle_subrecords: toggle_subrecords, + update_subrecord_count: update_subrecord_count, wait_when_submitting: wait_when_submitting, + wire_subrow_actions: wire_subrow_actions, registerVideoControls: registerVideoControls }; }(jQuery); @@ -700,6 +908,21 @@ $(function () { if (document.querySelectorAll('.video-cover video').length > 0 && document.querySelectorAll('.video-cover .video-control').length > 0) { profiles.registerVideoControls(); } + + // Subrecords: expand/collapse toggle (delegated so it works for new rows too) + $(document).on('click', '[data-toggle-subrecords]', profiles.toggle_subrecords); + + // Subrecords: add new subrow (delegated) + $(document).on('click', '[data-toggle="add_subrow"]', profiles.add_subrow); + + // Subrecords: initialize state and wire actions on existing rows + document.querySelectorAll('.record[data-supports-subrows="true"]').forEach(function (row) { + profiles.update_subrecord_count(row); + profiles.auto_expand_subrecords(row); + row.querySelectorAll('.subrecords > .subrecord:not(.template)').forEach(function (subrow) { + profiles.wire_subrow_actions(subrow); + }); + }); }); // Trix editor settings diff --git a/public/mix-manifest.json b/public/mix-manifest.json index dcb76aea..8b7bef0a 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -1,6 +1,6 @@ { - "/js/app.js": "/js/app.js?id=d53948d8ce9ce761bd7ca1001d67e004", - "/js/app.js.map": "/js/app.js.map?id=271c8f103c569b8f5613b8778d48ee75", + "/js/app.js": "/js/app.js?id=dd55e5216d210347a098a5ad571fb8ff", + "/js/app.js.map": "/js/app.js.map?id=f5e888cf436998e40a167a6337298ea3", "/js/manifest.js": "/js/manifest.js?id=dc9ead3d7857b522d7de22d75063453c", "/js/manifest.js.map": "/js/manifest.js.map?id=389e00e7d7680b68d4e1d128ce27ff48", "/css/app.css": "/css/app.css?id=0fd161f323dd5c77642c3240bbb47d16", diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 6c13e022..ed259742 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -201,6 +201,18 @@ var profiles = (function ($, undefined) { item_container.append(new_item); } $(new_item).slideDown(); + + if (new_item.dataset.supportsSubrows === 'true') { + const subrecords_container = new_item.querySelector('.subrecords'); + if (subrecords_container) { + // Remove any cloned subrecords (template stays, real ones get cleared for a fresh row) + subrecords_container.querySelectorAll(':scope > .subrecord:not(.template)').forEach((el) => el.remove()); + subrecords_container.dataset.nextSubrowId = '0'; + subrecords_container.style.display = 'none'; + } + update_subrecord_count(new_item); + auto_expand_subrecords(new_item); + } } } @@ -544,20 +556,236 @@ var profiles = (function ($, undefined) { } } + /** + * Toggle expand/collapse of the subrecords section for a parent row. + * + * @param {Event} event + */ + const toggle_subrecords = function (event) { + const button = event.currentTarget; + const row = button.closest('.record'); + const subrecords = row?.querySelector('.subrecords'); + if (!subrecords) return; + + const is_expanded = button.getAttribute('aria-expanded') === 'true'; + button.setAttribute('aria-expanded', String(!is_expanded)); + + const icon = button.querySelector('i'); + if (icon) { + icon.classList.toggle('fa-chevron-right', is_expanded); + icon.classList.toggle('fa-chevron-down', !is_expanded); + } + + if (is_expanded) { + $(subrecords).slideUp(200); + } else { + $(subrecords).slideDown(200); + } + }; + + /** + * Update the badge showing the number of subrecords for a parent row. + * + * @param {HTMLElement} row - the parent .record element + */ + const update_subrecord_count = function (row) { + if (!row) return; + const count = row.querySelectorAll('.subrecords > .subrecord:not(.template)').length; + const badge = row.querySelector('.subrecord-count'); + if (badge) { + badge.textContent = String(count); + } + }; + + /** + * Set the initial expanded/collapsed state of a row's subrecords section. + * Expanded if any subrecords exist, collapsed if empty. + * + * @param {HTMLElement} row - the parent .record element + */ + const auto_expand_subrecords = function (row) { + if (!row || row.dataset.supportsSubrows !== 'true') return; + + const subrecords = row.querySelector('.subrecords'); + const button = row.querySelector('[data-toggle-subrecords]'); + if (!subrecords || !button) return; + + const count = subrecords.querySelectorAll(':scope > .subrecord:not(.template)').length; + const should_expand = count > 0; + + button.setAttribute('aria-expanded', String(should_expand)); + const icon = button.querySelector('i'); + if (icon) { + icon.classList.toggle('fa-chevron-down', should_expand); + icon.classList.toggle('fa-chevron-right', !should_expand); + } + subrecords.style.display = should_expand ? 'block' : 'none'; + }; + + /** + * Replace placeholder strings in a subrecord clone's name/id/for attributes. + * + * @param {HTMLElement} element - the cloned subrecord + * @param {string} parent_id - the parent row's data-row-id + * @param {string} subrow_id - the new subrow's data-subrow-id + */ + const reindex_subrecord = function (element, parent_id, subrow_id) { + ['name', 'id', 'for'].forEach((attr) => { + element.querySelectorAll(`[${attr}]`).forEach((field) => { + const original = field.getAttribute(attr); + if (original) { + field.setAttribute( + attr, + original.replace(/__parent__/g, parent_id).replace(/__index__/g, subrow_id) + ); + } + }); + }); + }; + + /** + * Wire up the click handlers and pickers for a single subrow. + * + * @param {HTMLElement} subrow + */ + const wire_subrow_actions = function (subrow) { + subrow.querySelectorAll('.trash').forEach((el) => { + $(el).off('click.subrow').on('click.subrow', remove_subrow); + }); + subrow.querySelectorAll('.subrow-duplicate').forEach((el) => { + $(el).off('click.subrow').on('click.subrow', duplicate_subrow); + }); + subrow.querySelectorAll('.datepicker.year').forEach((el) => { + $(el).datepicker(config.datepicker.year); + }); + subrow.querySelectorAll('.datepicker.month').forEach((el) => { + $(el).datepicker(config.datepicker.month); + }); + }; + + /** + * Add a new (empty) subrecord row to a parent row's subrecords container. + * + * @param {Event} event - triggered by clicking [data-toggle="add_subrow"] + */ + const add_subrow = function (event) { + const button = event.currentTarget; + const row = button.closest('.record'); + if (!row || row.dataset.supportsSubrows !== 'true') return; + + const subrecords = row.querySelector('.subrecords'); + const template = subrecords?.querySelector('.subrecord.template'); + if (!subrecords || !template) return; + + const parent_id = row.dataset.rowId; + const next_id = subrecords.dataset.nextSubrowId || '0'; + subrecords.dataset.nextSubrowId = String(Number(next_id) + 1); + + const new_subrow = template.cloneNode(true); + new_subrow.classList.remove('template'); + new_subrow.dataset.subrowId = next_id; + new_subrow.style.display = ''; + + reindex_subrecord(new_subrow, parent_id, next_id); + wire_subrow_actions(new_subrow); + + $(new_subrow).hide(); + subrecords.appendChild(new_subrow); + $(new_subrow).slideDown(200); + + // Make sure the section is expanded after adding + const toggle = row.querySelector('[data-toggle-subrecords]'); + if (toggle && toggle.getAttribute('aria-expanded') !== 'true') { + toggle.click(); + } + + update_subrecord_count(row); + }; + + /** + * Duplicate an existing subrecord (copying its values) and append after it. + * + * @param {Event} event - triggered by clicking .subrow-duplicate + */ + const duplicate_subrow = function (event) { + const button = event.currentTarget; + const source = button.closest('.subrecord'); + const row = button.closest('.record'); + const subrecords = row?.querySelector('.subrecords'); + if (!source || !row || !subrecords) return; + + const next_id = subrecords.dataset.nextSubrowId || '0'; + subrecords.dataset.nextSubrowId = String(Number(next_id) + 1); + + const new_subrow = source.cloneNode(true); + new_subrow.dataset.subrowId = next_id; + + // Rename name/id/for attributes from source index to next_id + const search_for = new RegExp(`\\[members\\]\\[patent_${source.dataset.subrowId}\\]`, 'g'); + const replace_with = `[members][patent_${next_id}]`; + ['name', 'id', 'for'].forEach((attr) => { + new_subrow.querySelectorAll(`[${attr}]`).forEach((field) => { + const original = field.getAttribute(attr); + if (original) { + field.setAttribute(attr, original.replace(search_for, replace_with)); + } + }); + }); + + // Copy actual input VALUES (cloneNode doesn't copy user-entered values) + const source_inputs = source.querySelectorAll('input, textarea, select'); + const new_inputs = new_subrow.querySelectorAll('input, textarea, select'); + source_inputs.forEach((src, i) => { + if (new_inputs[i]) new_inputs[i].value = src.value; + }); + + wire_subrow_actions(new_subrow); + + $(new_subrow).hide(); + source.insertAdjacentElement('afterend', new_subrow); + $(new_subrow).slideDown(200); + + update_subrecord_count(row); + }; + + /** + * Remove a subrecord row. + * + * @param {Event} event - triggered by clicking .trash inside a subrecord + */ + const remove_subrow = function (event) { + const button = event.currentTarget; + const subrow = button.closest('.subrecord'); + const row = button.closest('.record'); + if (!subrow || !row) return; + + $(subrow).slideUp(200, () => { + subrow.remove(); + update_subrecord_count(row); + }); + }; + return { add_row: add_row, + add_subrow: add_subrow, + auto_expand_subrecords: auto_expand_subrecords, clear_row: clear_row, config: config, deobfuscate_mail_links: deobfuscate_mail_links, + duplicate_subrow: duplicate_subrow, on_list_updated: on_list_updated, preview_selected_image: preview_selected_image, + remove_subrow: remove_subrow, replace_icon: replace_icon, registerTagEditors: registerTagEditors, registerProfilePickers: registerProfilePickers, toast: toast, toggle_class: toggle_class, toggle_show: toggle_show, - wait_when_submitting : wait_when_submitting, + toggle_subrecords: toggle_subrecords, + update_subrecord_count: update_subrecord_count, + wait_when_submitting: wait_when_submitting, + wire_subrow_actions: wire_subrow_actions, registerVideoControls: registerVideoControls, }; @@ -643,6 +871,22 @@ $(function() { profiles.registerVideoControls(); } + // Subrecords: expand/collapse toggle (delegated so it works for new rows too) + $(document).on('click', '[data-toggle-subrecords]', profiles.toggle_subrecords); + + // Subrecords: add new subrow (delegated) + $(document).on('click', '[data-toggle="add_subrow"]', profiles.add_subrow); + + // Subrecords: initialize state and wire actions on existing rows + document.querySelectorAll('.record[data-supports-subrows="true"]').forEach((row) => { + profiles.update_subrecord_count(row); + profiles.auto_expand_subrecords(row); + + row.querySelectorAll('.subrecords > .subrecord:not(.template)').forEach((subrow) => { + profiles.wire_subrow_actions(subrow); + }); + }); + }); // Trix editor settings diff --git a/resources/views/profiles/edit/_patent_member_fields.blade.php b/resources/views/profiles/edit/_patent_member_fields.blade.php new file mode 100644 index 00000000..0997be99 --- /dev/null +++ b/resources/views/profiles/edit/_patent_member_fields.blade.php @@ -0,0 +1,62 @@ +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    \ No newline at end of file diff --git a/resources/views/profiles/edit/patents.blade.php b/resources/views/profiles/edit/patents.blade.php new file mode 100644 index 00000000..42aa2d66 --- /dev/null +++ b/resources/views/profiles/edit/patents.blade.php @@ -0,0 +1,91 @@ +@extends('profiles.edit.layout') + +@section('section_name', 'Patents') + +@php + /** + * Helper: compute the next available subrow ID for a patent. + * Looks at the existing members object keys, extracts the integer part of "patent_N", + * and returns max + 1. Returns 1 if no members exist. + */ + $next_subrow_id = function ($members) { + if (empty($members)) { + return 1; + } + $max_n = 0; + foreach (array_keys($members) as $key) { + if (preg_match('/^patent_(\d+)$/', $key, $m)) { + $max_n = max($max_n, (int) $m[1]); + } + } + return $max_n + 1; + }; +@endphp + +@section('form') + @foreach ($data as $patent) +
    + + @include('profiles.edit._actions') + +
    + + + +
    + +
    +
    + + +
    + + +
    +
    + @endforeach +@endsection \ No newline at end of file From 7977946425aca861f7f6610750a7427b28eefdc9 Mon Sep 17 00:00:00 2001 From: Betsy Castro <5490820+betsyecastro@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:26:42 -0500 Subject: [PATCH 03/25] Refactor patents import to read CSV native --- app/Traits/ReadsPatentSpreadsheets.php | 84 +++--- composer.json | 1 - composer.lock | 376 ++++++------------------- 3 files changed, 125 insertions(+), 336 deletions(-) diff --git a/app/Traits/ReadsPatentSpreadsheets.php b/app/Traits/ReadsPatentSpreadsheets.php index 1ace7790..98190a07 100644 --- a/app/Traits/ReadsPatentSpreadsheets.php +++ b/app/Traits/ReadsPatentSpreadsheets.php @@ -2,69 +2,75 @@ namespace App\Traits; -use PhpOffice\PhpSpreadsheet\IOFactory; +use Carbon\Carbon; +use RuntimeException; trait ReadsPatentSpreadsheets { /** - * Read a CSV or XLSX file and return rows keyed by header name. - * Optionally targets a specific sheet by name; defaults to the active sheet. + * Read a CSV file and return rows keyed by header name. * - * @return array> + * Expects: UTF-8 encoded CSV with a header row. + * Excel "CSV UTF-8" export works; "CSV (Comma delimited)" may need manual + * encoding conversion if your data has non-ASCII characters. */ - protected function readSpreadsheetRows(string $path, ?string $sheetName = null): array + protected function readSpreadsheetRows(string $path): array { - $spreadsheet = IOFactory::load($path); - - if ($sheetName) { - $sheet = null; - foreach ($spreadsheet->getAllSheets() as $s) { - if (strcasecmp($s->getTitle(), $sheetName) === 0) { - $sheet = $s; - break; - } - } - $sheet ??= $spreadsheet->getActiveSheet(); - } else { - $sheet = $spreadsheet->getActiveSheet(); + if (! is_file($path)) { + throw new RuntimeException("File not found: {$path}"); } - $data = $sheet->toArray(null, true, true, false); + $handle = fopen($path, 'r'); + if ($handle === false) { + throw new RuntimeException("Could not open file: {$path}"); + } - if (count($data) < 2) { - return []; + $headers = fgetcsv($handle); + if ($headers === false) { + fclose($handle); + throw new RuntimeException("Empty file or invalid CSV: {$path}"); } - $headers = array_map(fn ($h) => trim((string) $h), array_shift($data)); - $rows = []; + // Strip UTF-8 BOM from first header (Excel "CSV UTF-8" adds one) + $headers[0] = preg_replace('/^\xEF\xBB\xBF/', '', $headers[0]); + $headers = array_map('trim', $headers); - foreach ($data as $raw) { - if (count(array_filter($raw, fn ($v) => $v !== null && $v !== '')) === 0) { + $rows = []; + while (($row = fgetcsv($handle)) !== false) { + // Skip blank rows (Excel sometimes adds trailing empty rows) + $non_empty = array_filter($row, fn ($v) => $v !== null && trim((string) $v) !== ''); + if (empty($non_empty)) { continue; } - $assoc = []; - foreach ($headers as $i => $header) { - $assoc[$header] = $raw[$i] ?? null; - } - $rows[] = $assoc; + + // Pad/truncate row to match header count + $row = array_pad(array_slice($row, 0, count($headers)), count($headers), ''); + + // Trim each value + $row = array_map(fn ($v) => is_string($v) ? trim($v) : $v, $row); + + $rows[] = array_combine($headers, $row); } + fclose($handle); return $rows; } - protected function parseDate(?string $value): ?string + /** + * Parse a date string into ISO format (YYYY-MM-DD). + * Returns null for unparseable or empty input. + */ + protected function parseDate(string $value): ?string { - if ($value === null || trim($value) === '') { + $value = trim($value); + if ($value === '') { return null; } + try { - return \Carbon\Carbon::createFromFormat('n/j/y', trim($value))->format('Y-m-d'); - } catch (\Throwable) { - try { - return \Carbon\Carbon::parse($value)->format('Y-m-d'); - } catch (\Throwable) { - return null; - } + return Carbon::parse($value)->format('Y-m-d'); + } catch (\Exception $e) { + return null; } } } \ No newline at end of file diff --git a/composer.json b/composer.json index 2d77ed5f..cbffc480 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ "league/flysystem-aws-s3-v3": "^3.0", "livewire/livewire": "^2.10", "owen-it/laravel-auditing": "^13.0", - "phpoffice/phpspreadsheet": "^5.7", "predis/predis": "^1.1", "sentry/sentry-laravel": "^4.15.0", "spatie/browsershot": "^5.1.0", diff --git a/composer.lock b/composer.lock index 1274b929..6762bf83 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8410e85979806935951a534cebe4c5a8", + "content-hash": "8ff5298aa8683b4eae7f0de259ddc9c2", "packages": [ { "name": "adldap2/adldap2", @@ -411,85 +411,6 @@ ], "time": "2023-12-11T17:09:12+00:00" }, - { - "name": "composer/pcre", - "version": "3.3.2", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.10" - }, - "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" - }, - "type": "library", - "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-11-12T16:29:46+00:00" - }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -3042,113 +2963,6 @@ ], "time": "2025-07-17T11:15:13+00:00" }, - { - "name": "markbaker/complex", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/MarkBaker/PHPComplex.git", - "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", - "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "dev-master", - "phpcompatibility/php-compatibility": "^9.3", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "squizlabs/php_codesniffer": "^3.7" - }, - "type": "library", - "autoload": { - "psr-4": { - "Complex\\": "classes/src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mark Baker", - "email": "mark@lange.demon.co.uk" - } - ], - "description": "PHP Class for working with complex numbers", - "homepage": "https://github.com/MarkBaker/PHPComplex", - "keywords": [ - "complex", - "mathematics" - ], - "support": { - "issues": "https://github.com/MarkBaker/PHPComplex/issues", - "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" - }, - "time": "2022-12-06T16:21:08+00:00" - }, - { - "name": "markbaker/matrix", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/MarkBaker/PHPMatrix.git", - "reference": "728434227fe21be27ff6d86621a1b13107a2562c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", - "reference": "728434227fe21be27ff6d86621a1b13107a2562c", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "dev-master", - "phpcompatibility/php-compatibility": "^9.3", - "phpdocumentor/phpdocumentor": "2.*", - "phploc/phploc": "^4.0", - "phpmd/phpmd": "2.*", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "sebastian/phpcpd": "^4.0", - "squizlabs/php_codesniffer": "^3.7" - }, - "type": "library", - "autoload": { - "psr-4": { - "Matrix\\": "classes/src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mark Baker", - "email": "mark@demon-angel.eu" - } - ], - "description": "PHP Class for working with matrices", - "homepage": "https://github.com/MarkBaker/PHPMatrix", - "keywords": [ - "mathematics", - "matrix", - "vector" - ], - "support": { - "issues": "https://github.com/MarkBaker/PHPMatrix/issues", - "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" - }, - "time": "2022-12-02T22:17:43+00:00" - }, { "name": "monolog/monolog", "version": "3.9.0", @@ -3886,115 +3700,6 @@ }, "time": "2025-02-21T14:58:02+00:00" }, - { - "name": "phpoffice/phpspreadsheet", - "version": "5.7.0", - "source": { - "type": "git", - "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8", - "reference": "9f55d3b9b7bcb1084fda8340e4b7ce4ed10cd0c8", - "shasum": "" - }, - "require": { - "composer/pcre": "^1||^2||^3", - "ext-ctype": "*", - "ext-dom": "*", - "ext-fileinfo": "*", - "ext-filter": "*", - "ext-gd": "*", - "ext-iconv": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-simplexml": "*", - "ext-xml": "*", - "ext-xmlreader": "*", - "ext-xmlwriter": "*", - "ext-zip": "*", - "ext-zlib": "*", - "maennchen/zipstream-php": "^2.1 || ^3.0", - "markbaker/complex": "^3.0", - "markbaker/matrix": "^3.0", - "php": "^8.1", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "dev-main", - "dompdf/dompdf": "^2.0 || ^3.0", - "ext-intl": "*", - "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "^10.5", - "mpdf/mpdf": "^8.1.1", - "phpcompatibility/php-compatibility": "^9.3", - "phpstan/phpstan": "^1.1 || ^2.0", - "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", - "phpstan/phpstan-phpunit": "^1.0 || ^2.0", - "phpunit/phpunit": "^10.5", - "squizlabs/php_codesniffer": "^3.7", - "tecnickcom/tcpdf": "^6.5" - }, - "suggest": { - "dompdf/dompdf": "Option for rendering PDF with PDF Writer", - "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()", - "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", - "mpdf/mpdf": "Option for rendering PDF with PDF Writer", - "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Maarten Balliauw", - "homepage": "https://blog.maartenballiauw.be" - }, - { - "name": "Mark Baker", - "homepage": "https://markbakeruk.net" - }, - { - "name": "Franck Lefevre", - "homepage": "https://rootslabs.net" - }, - { - "name": "Erik Tilt" - }, - { - "name": "Adrien Crivelli" - }, - { - "name": "Owen Leibman" - } - ], - "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", - "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", - "keywords": [ - "OpenXML", - "excel", - "gnumeric", - "ods", - "php", - "spreadsheet", - "xls", - "xlsx" - ], - "support": { - "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.7.0" - }, - "time": "2026-04-20T02:42:17+00:00" - }, { "name": "phpoption/phpoption", "version": "1.9.4", @@ -9120,6 +8825,85 @@ ], "time": "2025-08-20T18:52:43+00:00" }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, { "name": "composer/semver", "version": "3.4.4", From e6b34997afba755a022f781b0a33019fe71ee98d Mon Sep 17 00:00:00 2001 From: Betsy Castro <5490820+betsyecastro@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:05:13 -0500 Subject: [PATCH 04/25] Fix bug undefined ID when adding a new subrow --- public/js/app.js | 8 ++++---- public/mix-manifest.json | 4 ++-- resources/assets/js/app.js | 7 +++---- resources/views/profiles/edit/patents.blade.php | 16 +++++++++------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index b18858ac..77d86f20 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -728,15 +728,15 @@ var profiles = function ($, undefined) { var row = button.closest('.record'); if (!row || row.dataset.supportsSubrows !== 'true') return; var subrecords = row.querySelector('.subrecords'); - var template = subrecords === null || subrecords === void 0 ? void 0 : subrecords.querySelector('.subrecord.template'); + var template = row.querySelector('template'); if (!subrecords || !template) return; var parent_id = row.dataset.rowId; var next_id = subrecords.dataset.nextSubrowId || '0'; subrecords.dataset.nextSubrowId = String(Number(next_id) + 1); - var new_subrow = template.cloneNode(true); - new_subrow.classList.remove('template'); + + //