lancer-shared 1.2.328 → 1.2.330

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,41 +9,41 @@ const ROOM_TYPES_MATCHING_EMPTY_EXTENDED_TYPE = new Set([
9
9
  'proposal',
10
10
  ]);
11
11
  const ROOM_CRM_STATUS_ORDER = [
12
- 'replied',
12
+ 'new',
13
+ 'qualified',
13
14
  'follow_up',
14
- 'interested',
15
- 'not_interested',
16
- 'closed',
15
+ 'won',
16
+ 'archived',
17
17
  ];
18
18
  const ROOM_CRM_STATUS_LABELS = {
19
- replied: 'Replied',
20
- interested: 'Interested',
19
+ new: 'New',
21
20
  follow_up: 'Follow Up',
22
- not_interested: 'Lost',
23
- closed: 'Won',
21
+ qualified: 'Qualified',
22
+ won: 'Won',
23
+ archived: 'Archived',
24
24
  };
25
- const DEFAULT_ROOM_CRM_STATUS = 'replied';
25
+ const DEFAULT_ROOM_CRM_STATUS = 'new';
26
26
  const ROOM_CRM_STATUS_IDS = {
27
- replied: 1,
27
+ new: 1,
28
28
  follow_up: 2,
29
- interested: 3,
30
- closed: 4,
31
- not_interested: 5,
29
+ qualified: 3,
30
+ won: 4,
31
+ archived: 5,
32
32
  };
33
33
  const ROOM_CRM_STATUS_BY_ID = {
34
- 1: 'replied',
34
+ 1: 'new',
35
35
  2: 'follow_up',
36
- 3: 'interested',
37
- 4: 'closed',
38
- 5: 'not_interested',
36
+ 3: 'qualified',
37
+ 4: 'won',
38
+ 5: 'archived',
39
39
  };
40
40
  // Explicit board order avoids scattered "exclude then append" logic in consumers.
41
41
  const ROOM_CRM_KANBAN_STATUS_ORDER = [
42
- 'replied',
42
+ 'new',
43
+ 'qualified',
43
44
  'follow_up',
44
- 'interested',
45
- 'closed',
46
- 'not_interested',
45
+ 'won',
46
+ 'archived',
47
47
  ];
