Skip to content

Commit 1c94fa6

Browse files
enkoclaude
andcommitted
feat: Convert professional info to employment history subresource
Replaces the single job_title, organization, department, and work_notes columns on friends table with a new friend_professional_history table that supports multiple employment entries with date ranges (month/year format like LinkedIn). Key changes: - New table with from_month/from_year (required) and to_month/to_year (nullable for current positions), plus is_primary flag for CardDAV - Migration creates one entry from existing data with is_primary=true - Full CRUD API endpoints for managing professional history - Frontend components with month/year pickers and "current position" toggle - CardDAV exports TITLE/ORG from primary employment entry - Full-text search updated to index primary employment data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent fc3c9e7 commit 1c94fa6

File tree

21 files changed

+2474
-410
lines changed

21 files changed

+2474
-410
lines changed

apps/backend/src/models/queries/friend-professional-history.queries.ts

Lines changed: 466 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Professional History CRUD queries
3+
* Employment history with date ranges for friends
4+
*/
5+
6+
/* @name GetProfessionalHistoryByFriendId */
7+
SELECT
8+
ph.external_id,
9+
ph.job_title,
10+
ph.organization,
11+
ph.department,
12+
ph.notes,
13+
ph.from_month,
14+
ph.from_year,
15+
ph.to_month,
16+
ph.to_year,
17+
ph.is_primary,
18+
ph.created_at
19+
FROM friends.friend_professional_history ph
20+
INNER JOIN friends.friends f ON ph.friend_id = f.id
21+
INNER JOIN auth.users u ON f.user_id = u.id
22+
WHERE f.external_id = :friendExternalId
23+
AND u.external_id = :userExternalId
24+
AND f.deleted_at IS NULL
25+
ORDER BY ph.is_primary DESC, ph.to_year IS NULL DESC, ph.from_year DESC, ph.from_month DESC, ph.created_at DESC;
26+
27+
28+
/* @name GetProfessionalHistoryById */
29+
SELECT
30+
ph.external_id,
31+
ph.job_title,
32+
ph.organization,
33+
ph.department,
34+
ph.notes,
35+
ph.from_month,
36+
ph.from_year,
37+
ph.to_month,
38+
ph.to_year,
39+
ph.is_primary,
40+
ph.created_at
41+
FROM friends.friend_professional_history ph
42+
INNER JOIN friends.friends f ON ph.friend_id = f.id
43+
INNER JOIN auth.users u ON f.user_id = u.id
44+
WHERE ph.external_id = :historyExternalId
45+
AND f.external_id = :friendExternalId
46+
AND u.external_id = :userExternalId
47+
AND f.deleted_at IS NULL;
48+
49+
50+
/* @name CreateProfessionalHistory */
51+
INSERT INTO friends.friend_professional_history (
52+
friend_id,
53+
job_title,
54+
organization,
55+
department,
56+
notes,
57+
from_month,
58+
from_year,
59+
to_month,
60+
to_year,
61+
is_primary
62+
)
63+
SELECT
64+
f.id,
65+
:jobTitle,
66+
:organization,
67+
:department,
68+
:notes,
69+
:fromMonth::smallint,
70+
:fromYear::smallint,
71+
:toMonth::smallint,
72+
:toYear::smallint,
73+
:isPrimary
74+
FROM friends.friends f
75+
INNER JOIN auth.users u ON f.user_id = u.id
76+
WHERE f.external_id = :friendExternalId
77+
AND u.external_id = :userExternalId
78+
AND f.deleted_at IS NULL
79+
RETURNING
80+
external_id,
81+
job_title,
82+
organization,
83+
department,
84+
notes,
85+
from_month,
86+
from_year,
87+
to_month,
88+
to_year,
89+
is_primary,
90+
created_at;
91+
92+
93+
/* @name UpdateProfessionalHistory */
94+
UPDATE friends.friend_professional_history ph
95+
SET
96+
job_title = :jobTitle,
97+
organization = :organization,
98+
department = :department,
99+
notes = :notes,
100+
from_month = :fromMonth::smallint,
101+
from_year = :fromYear::smallint,
102+
to_month = :toMonth::smallint,
103+
to_year = :toYear::smallint,
104+
is_primary = :isPrimary
105+
FROM friends.friends f
106+
INNER JOIN auth.users u ON f.user_id = u.id
107+
WHERE ph.external_id = :historyExternalId
108+
AND ph.friend_id = f.id
109+
AND f.external_id = :friendExternalId
110+
AND u.external_id = :userExternalId
111+
AND f.deleted_at IS NULL
112+
RETURNING
113+
ph.external_id,
114+
ph.job_title,
115+
ph.organization,
116+
ph.department,
117+
ph.notes,
118+
ph.from_month,
119+
ph.from_year,
120+
ph.to_month,
121+
ph.to_year,
122+
ph.is_primary,
123+
ph.created_at;
124+
125+
126+
/* @name DeleteProfessionalHistory */
127+
DELETE FROM friends.friend_professional_history ph
128+
USING friends.friends f, auth.users u
129+
WHERE ph.external_id = :historyExternalId
130+
AND ph.friend_id = f.id
131+
AND f.user_id = u.id
132+
AND f.external_id = :friendExternalId
133+
AND u.external_id = :userExternalId
134+
AND f.deleted_at IS NULL
135+
RETURNING ph.external_id;
136+
137+
138+
/* @name ClearPrimaryProfessionalHistory */
139+
-- Sets is_primary = false for all professional history entries for a friend
140+
-- Used before setting a new primary entry
141+
UPDATE friends.friend_professional_history ph
142+
SET is_primary = false
143+
FROM friends.friends f
144+
INNER JOIN auth.users u ON f.user_id = u.id
145+
WHERE ph.friend_id = f.id
146+
AND ph.is_primary = true
147+
AND f.external_id = :friendExternalId
148+
AND u.external_id = :userExternalId
149+
AND f.deleted_at IS NULL
150+
RETURNING ph.external_id;
151+
152+
153+
/* @name GetPrimaryProfessionalHistory */
154+
-- Get the primary professional history entry for a friend (for CardDAV/search)
155+
SELECT
156+
ph.external_id,
157+
ph.job_title,
158+
ph.organization,
159+
ph.department,
160+
ph.notes,
161+
ph.from_month,
162+
ph.from_year,
163+
ph.to_month,
164+
ph.to_year,
165+
ph.is_primary,
166+
ph.created_at
167+
FROM friends.friend_professional_history ph
168+
INNER JOIN friends.friends f ON ph.friend_id = f.id
169+
WHERE f.id = :friendId
170+
AND ph.is_primary = true
171+
LIMIT 1;

