Skip to content

Commit b005edc

Browse files
TortoiseWolfeclaude
andcommitted
feat(011): implement Company Management feature
- Add Companies page with full CRUD operations - Create 8 new components following 5-file pattern: - CompanyForm, CompanyTable, CompanyRow, CompanyFilters - CompanyImport, CompanyExport - HomeLocationSettings, CoordinateMap - Add company service layer with offline sync support - Implement import/export (CSV, JSON, GPX, Printable) - Add priority multi-select filter - Update database schema with companies table and RLS policies - Add home location columns to user_profiles - Include seed script for test data Note: Some service tests have mocking issues to address 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b7d674b commit b005edc

File tree

66 files changed

+12001
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+12001
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,4 @@ tasks
7272
storybook-static/
7373
.component-backup-*
7474
docs/visual-testing/
75+
master_job_tracker.csv

scripts/seed-companies.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* Seed companies from master_job_tracker.csv
3+
* Run with: docker compose exec spoketowork npx tsx scripts/seed-companies.ts
4+
*
5+
* Uses Management API to bypass RLS
6+
*/
7+
8+
import { readFileSync } from 'fs';
9+
10+
const PROJECT_REF = 'utxdunkaropkwnrqrsef';
11+
const ACCESS_TOKEN =
12+
process.env.SUPABASE_ACCESS_TOKEN ||
13+
'sbp_5ae9c1d7aee0ae94602c3054915e699a4eb8941b';
14+
const USER_ID = '05ef57ea-65a8-4694-aff6-2d8ece3dd8e5';
15+
16+
async function executeSQL(query: string): Promise<unknown[]> {
17+
const response = await fetch(
18+
`https://api.supabase.com/v1/projects/${PROJECT_REF}/database/query`,
19+
{
20+
method: 'POST',
21+
headers: {
22+
Authorization: `Bearer ${ACCESS_TOKEN}`,
23+
'Content-Type': 'application/json',
24+
},
25+
body: JSON.stringify({ query }),
26+
}
27+
);
28+
return response.json();
29+
}
30+
31+
function parsePriority(stars: string): number {
32+
// Scale 1-5: 1=highest, 5=lowest
33+
// ⭐⭐⭐=1 (highest), ⭐⭐=2, ⭐=3, no stars=5 (lowest)
34+
if (!stars) return 5;
35+
const count = (stars.match(//g) || []).length;
36+
if (count === 0) return 5;
37+
if (count === 1) return 3;
38+
if (count === 2) return 2;
39+
return 1; // 3 stars = highest priority
40+
}
41+
42+
// Simple CSV parser that handles quoted fields
43+
function parseCSV(content: string): Record<string, string>[] {
44+
const lines = content.split('\n');
45+
const headers = parseCSVLine(lines[0]);
46+
const records: Record<string, string>[] = [];
47+
48+
for (let i = 1; i < lines.length; i++) {
49+
const line = lines[i].trim();
50+
if (!line) continue;
51+
52+
const values = parseCSVLine(line);
53+
const record: Record<string, string> = {};
54+
55+
headers.forEach((header, idx) => {
56+
record[header] = values[idx] || '';
57+
});
58+
59+
records.push(record);
60+
}
61+
62+
return records;
63+
}
64+
65+
function parseCSVLine(line: string): string[] {
66+
const values: string[] = [];
67+
let current = '';
68+
let inQuotes = false;
69+
70+
for (let i = 0; i < line.length; i++) {
71+
const char = line[i];
72+
73+
if (char === '"') {
74+
if (inQuotes && line[i + 1] === '"') {
75+
current += '"';
76+
i++;
77+
} else {
78+
inQuotes = !inQuotes;
79+
}
80+
} else if (char === ',' && !inQuotes) {
81+
values.push(current.trim());
82+
current = '';
83+
} else {
84+
current += char;
85+
}
86+
}
87+
88+
values.push(current.trim());
89+
return values;
90+
}
91+
92+
async function main() {
93+
console.log('Reading CSV file...');
94+
const csvContent = readFileSync('master_job_tracker.csv', 'utf-8');
95+
96+
// Remove BOM if present
97+
const cleanContent = csvContent.replace(/^\uFEFF/, '');
98+
99+
const records = parseCSV(cleanContent);
100+
console.log(`Found ${records.length} records`);
101+
102+
// Filter to active companies only
103+
const activeCompanies = records.filter(
104+
(r) => r['Active']?.toLowerCase() === 'true'
105+
);
106+
console.log(`${activeCompanies.length} active companies to import`);
107+
108+
console.log('Inserting companies via Management API...');
109+
110+
let inserted = 0;
111+
112+
for (const row of activeCompanies) {
113+
const name = (row['Company Name'] || 'Unknown').replace(/'/g, "''");
114+
const contactName = row['Contact Name']
115+
? `'${row['Contact Name'].replace(/'/g, "''")}'`
116+
: 'NULL';
117+
const contactTitle = row['Title']
118+
? `'${row['Title'].replace(/'/g, "''")}'`
119+
: 'NULL';
120+
const phone = row['Phone']
121+
? `'${row['Phone'].replace(/'/g, "''")}'`
122+
: 'NULL';
123+
const email = row['Email']
124+
? `'${row['Email'].replace(/'/g, "''")}'`
125+
: 'NULL';
126+
const website = row['Website']
127+
? `'${row['Website'].replace(/'/g, "''")}'`
128+
: 'NULL';
129+
const address = row['Address']
130+
? `'${row['Address'].replace(/'/g, "''")}'`
131+
: 'NULL';
132+
const latitude = row['Latitude'] ? parseFloat(row['Latitude']) : 'NULL';
133+
const longitude = row['Longitude'] ? parseFloat(row['Longitude']) : 'NULL';
134+
const priority = parsePriority(row['Priority']);
135+
const notesRaw = row['Notes']
136+
? `${row['Notes']}${row['Route'] ? ' | Route: ' + row['Route'] : ''}`
137+
: row['Route'] || '';
138+
const notes = notesRaw ? `'${notesRaw.replace(/'/g, "''")}'` : 'NULL';
139+
140+
const sql = `INSERT INTO companies (user_id, name, contact_name, contact_title, phone, email, website, address, latitude, longitude, status, priority, notes) VALUES ('${USER_ID}', '${name}', ${contactName}, ${contactTitle}, ${phone}, ${email}, ${website}, ${address}, ${latitude}, ${longitude}, 'not_contacted', ${priority}, ${notes}) RETURNING id`;
141+
142+
try {
143+
const result = await executeSQL(sql);
144+
if (Array.isArray(result) && result.length > 0) {
145+
inserted++;
146+
if (inserted % 10 === 0) {
147+
console.log(`Inserted ${inserted} companies...`);
148+
}
149+
} else {
150+
console.error(`Failed to insert ${name}:`, result);
151+
}
152+
} catch (err) {
153+
console.error(`Error inserting ${name}:`, err);
154+
}
155+
}
156+
157+
console.log(`\nDone! Inserted ${inserted} companies.`);
158+
}
159+
160+
main().catch(console.error);
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Requirements Quality Checklist: Company Management
2+
3+
**Purpose**: Comprehensive author self-review before implementation
4+
**Feature**: 011-company-management
5+
**Created**: 2025-12-04
6+
**Depth**: Thorough (~40 items)
7+
**Focus**: All areas (Data Model, Offline Sync, UX, Integrations)
8+
9+
---
10+
11+
## Requirement Completeness
12+
13+
- [x] CHK001 - Are all Company entity fields explicitly defined with data types and constraints? [Completeness, Spec §FR-001] ✓ data-model.md: complete schema with types, constraints, CHECK clauses
14+
- [x] CHK002 - Are validation rules specified for each required field (name, address)? [Completeness, Spec §FR-002] ✓ data-model.md: Validation Rules table
15+
- [x] CHK003 - Are all ApplicationStatus enum values documented with their meanings? [Completeness, Spec §FR-006] ✓ data-model.md: 6 values (not_contacted → outcome_positive/negative)
16+
- [x] CHK004 - Are priority level semantics defined (what does priority 1 vs 5 mean)? [Gap, Spec §FR-001] ✓ FR-001 updated: 1=highest
17+
- [x] CHK005 - Are IndexedDB schema requirements specified for offline storage? [Completeness, Spec §FR-005] ✓ data-model.md + contracts/offline-sync.md: complete IDB schema
18+
- [x] CHK006 - Are sync queue data structures and fields documented? [Gap] ✓ data-model.md: SyncQueueItem interface with all fields
19+
- [x] CHK007 - Are CSV column mappings explicitly defined for import? [Completeness, Spec §FR-010] ✓ contracts/company-service.md: CSV columns documented
20+
- [x] CHK008 - Are both CSV and JSON export schemas documented? [Completeness, Spec §FR-011] ✓ contracts/company-service.md: exportToCSV and exportToJSON
21+
- [x] CHK009 - Are loading/empty/error states defined for the company table UI? [Gap] ✓ FR-014 added
22+
- [x] CHK010 - Are form field labels and placeholder text specified? [Gap] ✓ Implementation detail: use field names as labels (name, address, contact_name, etc.)
23+
24+
## Requirement Clarity
25+
26+
- [x] CHK011 - Is "reasonable geographic bounds (~20 miles)" quantified with exact threshold? [Clarity, Spec §FR-012] ✓ FR-012: default 20, configurable 1-100 miles
27+
- [x] CHK012 - Is the geocoding rate limit (1 req/sec) specified with queue behavior? [Clarity, Spec §FR-003] ✓ contracts/geocoding.md: RateLimiter class with queue processing
28+
- [x] CHK013 - Is "extended range" warning behavior clearly defined (toast, inline, modal)? [Ambiguity, Edge Cases] ✓ FR-015: inline warning
29+
- [x] CHK014 - Is "bulk import" batch size or file size limit specified? [Clarity, Spec §FR-010] ✓ FR-010a added: max 500 rows, 5MB file size
30+
- [x] CHK015 - Is "spreadsheet-like interface" defined with specific UI elements? [Ambiguity, User Story 2] ✓ Standard table UI: sortable columns, row selection, inline status updates
31+
- [x] CHK016 - Are filter combination behaviors specified (AND vs OR logic)? [Clarity, Spec §FR-007] ✓ FR-007: AND logic
32+
- [x] CHK017 - Is search behavior defined (substring, fuzzy, case-sensitivity)? [Clarity, Spec §FR-009] ✓ FR-009: case-insensitive substring
33+
- [x] CHK018 - Is "allow up to 5000 characters" for notes enforced client-side, server-side, or both? [Clarity, Edge Cases] ✓ Both: data-model.md CHECK constraint (server) + client form validation
34+
35+
## Requirement Consistency
36+
37+
- [x] CHK019 - Are status values consistent between spec (FR-006) and data model? [Consistency] ✓ Both: spec FR-006 and data-model.md use same 6 values
38+
- [x] CHK020 - Is the uniqueness constraint (name+address) consistent across spec and clarifications? [Consistency, Spec §FR-002a] ✓ FR-002a + Clarifications
39+
- [x] CHK021 - Are coordinate validation rules consistent between add and edit flows? [Consistency, User Stories 1 & 3] ✓ Same validateCoordinates service method for both
40+
- [x] CHK022 - Is the home location requirement consistent between spec and plan? [Consistency, Spec §FR-012a] ✓ FR-012a, FR-012b + plan.md
41+
- [x] CHK023 - Are timestamp fields (created_at, updated_at) consistently defined across all artifacts? [Consistency, Spec §FR-013] ✓ data-model.md: TIMESTAMPTZ with trigger for updated_at
42+
43+
## Acceptance Criteria Quality
44+
45+
- [x] CHK024 - Can SC-001 (add company in <60 seconds) be objectively measured? [Measurability, Success Criteria] ✓ Yes: time from form open to save completion
46+
- [x] CHK025 - Can SC-002 (find in <10 seconds with 100+ companies) be objectively measured? [Measurability, Success Criteria] ✓ Yes: time from search query to results displayed
47+
- [x] CHK026 - Can SC-003 (90%+ geocoding success) be measured with test data? [Measurability, Success Criteria] ✓ Yes: use standard US address test set
48+
- [x] CHK027 - Is SC-004 (data persists across sessions) testable without ambiguity? [Measurability, Success Criteria] ✓ Yes: save, close browser, reopen, verify data present
49+
- [x] CHK028 - Are acceptance scenarios in User Stories testable as written? [Measurability, User Stories] ✓ Yes: Given/When/Then format is executable
50+
51+
## Scenario Coverage
52+
53+
- [x] CHK029 - Are requirements defined for first-time user with no companies (empty state)? [Coverage, Gap] ✓ FR-014: empty state with prompt
54+
- [x] CHK030 - Are requirements defined for user without home location set? [Coverage, Gap] ✓ FR-012a: required before distance validation
55+
- [x] CHK031 - Are requirements defined for concurrent edits from multiple browser tabs? [Coverage, Gap] ✓ FR-005a added: last-write-wins with timestamp check
56+
- [x] CHK032 - Are requirements defined for partial sync failure (some records fail)? [Coverage, Exception Flow] ✓ contracts/offline-sync.md: SyncResult returns failed count, items stay in queue
57+
- [x] CHK033 - Are requirements defined for import with mixed valid/invalid rows? [Coverage, Spec §FR-010] ✓ US6 acceptance: "report showing which rows failed"
58+
- [x] CHK034 - Are requirements defined for editing a company while offline? [Coverage, Spec §FR-005] ✓ FR-005 + Clarifications: offline sync with conflict resolution
59+
60+
## Edge Case Coverage
61+
62+
- [x] CHK035 - Is behavior defined when Nominatim API is unavailable? [Edge Case, Dependency] ✓ Edge Cases: "geocoding fails → manual coordinate entry"
63+
- [x] CHK036 - Is behavior defined when IndexedDB quota is exceeded? [Edge Case, Gap] ✓ contracts/offline-sync.md "Storage Limits": warn user if approaching limit
64+
- [x] CHK037 - Is behavior defined for addresses that geocode to multiple results? [Edge Case, Spec §FR-003] ✓ FR-003a added: use first result, show on map for verification
65+
- [x] CHK038 - Is behavior defined when user deletes a company with pending sync? [Edge Case, Gap] ✓ FR-005b added: delete queued, synced on reconnect
66+
- [x] CHK039 - Is behavior defined for export when user has zero companies? [Edge Case, Spec §FR-011] ✓ Edge Cases added: export produces empty file with headers only
67+
- [x] CHK040 - Is import duplicate detection behavior fully specified (skip vs overwrite vs prompt)? [Edge Case, Edge Cases section] ✓ Edge Cases: "Warn user and allow skip/overwrite"
68+
69+
## Non-Functional Requirements
70+
71+
- [x] CHK041 - Are accessibility requirements specified for all form inputs? [NFR, Gap] ✓ Constitution: 5-file pattern requires accessibility.test.tsx + Pa11y
72+
- [x] CHK042 - Are keyboard navigation requirements defined for the company table? [NFR, Gap] ✓ Constitution: accessibility tests cover keyboard navigation
73+
- [x] CHK043 - Are screen reader requirements defined for status/priority indicators? [NFR, Gap] ✓ Constitution: Pa11y + ARIA requirements in accessibility tests
74+
- [x] CHK044 - Is mobile-responsive behavior specified for the table component? [NFR, Gap] ✓ Constitution: mobile-first design, 44px touch targets
75+
- [x] CHK045 - Are performance requirements specified for large datasets (1000+ companies)? [NFR, Gap] ✓ SC-002 (<10s find in 100+), IndexedDB handles thousands
76+
77+
## Dependencies & Assumptions
78+
79+
- [x] CHK046 - Is the Nominatim API usage policy and attribution requirement documented? [Dependency, Clarifications] ✓ FR-003: OSM attribution in footer
80+
- [x] CHK047 - Is the assumption of browser IndexedDB support validated? [Assumption] ✓ Standard API in all modern browsers (IE11+, all evergreen)
81+
- [x] CHK048 - Is the dependency on user_profiles table for home location documented? [Dependency, data-model.md] ✓ data-model.md: ALTER TABLE user_profiles section
82+
- [x] CHK049 - Is the future dependency on route_id (routes table) clearly marked as deferred? [Dependency, Spec §Key Entities] ✓ Key Entities: "to be implemented in Route Cluster feature"
83+
- [x] CHK050 - Is the assumption of Supabase RLS for user isolation documented? [Assumption] ✓ data-model.md: 4 RLS policies (SELECT, INSERT, UPDATE, DELETE)
84+
85+
## Ambiguities & Conflicts
86+
87+
- [x] CHK051 - Is the term "active/inactive flag" purpose clarified (soft delete vs visibility filter)? [Ambiguity, Spec §FR-001] ✓ FR-007a: visibility filter with strikethrough
88+
- [x] CHK052 - Does "outcome (positive/negative)" need separate enum values or a sub-status? [Ambiguity, Spec §FR-006] ✓ data-model.md: separate enum values (outcome_positive, outcome_negative)
89+
- [x] CHK053 - Is conflict resolution UI behavior specified (modal, inline, separate page)? [Ambiguity, Clarifications] ✓ FR-005c added: modal dialog with side-by-side comparison
90+
- [x] CHK054 - Is the OSM attribution placement requirement specified (footer, map component, both)? [Ambiguity, contracts/geocoding.md] ✓ FR-003 and contracts/geocoding.md: footer only
91+
92+
---
93+
94+
## Summary
95+
96+
| Category | Items | Status |
97+
| ------------------- | ----- | --------------- |
98+
| Completeness | 10 | ✓ All addressed |
99+
| Clarity | 8 | ✓ All addressed |
100+
| Consistency | 5 | ✓ All addressed |
101+
| Acceptance Criteria | 5 | ✓ All addressed |
102+
| Scenario Coverage | 6 | ✓ All addressed |
103+
| Edge Cases | 6 | ✓ All addressed |
104+
| NFR | 5 | ✓ All addressed |
105+
| Dependencies | 5 | ✓ All addressed |
106+
| Ambiguities | 4 | ✓ All addressed |
107+
108+
**Total Items**: 54 | **Completed**: 54 | **Status**: ✅ Ready for Implementation
109+
110+
### Requirements Added During Review
111+
112+
- FR-003a: Multiple geocode results handling
113+
- FR-005a: Concurrent tab edits (last-write-wins)
114+
- FR-005b: Offline delete queuing
115+
- FR-005c: Conflict resolution modal UI
116+
- FR-010a: Import limits (500 rows, 5MB)
117+
- Edge case: Export with zero companies

0 commit comments

Comments
 (0)