48
48
  const isRoomCrmStatus = (status) => {
49
49
  return ROOM_CRM_STATUS_ORDER.includes(status);
@@ -53,9 +53,6 @@ const normalizeRoomCrmStatus = (status) => {
53
53
  return DEFAULT_ROOM_CRM_STATUS;
54
54
  }
55
55
  const normalizedStatus = status.trim().toLowerCase();
56
- if (normalizedStatus === 'archived') {
57
- return 'closed';
58
- }
59
56
  if (isRoomCrmStatus(normalizedStatus)) {
60
57
  return normalizedStatus;
61
58
  }
@@ -70,9 +67,6 @@ const normalizeRoomCrmStatuses = (statuses) => {
70
67
  if (!normalizedStatus) {
71
68
  return null;
72
69
  }
73
- if (normalizedStatus === 'archived') {
74
- return 'closed';
75
- }
76
70
  if (isRoomCrmStatus(normalizedStatus)) {
77
71
  return normalizedStatus;
78
72
  }
@@ -7832,6 +7826,7 @@ const SearchFieldsSchema = z.object({
7832
7826
  exactPhrase: z.string().default(''),
7833
7827
  titleSearch: z.string().default(''), // AND logic for titles
7834
7828
  titleAny: z.string().default(''), // OR logic for titles
7829
+ titleNoneAny: z.string().default(''), // NOT logic for titles
7835
7830
  skillsSearch: z.string().default(''),
7836
7831
  });
7837
7832
  class SearchQueryBuilder {
@@ -7903,6 +7898,14 @@ class SearchQueryBuilder {
7903
7898
  parts.push(`(${titleParts.join(' OR ')})`);
7904
7899
  }
7905
7900
  }
7901
+ if (validatedFields.titleNoneAny.trim()) {
7902
+ const titleTerms = this.splitTerms(validatedFields.titleNoneAny);
7903
+ const titleNotParts = titleTerms.map((term) => {
7904
+ const isQuoted = term.startsWith('"') && term.endsWith('"');
7905
+ return isQuoted ? `NOT title:${term}` : `NOT title:"${term}"`;
7906
+ });
7907
+ parts.push(...titleNotParts);
7908
+ }
7906
7909
  if (validatedFields.skillsSearch.trim()) {
7907
7910
  const skillsTerms = this.splitTerms(validatedFields.skillsSearch);
7908
7911
  if (skillsTerms.length === 1) {
@@ -7938,6 +7941,8 @@ class SearchQueryBuilder {
7938
7941
  descriptions.push(`Title (all): ${validatedFields.titleSearch}`);
7939
7942
  if (validatedFields.titleAny)
7940
7943
  descriptions.push(`Title (any): ${validatedFields.titleAny}`);
7944
+ if (validatedFields.titleNoneAny)
7945
+ descriptions.push(`Title (excluding any): ${validatedFields.titleNoneAny}`);
7941
7946
  if (validatedFields.skillsSearch)
7942
7947
  descriptions.push(`Skills: ${validatedFields.skillsSearch}`);
7943
7948
  return descriptions.length > 0 ? descriptions.join(' • ') : 'All results';
@@ -7967,6 +7972,7 @@ class SearchQueryBuilder {
7967
7972
  exactPhrase: '',
7968
7973
  titleSearch: '',
7969
7974
  titleAny: '',
7975
+ titleNoneAny: '',
7970
7976
  skillsSearch: '',
7971
7977
  };
7972
7978
  if (!query || query.trim() === '*') {
@@ -7999,7 +8005,35 @@ class SearchQueryBuilder {
7999
8005
  // Remove the matched group from remaining query
8000
8006
  remainingQuery = remainingQuery.replace(match[0], '').trim();
8001
8007
  }
8002
- // 2. THEN: Extract individual title: and skills: patterns
8008
+ // 2. THEN: Extract NOT title groups like NOT (title:"a" OR title:"b")
8009
+ const titleNotGroupPattern = /NOT\s+\((title:"[^"]+"(?:\s+OR\s+title:"[^"]+")+?)\)/g;
8010
+ const titleNotGroupMatches = [...remainingQuery.matchAll(titleNotGroupPattern)];
8011
+ for (const match of titleNotGroupMatches) {
8012
+ const terms = match[1]
8013
+ .split(/\s+OR\s+/)
8014
+ .map((term) => term.replace(/^title:"([^"]+)"$/, '"$1"'))
8015
+ .filter(Boolean);
8016
+ result.titleNoneAny = result.titleNoneAny
8017
+ ? `${result.titleNoneAny} ${terms.join(' ')}`
8018
+ : terms.join(' ');
8019
+ remainingQuery = remainingQuery.replace(match[0], '').trim();
8020
+ }
8021
+ // 3. Extract individual NOT title: terms before generic title extraction
8022
+ const titleNotPattern = /NOT\s+title:("([^"]+)"|([^\s)]+))/g;
8023
+ const titleNotMatches = [...remainingQuery.matchAll(titleNotPattern)];
8024
+ if (titleNotMatches.length > 0) {
8025
+ const terms = titleNotMatches.map((match) => {
8026
+ if (match[2]) {
8027
+ return `"${match[2]}"`;
8028
+ }
8029
+ return match[3];
8030
+ });
8031
+ result.titleNoneAny = result.titleNoneAny
8032
+ ? `${result.titleNoneAny} ${terms.join(' ')}`
8033
+ : terms.join(' ');
8034
+ remainingQuery = remainingQuery.replace(titleNotPattern, '').trim();
8035
+ }
8036
+ // 4. THEN: Extract individual title: and skills: patterns
8003
8037
  const fieldPatterns = [
8004
8038
  { field: 'titleSearch', pattern: /title:("([^"]+)"|([^\s)]+))/g },
8005
8039
  { field: 'skillsSearch', pattern: /skills:("([^"]+)"|([^\s)]+))/g },
@@ -8024,7 +8058,7 @@ class SearchQueryBuilder {
8024
8058
  remainingQuery = remainingQuery.replace(pattern, '').trim();
8025
8059
  }
8026
8060
  }
8027
- // 3. Handle legacy grouped field searches like title:(term1 AND term2)
8061
+ // 5. Handle legacy grouped field searches like title:(term1 AND term2)
8028
8062
  const groupedFieldPattern = /(title|skills):\(([^)]+)\)/g;
8029
8063
  let groupMatch;
8030
8064
  while ((groupMatch = groupedFieldPattern.exec(remainingQuery)) !== null) {
@@ -8052,7 +8086,7 @@ class SearchQueryBuilder {
8052
8086
  }
8053
8087
  remainingQuery = remainingQuery.replace(groupMatch[0], '').trim();
8054
8088
  }
8055
- // 4. Extract grouped NOT expressions: NOT (term1 OR term2 OR ...)
8089
+ // 6. Extract grouped NOT expressions: NOT (term1 OR term2 OR ...)
8056
8090
  const groupedNotPattern = /NOT\s+\(([^)]+(?:\s+OR\s+[^)]+)+)\)/g;
8057
8091
  const groupedNotMatches = [...remainingQuery.matchAll(groupedNotPattern)];
8058
8092
  if (groupedNotMatches.length > 0) {
@@ -8062,9 +8096,9 @@ class SearchQueryBuilder {
8062
8096
  .replace(groupedNotMatches[0][0], '')
8063
8097
  .trim();
8064
8098
  }
8065
- // 5. Extract individual NOT terms (only if no grouped NOT was found)
8099
+ // 7. Extract individual NOT terms (only if no grouped NOT was found)
8066
8100
  if (!result.noneWords) {
8067
- const notPattern = /NOT\s+("([^"]+)"|([^\s)]+))/g;
8101
+ const notPattern = /NOT\s+(?!title:)("([^"]+)"|([^\s)]+))/g;
8068
8102
  const notMatches = [...remainingQuery.matchAll(notPattern)];
8069
8103
  if (notMatches.length > 0) {
8070
8104
  const noneTerms = notMatches.map((match) => {
@@ -8077,7 +8111,7 @@ class SearchQueryBuilder {
8077
8111
  remainingQuery = remainingQuery.replace(notPattern, '').trim();
8078
8112
  }
8079
8113
  }
8080
- // 6. Process grouped expressions - DON'T clean up connectors first!
8114
+ // 8. Process grouped expressions - DON'T clean up connectors first!
8081
8115
  // Extract OR groups (anyWords) - PROCESS FIRST
8082
8116
  const orGroupPattern = /\(([^)]+(?:\s+OR\s+[^)]+)+)\)/g;
8083
8117
  const orMatches = [...remainingQuery.matchAll(orGroupPattern)];
@@ -8096,14 +8130,14 @@ class SearchQueryBuilder {
8096
8130
  }
8097
8131
  // NOW clean up connectors after group processing
8098
8132
  remainingQuery = this.cleanupConnectors(remainingQuery);
8099
- // 7. Extract standalone quoted phrases (exactPhrase) - ONLY after all groups processed
8133
+ // 9. Extract standalone quoted phrases (exactPhrase) - ONLY after all groups processed
8100
8134
  const standaloneQuotePattern = /(?:^|\s)("([^"]+)")(?=\s|$)/g;