apps/backend/src/models/queries/friends.queries.ts

Lines changed: 27 additions & 76 deletions
Large diffs are not rendered by default.

apps/backend/src/models/queries/friends.sql

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@ SELECT
1010
c.name_suffix,
1111
c.photo_url,
1212
c.photo_thumbnail_url,
13-
-- Epic 1B: Professional fields
14-
c.job_title,
15-
c.organization,
16-
c.department,
17-
c.work_notes,
1813
c.interests,
1914
-- Epic 4: Categorization & Organization
2015
c.is_favorite,
@@ -111,6 +106,24 @@ SELECT
111106
FROM friends.friend_social_profiles sp
112107
WHERE sp.friend_id = c.id
113108
) as social_profiles,
109+
-- Professional history (employment records with date ranges)
110+
(
111+
SELECT COALESCE(json_agg(json_build_object(
112+
'external_id', ph.external_id,
113+
'job_title', ph.job_title,
114+
'organization', ph.organization,
115+
'department', ph.department,
116+
'notes', ph.notes,
117+
'from_month', ph.from_month,
118+
'from_year', ph.from_year,
119+
'to_month', ph.to_month,
120+
'to_year', ph.to_year,
121+
'is_primary', ph.is_primary,
122+
'created_at', ph.created_at
123+
) ORDER BY ph.is_primary DESC, ph.to_year IS NULL DESC, ph.from_year DESC, ph.from_month DESC), '[]'::json)
124+
FROM friends.friend_professional_history ph
125+
WHERE ph.friend_id = c.id
126+
) as professional_history,
114127
-- Epic 1D: Relationships
115128
(
116129
SELECT COALESCE(json_agg(json_build_object(
@@ -162,9 +175,6 @@ WITH friend_list AS (
162175
c.display_name,
163176
c.nickname,
164177
c.photo_thumbnail_url,
165-
c.job_title,
166-
c.organization,
167-
c.department,
168178
c.is_favorite,
169179
c.archived_at,
170180
c.created_at,
@@ -192,15 +202,16 @@ SELECT
192202
cl.display_name,
193203
cl.nickname,
194204
cl.photo_thumbnail_url,
195-
cl.job_title,
196-
cl.organization,
197-
cl.department,
198205
cl.is_favorite,
199206
cl.archived_at,
200207
cl.created_at,
201208
cl.updated_at,
202209
(SELECT e.email_address FROM friends.friend_emails e WHERE e.friend_id = cl.id AND e.is_primary = true LIMIT 1) as primary_email,
203210
(SELECT p.phone_number FROM friends.friend_phones p WHERE p.friend_id = cl.id AND p.is_primary = true LIMIT 1) as primary_phone,
211+
-- Professional info from primary professional history
212+
(SELECT ph.job_title FROM friends.friend_professional_history ph WHERE ph.friend_id = cl.id AND ph.is_primary = true LIMIT 1) as job_title,
213+
(SELECT ph.organization FROM friends.friend_professional_history ph WHERE ph.friend_id = cl.id AND ph.is_primary = true LIMIT 1) as organization,
214+
(SELECT ph.department FROM friends.friend_professional_history ph WHERE ph.friend_id = cl.id AND ph.is_primary = true LIMIT 1) as department,
204215
-- Extended fields for dynamic columns
205216
(SELECT a.city FROM friends.friend_addresses a WHERE a.friend_id = cl.id AND a.is_primary = true LIMIT 1) as primary_city,
206217
(SELECT a.country FROM friends.friend_addresses a WHERE a.friend_id = cl.id AND a.is_primary = true LIMIT 1) as primary_country,
@@ -241,11 +252,6 @@ INSERT INTO friends.friends (
241252
name_middle,
242253
name_last,
243254
name_suffix,
244-
-- Epic 1B: Professional fields
245-
job_title,
246-
organization,
247-
department,
248-
work_notes,
249255
interests
250256
)
251257
SELECT
@@ -257,10 +263,6 @@ SELECT
257263
:nameMiddle,
258264
:nameLast,
259265
:nameSuffix,
260-
:jobTitle,
261-
:organization,
262-
:department,
263-
:workNotes,
264266
:interests
265267
FROM auth.users u
266268
WHERE u.external_id = :userExternalId
@@ -275,10 +277,6 @@ RETURNING
275277
name_suffix,
276278
photo_url,
277279
photo_thumbnail_url,
278-
job_title,
279-
organization,
280-
department,
281-
work_notes,
282280
interests,
283281
created_at,
284282
updated_at;
@@ -293,11 +291,6 @@ SET
293291
name_middle = CASE WHEN :updateNameMiddle THEN :nameMiddle ELSE c.name_middle END,
294292
name_last = CASE WHEN :updateNameLast THEN :nameLast ELSE c.name_last END,
295293
name_suffix = CASE WHEN :updateNameSuffix THEN :nameSuffix ELSE c.name_suffix END,
296-
-- Epic 1B: Professional fields
297-
job_title = CASE WHEN :updateJobTitle THEN :jobTitle ELSE c.job_title END,
298-
organization = CASE WHEN :updateOrganization THEN :organization ELSE c.organization END,
299-
department = CASE WHEN :updateDepartment THEN :department ELSE c.department END,
300-
work_notes = CASE WHEN :updateWorkNotes THEN :workNotes ELSE c.work_notes END,
301294
interests = CASE WHEN :updateInterests THEN :interests ELSE c.interests END
302295
FROM auth.users u
303296
WHERE c.external_id = :friendExternalId
@@ -315,10 +308,6 @@ RETURNING
315308
c.name_suffix,
316309
c.photo_url,
317310
c.photo_thumbnail_url,
318-
c.job_title,
319-
c.organization,
320-
c.department,
321-
c.work_notes,
322311
c.interests,
323312
c.created_at,
324313
c.updated_at;

0 commit comments

Comments
 (0)