8101
8135
  const quoteMatches = [...remainingQuery.matchAll(standaloneQuotePattern)];
8102
8136
  if (quoteMatches.length > 0) {
8103
8137
  result.exactPhrase = quoteMatches[0][2];
8104
8138
  remainingQuery = remainingQuery.replace(quoteMatches[0][1], '').trim();
8105
8139
  }
8106
- // 8. Handle remaining simple terms as allWords
8140
+ // 10. Handle remaining simple terms as allWords
8107
8141
  if (remainingQuery) {
8108
8142
  const remainingTerms = this.splitTermsWithQuotes(remainingQuery);
8109
8143
  if (remainingTerms.length > 0) {
@@ -8845,11 +8879,11 @@ const roomNoteSchema = z.object({
8845
8879
  updatedAt: z.number(),
8846
8880
  });
8847
8881
  const roomCrmStatusEnum = z.enum([
8848
- 'replied',
8849
- 'interested',
8882
+ 'new',
8850
8883
  'follow_up',
8851
- 'not_interested',
8852
- 'closed',
8884
+ 'qualified',
8885
+ 'won',
8886
+ 'archived',
8853
8887
  ]);
8854
8888
  const roomJobClientHistorySchema = z.object({
8855
8889
  jobUrl: z.string().nullable(),