diff --git a/api/ql b/api/ql
deleted file mode 100644
index 333a0b5..0000000
--- a/api/ql
+++ /dev/null
@@ -1,258 +0,0 @@
-
-                   SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
-
-      Commands marked with * may be preceded by a number, _N.
-      Notes in parentheses indicate the behavior if _N is given.
-      A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
-
-  h  H                 Display this help.
-  q  :q  Q  :Q  ZZ     Exit.
- ---------------------------------------------------------------------------
-
-                           MMOOVVIINNGG
-
-  e  ^E  j  ^N  CR  *  Forward  one line   (or _N lines).
-  y  ^Y  k  ^K  ^P  *  Backward one line   (or _N lines).
-  f  ^F  ^V  SPACE  *  Forward  one window (or _N lines).
-  b  ^B  ESC-v      *  Backward one window (or _N lines).
-  z                 *  Forward  one window (and set window to _N).
-  w                 *  Backward one window (and set window to _N).
-  ESC-SPACE         *  Forward  one window, but don't stop at end-of-file.
-  d  ^D             *  Forward  one half-window (and set half-window to _N).
-  u  ^U             *  Backward one half-window (and set half-window to _N).
-  ESC-)  RightArrow *  Right one half screen width (or _N positions).
-  ESC-(  LeftArrow  *  Left  one half screen width (or _N positions).
-  ESC-}  ^RightArrow   Right to last column displayed.
-  ESC-{  ^LeftArrow    Left  to first column.
-  F                    Forward forever; like "tail -f".
-  ESC-F                Like F but stop when search pattern is found.
-  r  ^R  ^L            Repaint screen.
-  R                    Repaint screen, discarding buffered input.
-        ---------------------------------------------------
-        Default "window" is the screen height.
-        Default "half-window" is half of the screen height.
- ---------------------------------------------------------------------------
-
-                          SSEEAARRCCHHIINNGG
-
-  /_p_a_t_t_e_r_n          *  Search forward for (_N-th) matching line.
-  ?_p_a_t_t_e_r_n          *  Search backward for (_N-th) matching line.
-  n                 *  Repeat previous search (for _N-th occurrence).
-  N                 *  Repeat previous search in reverse direction.
-  ESC-n             *  Repeat previous search, spanning files.
-  ESC-N             *  Repeat previous search, reverse dir. & spanning files.
-  ESC-u                Undo (toggle) search highlighting.
-  ESC-U                Clear search highlighting.
-  &_p_a_t_t_e_r_n          *  Display only matching lines.
-        ---------------------------------------------------
-        A search pattern may begin with one or more of:
-        ^N or !  Search for NON-matching lines.
-        ^E or *  Search multiple files (pass thru END OF FILE).
-        ^F or @  Start search at FIRST file (for /) or last file (for ?).
-        ^K       Highlight matches, but don't move (KEEP position).
-        ^R       Don't use REGULAR EXPRESSIONS.
-        ^W       WRAP search if no match found.
- ---------------------------------------------------------------------------
-
-                           JJUUMMPPIINNGG
-
-  g  <  ESC-<       *  Go to first line in file (or line _N).
-  G  >  ESC->       *  Go to last line in file (or line _N).
-  p  %              *  Go to beginning of file (or _N percent into file).
-  t                 *  Go to the (_N-th) next tag.
-  T                 *  Go to the (_N-th) previous tag.
-  {  (  [           *  Find close bracket } ) ].
-  }  )  ]           *  Find open bracket { ( [.
-  ESC-^F _<_c_1_> _<_c_2_>  *  Find close bracket _<_c_2_>.
-  ESC-^B _<_c_1_> _<_c_2_>  *  Find open bracket _<_c_1_>.
-        ---------------------------------------------------
-        Each "find close bracket" command goes forward to the close bracket 
-          matching the (_N-th) open bracket in the top line.
-        Each "find open bracket" command goes backward to the open bracket 
-          matching the (_N-th) close bracket in the bottom line.
-
-  m_<_l_e_t_t_e_r_>            Mark the current top line with <letter>.
-  M_<_l_e_t_t_e_r_>            Mark the current bottom line with <letter>.
-  '_<_l_e_t_t_e_r_>            Go to a previously marked position.
-  ''                   Go to the previous position.
-  ^X^X                 Same as '.
-  ESC-M_<_l_e_t_t_e_r_>        Clear a mark.
-        ---------------------------------------------------
-        A mark is any upper-case or lower-case letter.
-        Certain marks are predefined:
-             ^  means  beginning of the file
-             $  means  end of the file
- ---------------------------------------------------------------------------
-
-                        CCHHAANNGGIINNGG FFIILLEESS
-
-  :e [_f_i_l_e]            Examine a new file.
-  ^X^V                 Same as :e.
-  :n                *  Examine the (_N-th) next file from the command line.
-  :p                *  Examine the (_N-th) previous file from the command line.
-  :x                *  Examine the first (or _N-th) file from the command line.
-  :d                   Delete the current file from the command line list.
-  =  ^G  :f            Print current file name.
- ---------------------------------------------------------------------------
-
-                    MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-
-  -_<_f_l_a_g_>              Toggle a command line option [see OPTIONS below].
-  --_<_n_a_m_e_>             Toggle a command line option, by name.
-  __<_f_l_a_g_>              Display the setting of a command line option.
-  ___<_n_a_m_e_>             Display the setting of an option, by name.
-  +_c_m_d                 Execute the less cmd each time a new file is examined.
-
-  !_c_o_m_m_a_n_d             Execute the shell command with $SHELL.
-  |XX_c_o_m_m_a_n_d            Pipe file between current pos & mark XX to shell command.
-  s _f_i_l_e               Save input to a file.
-  v                    Edit the current file with $VISUAL or $EDITOR.
-  V                    Print version number of "less".
- ---------------------------------------------------------------------------
-
-                           OOPPTTIIOONNSS
-
-        Most options may be changed either on the command line,
-        or from within less by using the - or -- command.
-        Options may be given in one of two forms: either a single
-        character preceded by a -, or a name preceded by --.
-
-  -?  ........  --help
-                  Display help (from command line).
-  -a  ........  --search-skip-screen
-                  Search skips current screen.
-  -A  ........  --SEARCH-SKIP-SCREEN
-                  Search starts just after target line.
-  -b [_N]  ....  --buffers=[_N]
-                  Number of buffers.
-  -B  ........  --auto-buffers
-                  Don't automatically allocate buffers for pipes.
-  -c  ........  --clear-screen
-                  Repaint by clearing rather than scrolling.
-  -d  ........  --dumb
-                  Dumb terminal.
-  -D xx_c_o_l_o_r  .  --color=xx_c_o_l_o_r
-                  Set screen colors.
-  -e  -E  ....  --quit-at-eof  --QUIT-AT-EOF
-                  Quit at end of file.
-  -f  ........  --force
-                  Force open non-regular files.
-  -F  ........  --quit-if-one-screen
-                  Quit if entire file fits on first screen.
-  -g  ........  --hilite-search
-                  Highlight only last match for searches.
-  -G  ........  --HILITE-SEARCH
-                  Don't highlight any matches for searches.
-  -h [_N]  ....  --max-back-scroll=[_N]
-                  Backward scroll limit.
-  -i  ........  --ignore-case
-                  Ignore case in searches that do not contain uppercase.
-  -I  ........  --IGNORE-CASE
-                  Ignore case in all searches.
-  -j [_N]  ....  --jump-target=[_N]
-                  Screen position of target lines.
-  -J  ........  --status-column
-                  Display a status column at left edge of screen.
-  -k [_f_i_l_e]  .  --lesskey-file=[_f_i_l_e]
-                  Use a lesskey file.
-  -K  ........  --quit-on-intr
-                  Exit less in response to ctrl-C.
-  -L  ........  --no-lessopen
-                  Ignore the LESSOPEN environment variable.
-  -m  -M  ....  --long-prompt  --LONG-PROMPT
-                  Set prompt style.
-  -n  -N  ....  --line-numbers  --LINE-NUMBERS
-                  Don't use line numbers.
-  -o [_f_i_l_e]  .  --log-file=[_f_i_l_e]
-                  Copy to log file (standard input only).
-  -O [_f_i_l_e]  .  --LOG-FILE=[_f_i_l_e]
-                  Copy to log file (unconditionally overwrite).
-  -p [_p_a_t_t_e_r_n]  --pattern=[_p_a_t_t_e_r_n]
-                  Start at pattern (from command line).
-  -P [_p_r_o_m_p_t]   --prompt=[_p_r_o_m_p_t]
-                  Define new prompt.
-  -q  -Q  ....  --quiet  --QUIET  --silent --SILENT
-                  Quiet the terminal bell.
-  -r  -R  ....  --raw-control-chars  --RAW-CONTROL-CHARS
-                  Output "raw" control characters.
-  -s  ........  --squeeze-blank-lines
-                  Squeeze multiple blank lines.
-  -S  ........  --chop-long-lines
-                  Chop (truncate) long lines rather than wrapping.
-  -t [_t_a_g]  ..  --tag=[_t_a_g]
-                  Find a tag.
-  -T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
-                  Use an alternate tags file.
-  -u  -U  ....  --underline-special  --UNDERLINE-SPECIAL
-                  Change handling of backspaces.
-  -V  ........  --version
-                  Display the version number of "less".
-  -w  ........  --hilite-unread
-                  Highlight first new line after forward-screen.
-  -W  ........  --HILITE-UNREAD
-                  Highlight first new line after any forward movement.
-  -x [_N[,...]]  --tabs=[_N[,...]]
-                  Set tab stops.
-  -X  ........  --no-init
-                  Don't use termcap init/deinit strings.
-  -y [_N]  ....  --max-forw-scroll=[_N]
-                  Forward scroll limit.
-  -z [_N]  ....  --window=[_N]
-                  Set size of window.
-  -" [_c[_c]]  .  --quotes=[_c[_c]]
-                  Set shell quote characters.
-  -~  ........  --tilde
-                  Don't display tildes after end of file.
-  -# [_N]  ....  --shift=[_N]
-                  Set horizontal scroll amount (0 = one half screen width).
-                --file-size
-                  Automatically determine the size of the input file.
-                --follow-name
-                  The F command changes files if the input file is renamed.
-                --incsearch
-                  Search file as each pattern character is typed in.
-                --line-num-width=N
-                  Set the width of the -N line number field to N characters.
-                --mouse
-                  Enable mouse input.
-                --no-keypad
-                  Don't send termcap keypad init/deinit strings.
-                --no-histdups
-                  Remove duplicates from command history.
-                --rscroll=C
-                  Set the character used to mark truncated lines.
-                --save-marks
-                  Retain marks across invocations of less.
-                --status-col-width=N
-                  Set the width of the -J status column to N characters.
-                --use-backslash
-                  Subsequent options use backslash as escape char.
-                --use-color
-                  Enables colored text.
-                --wheel-lines=N
-                  Each click of the mouse wheel moves N lines.
-
-
- ---------------------------------------------------------------------------
-
-                          LLIINNEE EEDDIITTIINNGG
-
-        These keys can be used to edit text being entered 
-        on the "command line" at the bottom of the screen.
-
- RightArrow ..................... ESC-l ... Move cursor right one character.
- LeftArrow ...................... ESC-h ... Move cursor left one character.
- ctrl-RightArrow  ESC-RightArrow  ESC-w ... Move cursor right one word.
- ctrl-LeftArrow   ESC-LeftArrow   ESC-b ... Move cursor left one word.
- HOME ........................... ESC-0 ... Move cursor to start of line.
- END ............................ ESC-$ ... Move cursor to end of line.
- BACKSPACE ................................ Delete char to left of cursor.
- DELETE ......................... ESC-x ... Delete char under cursor.
- ctrl-BACKSPACE   ESC-BACKSPACE ........... Delete word to left of cursor.
- ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
- ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
- UpArrow ........................ ESC-k ... Retrieve previous command line.
- DownArrow ...................... ESC-j ... Retrieve next command line.
- TAB ...................................... Complete filename & cycle.
- SHIFT-TAB ...................... ESC-TAB   Complete filename & reverse cycle.
- ctrl-L ................................... Complete filename, list all.
diff --git a/api/ql -h localhost -U optima -d optima -c SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = 'Opportunity' ORDER BY ordinal_position; b/api/ql -h localhost -U optima -d optima -c SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = 'Opportunity' ORDER BY ordinal_position;
deleted file mode 100644
index 5f8e1f6..0000000
--- a/api/ql -h localhost -U optima -d optima -c SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = 'Opportunity' ORDER BY ordinal_position;	
+++ /dev/null
@@ -1,35 +0,0 @@
-     column_name     |          data_type          | is_nullable |   column_default   
----------------------+-----------------------------+-------------+--------------------
- id                  | integer                     | NO          | 
- uid                 | text                        | NO          | 
- name                | text                        | NO          | 
- notes               | text                        | YES         | 
- oppNarrative        | text                        | YES         | 
- typeId              | integer                     | NO          | 
- stageId             | integer                     | YES         | 
- statusId            | integer                     | YES         | 
- interest            | USER-DEFINED                | YES         | 
- probability         | double precision            | NO          | 0
- source              | text                        | YES         | 
- primarySalesRepId   | text                        | YES         | 
- secondarySalesRepId | text                        | YES         | 
- companyId           | integer                     | YES         | 
- contactId           | integer                     | YES         | 
- siteId              | integer                     | YES         | 
- customerPO          | text                        | YES         | 
- locationId          | integer                     | YES         | 
- departmentId        | integer                     | YES         | 
- expectedCloseDate   | timestamp without time zone | YES         | 
- pipelineChangeDate  | timestamp without time zone | YES         | 
- dateBecameLead      | timestamp without time zone | YES         | 
- closedDate          | timestamp without time zone | YES         | 
- closedFlag          | boolean                     | NO          | false
- closedById          | text                        | YES         | 
- productSequence     | ARRAY                       | YES         | ARRAY[]::integer[]
- updatedBy           | text                        | NO          | 
- eneteredBy          | text                        | NO          | 
- createdAt           | timestamp without time zone | NO          | CURRENT_TIMESTAMP
- updatedAt           | timestamp without time zone | NO          | 
- taxCodeId           | integer                     | YES         | 
-(31 rows)
-
diff --git a/api/src/api/sales/index.ts b/api/src/api/sales/index.ts
index 5110699..374af81 100644
--- a/api/src/api/sales/index.ts
+++ b/api/src/api/sales/index.ts
@@ -34,6 +34,8 @@ import { default as fetchByUserId } from "./opportunities/fetchByUserId";
 import { default as workflowDispatch } from "./opportunities/[id]/workflow/dispatch";
 import { default as workflowStatus } from "./opportunities/[id]/workflow/status";
 import { default as workflowHistory } from "./opportunities/[id]/workflow/history";
+import { default as addTime } from "./opportunities/[id]/time";
+import { default as fetchActivities } from "./opportunities/[id]/activities";
 
 export {
   addProduct,
@@ -72,4 +74,6 @@ export {
   workflowDispatch,
   workflowStatus,
   workflowHistory,
+  addTime,
+  fetchActivities,
 };
diff --git a/api/src/api/sales/opportunities/[id]/activities.ts b/api/src/api/sales/opportunities/[id]/activities.ts
new file mode 100644
index 0000000..341d2b6
--- /dev/null
+++ b/api/src/api/sales/opportunities/[id]/activities.ts
@@ -0,0 +1,51 @@
+import { createRoute } from "../../../../modules/api-utils/createRoute";
+import { apiResponse } from "../../../../modules/api-utils/apiResponse";
+import { ContentfulStatusCode } from "hono/utils/http-status";
+import { authMiddleware } from "../../../middleware/authorization";
+import { opportunities } from "../../../../managers/opportunities";
+import { activityCw } from "../../../../modules/cw-utils/activities/activities";
+import { OptimaType } from "../../../../workflows/wf.opportunity";
+
+/* GET /v1/sales/opportunities/opportunity/:identifier/activities */
+export default createRoute(
+  "get",
+  ["/opportunities/opportunity/:identifier/activities"],
+  async (c) => {
+    const identifier = c.req.param("identifier");
+
+    const opportunity = await opportunities.fetchItem(identifier);
+
+    const rawActivities = await activityCw.fetchByOpportunityDirect(
+      opportunity.cwOpportunityId,
+    );
+
+    // Return only open workflow activities (status != 2)
+    const openActivities = rawActivities
+      .filter((a: any) => a.status?.id !== 2)
+      .map((a: any) => {
+        const optimaTypeField = a.customFields?.find(
+          (f: any) => f.id === OptimaType.FIELD_ID || f.caption === "Optima_Type",
+        );
+        const parentActivityField = a.customFields?.find(
+          (f: any) => f.id === 50 || f.caption === "Parent_Activity",
+        );
+        return {
+          cwActivityId: a.id,
+          name: a.name,
+          optimaType: optimaTypeField?.value ?? null,
+          parentActivityId: parentActivityField?.value
+            ? parseInt(parentActivityField.value, 10) || null
+            : null,
+          status: a.status,
+          dateStart: a.dateStart ?? null,
+          dateEnd: a.dateEnd ?? null,
+        };
+      });
+
+    const response = apiResponse.successful("Open activities fetched.", {
+      activities: openActivities,
+    });
+    return c.json(response, response.status as ContentfulStatusCode);
+  },
+  authMiddleware({ permissions: ["sales.opportunity.view"] }),
+);
diff --git a/api/src/api/sales/opportunities/[id]/time.ts b/api/src/api/sales/opportunities/[id]/time.ts
new file mode 100644
index 0000000..db0f62f
--- /dev/null
+++ b/api/src/api/sales/opportunities/[id]/time.ts
@@ -0,0 +1,112 @@
+import { z } from "zod";
+import { createRoute } from "../../../../modules/api-utils/createRoute";
+import { apiResponse } from "../../../../modules/api-utils/apiResponse";
+import { ContentfulStatusCode } from "hono/utils/http-status";
+import { authMiddleware } from "../../../middleware/authorization";
+import { opportunities } from "../../../../managers/opportunities";
+import { cwMembers } from "../../../../managers/cwMembers";
+import { activityCw } from "../../../../modules/cw-utils/activities/activities";
+import { submitTimeEntry } from "../../../../services/cw.opportunityService";
+import { OptimaType } from "../../../../workflows/wf.opportunity";
+import GenericError from "../../../../Errors/GenericError";
+
+const addTimeSchema = z.object({
+  /** CW activity ID to log time against. */
+  activityId: z.number().int().positive(),
+  /** ISO-8601 datetime when work started. */
+  timeStarted: z.string().datetime(),
+  /** ISO-8601 datetime when work ended. */
+  timeEnded: z.string().datetime(),
+  /** Optional notes for the time entry. */
+  notes: z.string().optional(),
+});
+
+/* POST /v1/sales/opportunities/opportunity/:identifier/time */
+export default createRoute(
+  "post",
+  ["/opportunities/opportunity/:identifier/time"],
+  async (c) => {
+    const identifier = c.req.param("identifier");
+    const body = await c.req.json();
+    const data = addTimeSchema.parse(body);
+
+    const user = c.get("user");
+
+    if (!user.cwIdentifier) {
+      throw new GenericError({
+        status: 400,
+        name: "MissingCwIdentifier",
+        message:
+          "Your account is not linked to a ConnectWise member. A CW member association is required to log time.",
+      });
+    }
+
+    // Verify the opportunity exists and belongs to the correct identifier
+    const opportunity = await opportunities.fetchItem(identifier);
+
+    const cwMember = await cwMembers.fetch(user.cwIdentifier);
+
+    // Fetch the activity to verify it belongs to this opportunity and is open
+    const activity = await activityCw.fetch(data.activityId);
+
+    if (activity.opportunity?.id !== opportunity.cwOpportunityId) {
+      throw new GenericError({
+        status: 400,
+        name: "ActivityMismatch",
+        message: "The specified activity does not belong to this opportunity.",
+      });
+    }
+
+    if (activity.status?.id === 2) {
+      throw new GenericError({
+        status: 400,
+        name: "ActivityClosed",
+        message: "Cannot log time against a closed activity.",
+      });
+    }
+
+    // Submit the time entry
+    const result = await submitTimeEntry({
+      activityId: data.activityId,
+      cwMemberId: cwMember.cwMemberId,
+      timeStart: data.timeStarted,
+      timeEnd: data.timeEnded,
+      notes: data.notes ?? "",
+    });
+
+    if (!result.success) {
+      throw new GenericError({
+        status: 502,
+        name: "TimeEntryFailed",
+        message: result.message,
+      });
+    }
+
+    // If the activity is a Schedule Entry (Optima_Type = "Schedule Entry"),
+    // close it now that time has been logged against it.
+    const optimaTypeField = activity.customFields?.find(
+      (f: any) => f.id === OptimaType.FIELD_ID || f.caption === "Optima_Type",
+    );
+    if (optimaTypeField?.value === OptimaType.ScheduleEntry) {
+      try {
+        await activityCw.update(data.activityId, [
+          { op: "replace", path: "status", value: { id: 2 } },
+        ]);
+      } catch (closeErr) {
+        // Non-fatal — time entry was already submitted successfully
+        console.error(
+          `[AddTime] Failed to close Schedule Entry activity ${data.activityId}:`,
+          closeErr,
+        );
+      }
+    }
+
+    const response = apiResponse.successful("Time entry submitted successfully.", {
+      cwTimeEntryId: result.cwTimeEntryId,
+      activityId: data.activityId,
+      activityWasClosed: optimaTypeField?.value === OptimaType.ScheduleEntry,
+    });
+    return c.json(response, response.status as ContentfulStatusCode);
+  },
+  authMiddleware({ permissions: ["sales.opportunity.update"] }),
+);
diff --git a/api/src/api/sales/opportunities/[id]/update.ts b/api/src/api/sales/opportunities/[id]/update.ts
index 7e4a57c..5578c21 100644
--- a/api/src/api/sales/opportunities/[id]/update.ts
+++ b/api/src/api/sales/opportunities/[id]/update.ts
@@ -5,6 +5,10 @@ import { ContentfulStatusCode } from "hono/utils/http-status";
 import { authMiddleware } from "../../../middleware/authorization";
 import GenericError from "../../../../Errors/GenericError";
 import { z } from "zod";
+import {
+  OpportunityStatus,
+  StatusIdToKey,
+} from "../../../../workflows/wf.opportunity";
 
 const updateSchema = z
   .object({
@@ -46,6 +50,21 @@ export default createRoute(
 
     const item = await opportunities.fetchRecord(identifier);
 
+    // Read-only guard: only New and Active statuses allow opportunity data mutations.
+    const editableStatuses = new Set<number>([
+      OpportunityStatus.New,
+      OpportunityStatus.Active,
+    ]);
+    const currentStatusId = item.statusCwId ?? null;
+    if (currentStatusId !== null && !editableStatuses.has(currentStatusId)) {
+      const statusKey = StatusIdToKey[currentStatusId] ?? `ID ${currentStatusId}`;
+      throw new GenericError({
+        status: 422,
+        name: "OpportunityReadOnly",
+        message: `Opportunity data cannot be edited in "${statusKey}" status. Only "New" and "Active" opportunities are editable.`,
+      });
+    }
+
     try {
       const updated = await item.updateOpportunity(data);
 
diff --git a/api/src/api/sales/opportunities/[id]/workflow/dispatch.ts b/api/src/api/sales/opportunities/[id]/workflow/dispatch.ts
index 72eec34..5f26a15 100644
--- a/api/src/api/sales/opportunities/[id]/workflow/dispatch.ts
+++ b/api/src/api/sales/opportunities/[id]/workflow/dispatch.ts
@@ -91,6 +91,19 @@ const dispatchSchema = z.discriminatedUnion("action", [
     action: z.literal("reopen"),
     payload: noteRequiredPayload,
   }),
+  z.object({
+    action: z.literal("sendBackForRevision"),
+    payload: noteRequiredPayload,
+  }),
+  z.object({
+    action: z.literal("createScheduleEntry"),
+    payload: basePayload.extend({
+      activityTypeValue: z.enum(["Follow-Up", "Appointment", "Admin"]),
+      dueDate: z.string().optional(),
+      startTime: z.string().optional(),
+      endTime: z.string().optional(),
+    }),
+  }),
 ]);
 
 // ── Route ─────────────────────────────────────────────────────────────────
diff --git a/api/src/api/sales/opportunities/[id]/workflow/history.ts b/api/src/api/sales/opportunities/[id]/workflow/history.ts
index 8153b20..c6cb344 100644
--- a/api/src/api/sales/opportunities/[id]/workflow/history.ts
+++ b/api/src/api/sales/opportunities/[id]/workflow/history.ts
@@ -22,6 +22,7 @@ const OPTIMA_TYPE_VALUES = new Set<string>([
   OptimaType.Revision,
   OptimaType.Finalized,
   OptimaType.Converted,
+  OptimaType.ScheduleEntry,
 ]);
 
 /** QuoteID custom field ID (matches wf.opportunity.ts QUOTE_ID_FIELD_ID). */
diff --git a/api/src/api/sales/opportunities/[id]/workflow/status.ts b/api/src/api/sales/opportunities/[id]/workflow/status.ts
index eb4a470..0d0055d 100644
--- a/api/src/api/sales/opportunities/[id]/workflow/status.ts
+++ b/api/src/api/sales/opportunities/[id]/workflow/status.ts
@@ -248,6 +248,15 @@ const ACTION_MAP: Record<number, AvailableAction[]> = {
       requiresPermission: null,
       payloadHints: { needsRevision: "true" },
     },
+    {
+      action: "sendBackForRevision",
+      label: "Send Back for Revision",
+      targetStatuses: [
+        { key: "PendingRevision", id: OpportunityStatus.PendingRevision },
+      ],
+      requiresNote: true,
+      requiresPermission: null,
+    },
   ],
 
   [OpportunityStatus.Active]: [
diff --git a/api/src/workflows/wf.opportunity.ts b/api/src/workflows/wf.opportunity.ts
index 0a30c63..39fd681 100644
--- a/api/src/workflows/wf.opportunity.ts
+++ b/api/src/workflows/wf.opportunity.ts
@@ -123,6 +123,7 @@ export const OptimaType = {
   Revision: "Revision",
   Finalized: "Finalized",
   Converted: "Converted",
+  ScheduleEntry: "Schedule Entry",
 } as const;
 
 /** CW custom field ID for the QuoteID field on activities. */
@@ -131,6 +132,9 @@ const QUOTE_ID_FIELD_ID = 48;
 /** CW custom field ID for the Close Date field on activities. */
 const CLOSE_DATE_FIELD_ID = 49;
 
+/** CW custom field ID for the Parent Activity field on activities. */
+export const PARENT_ACTIVITY_FIELD_ID = 50;
+
 /**
  * Optima_Type values whose activities should remain Open until the
  * next workflow transition closes them automatically.
@@ -139,6 +143,7 @@ const STAYS_OPEN_TYPES = new Set<OptimaTypeValue>([
   OptimaType.OpportunitySetup,
   OptimaType.OpportunityReview,
   OptimaType.Revision,
+  OptimaType.ScheduleEntry,
 ]);
 
 export type OptimaTypeValue =
@@ -151,7 +156,8 @@ export type OptimaTypeValue =
   | typeof OptimaType.QuoteGenerated
   | typeof OptimaType.Revision
   | typeof OptimaType.Finalized
-  | typeof OptimaType.Converted;
+  | typeof OptimaType.Converted
+  | typeof OptimaType.ScheduleEntry;
 
 /** Permission nodes required by gated transitions. */
 export const WorkflowPermissions = {
@@ -210,6 +216,7 @@ const ALLOWED_TRANSITIONS: Record<number, Set<number>> = {
     OpportunityStatus.PendingWon,
     OpportunityStatus.PendingLost,
     OpportunityStatus.Active,
+    OpportunityStatus.PendingRevision, // needs revision
     OpportunityStatus.InternalReview, // cold automation only
   ]),
 
@@ -219,6 +226,7 @@ const ALLOWED_TRANSITIONS: Record<number, Set<number>> = {
     OpportunityStatus.PendingLost,
     OpportunityStatus.Active,
     OpportunityStatus.InternalReview, // cold automation only
+    OpportunityStatus.PendingRevision, // send back for revision
   ]),
 
   [OpportunityStatus.Active]: new Set([
@@ -317,7 +325,7 @@ export interface SendQuotePayload extends BaseActionPayload {
 
   /**
    * Quote needs revision.
-   * Creates a revision activity and transitions to Active.
+   * Creates a revision activity and transitions to PendingRevision.
    */
   needsRevision?: boolean;
 }
@@ -342,6 +350,25 @@ export interface ResurrectPayload extends BaseActionPayload {
 /** Begin revision from PendingRevision → Active. */
 export interface BeginRevisionPayload extends BaseActionPayload {}
 
+/** Send back for revision from ConfirmedQuote → PendingRevision. */
+export interface SendBackForRevisionPayload extends BaseActionPayload {
+  note: string; // required
+}
+
+/** Create a Schedule Entry activity (no status change). */
+export interface CreateScheduleEntryPayload extends BaseActionPayload {
+  /** Activity type value: Follow-Up, Appointment, or Admin. */
+  activityTypeValue: "Follow-Up" | "Appointment" | "Admin";
+  /** ISO-8601 due date. */
+  dueDate?: string;
+  /** ISO-8601 start time. */
+  startTime?: string;
+  /** ISO-8601 end time. */
+  endTime?: string;
+  /** Optional notes for the schedule entry. */
+  note?: string;
+}
+
 /** Re-send from Active → QuoteSent. */
 export interface ResendQuotePayload extends SendQuotePayload {}
 
@@ -371,7 +398,9 @@ export type WorkflowAction =
   | { action: "beginRevision"; payload: BeginRevisionPayload }
   | { action: "resendQuote"; payload: ResendQuotePayload }
   | { action: "cancel"; payload: CancelPayload }
-  | { action: "reopen"; payload: ReopenPayload };
+  | { action: "reopen"; payload: ReopenPayload }
+  | { action: "sendBackForRevision"; payload: SendBackForRevisionPayload }
+  | { action: "createScheduleEntry"; payload: CreateScheduleEntryPayload };
 
 // -----------------------------------------------------------------------
 // Result
@@ -1083,9 +1112,9 @@ export async function transitionToQuoteSent(
     return ok(currentStatus, targetStatus, activities);
   }
 
-  // ── needsRevision flag → Active ────────────────────────────────────
+  // ── needsRevision flag → PendingRevision ────────────────────────────
   if (payload.needsRevision) {
-    const targetStatus = OpportunityStatus.Active;
+    const targetStatus = OpportunityStatus.PendingRevision;
 
     const wasAlsoConfirmed = !!payload.quoteConfirmed;
 
@@ -1518,6 +1547,117 @@ export async function beginRevision(
   return ok(currentStatus, targetStatus, activities);
 }
 
+/**
+ * ConfirmedQuote → PendingRevision
+ *
+ * Sends the opportunity back for revision from ConfirmedQuote.
+ * Requires a mandatory note explaining why.
+ */
+export async function sendBackForRevision(
+  opportunity: OpportunityController,
+  user: WorkflowUser,
+  payload: SendBackForRevisionPayload,
+): Promise<WorkflowResult> {
+  const currentStatus = opportunity.statusCwId;
+  if (currentStatus == null) return fail("Opportunity has no current status.");
+
+  const noteErr = assertNotePresent(payload.note);
+  if (noteErr) return fail(noteErr, currentStatus);
+
+  const targetStatus = OpportunityStatus.PendingRevision;
+
+  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
+  if (transErr) return fail(transErr, currentStatus);
+
+  const activity = await createWorkflowActivity({
+    name: `[Workflow] Sent back for revision — ${opportunity.name}`,
+    opportunityCwId: opportunity.cwOpportunityId,
+    companyCwId: opportunity.companyCwId,
+    assignToCwMemberId: user.cwMemberId,
+    notes: payload.note,
+    optimaType: OptimaType.Revision,
+  });
+
+  await syncOpportunityStatus({
+    opportunityId: opportunity.cwOpportunityId,
+    statusCwId: targetStatus,
+  });
+
+  await handleTimeEntry(
+    activity.cwActivityId,
+    user.cwMemberId,
+    payload,
+    payload.note,
+  );
+
+  return ok(currentStatus, targetStatus, [activity]);
+}
+
+/**
+ * Create a Schedule Entry activity without changing the opportunity status.
+ *
+ * Schedule Entry activities stay open until time is logged against them.
+ */
+export async function createScheduleEntry(
+  opportunity: OpportunityController,
+  user: WorkflowUser,
+  payload: CreateScheduleEntryPayload,
+): Promise<WorkflowResult> {
+  const currentStatus = opportunity.statusCwId;
+  if (currentStatus == null) return fail("Opportunity has no current status.");
+
+  // CW activities require ISO-8601 without milliseconds, e.g. "2026-04-19T20:15:00Z"
+  const toCwDateTime = (iso: string): string => iso.replace(/\.\d+Z$/, "Z");
+
+  const dateStart = payload.startTime
+    ? toCwDateTime(payload.startTime)
+    : payload.dueDate
+      ? toCwDateTime(payload.dueDate)
+      : undefined;
+  const dateEnd = payload.endTime ? toCwDateTime(payload.endTime) : undefined;
+
+  const activity = await ActivityController.create({
+    name: `[Schedule Entry] ${payload.activityTypeValue} — ${opportunity.name}`,
+    type: { id: 3 }, // HistoricEntry
+    opportunity: { id: opportunity.cwOpportunityId },
+    ...(opportunity.companyCwId ? { company: { id: opportunity.companyCwId } } : {}),
+    assignTo: { id: user.cwMemberId },
+    notes: payload.note ?? "",
+    ...(dateStart ? { dateStart } : {}),
+    ...(dateEnd ? { dateEnd } : {}),
+  });
+
+  // Set Optima_Type to Schedule Entry (stays open)
+  await activity.update([
+    {
+      op: "replace",
+      path: "customFields",
+      value: [
+        {
+          id: OptimaType.FIELD_ID,
+          caption: "Optima_Type",
+          type: "Text",
+          entryMethod: "List",
+          numberOfDecimals: 0,
+          value: OptimaType.ScheduleEntry,
+        },
+      ],
+    },
+  ]);
+
+  // Return a no-transition result (status unchanged)
+  return {
+    success: true,
+    previousStatusId: currentStatus,
+    newStatusId: currentStatus,
+    previousStatus: StatusIdToKey[currentStatus] ?? null,
+    newStatus: StatusIdToKey[currentStatus] ?? null,
+    activitiesCreated: [activity],
+    coldCheck: null,
+    error: null,
+  };
+}
+
 /**
  * Any cancelable status → Canceled
  *
@@ -1732,6 +1872,9 @@ async function closeOpenWorkflowActivities(
       // Only close activities whose type is in the stays-open set
       if (!STAYS_OPEN_TYPES.has(optimaField.value as OptimaTypeValue)) continue;
 
+      // Never auto-close Schedule Entry activities — they close only when time is logged
+      if (optimaField.value === OptimaType.ScheduleEntry) continue;
+
       const closeDate = new Date().toISOString();
       const existingFields = (raw.customFields ?? []).map((f: any) =>
         f.id === CLOSE_DATE_FIELD_ID ? { ...f, value: closeDate } : f,
@@ -1797,7 +1940,10 @@ export async function processOpportunityAction(
   }
 
   // ── Close any open workflow activities from previous stage ──────────
-  await closeOpenWorkflowActivities(opportunity.cwOpportunityId);
+  // Skip for createScheduleEntry — we intentionally preserve open activities.
+  if (action !== "createScheduleEntry") {
+    await closeOpenWorkflowActivities(opportunity.cwOpportunityId);
+  }
 
   // ── Route to transition function ────────────────────────────────────
   let result: WorkflowResult;
@@ -1856,6 +2002,16 @@ export async function processOpportunityAction(
       result = await reopenCancelledOpportunity(opportunity, user, payload);
       break;
 
+    case "sendBackForRevision":
+      result = await sendBackForRevision(opportunity, user, payload);
+      break;
+
+    case "createScheduleEntry":
+      // Schedule Entry does not close open activities — skip that step.
+      // We call it directly rather than falling through closeOpenWorkflowActivities.
+      result = await createScheduleEntry(opportunity, user, payload);
+      break;
+
     default: {
       const _exhaustive: never = action;
       return fail(`Unknown workflow action: "${_exhaustive}"`);
diff --git a/api/tmp-check-opp-schema.ts b/api/tmp-check-opp-schema.ts
deleted file mode 100644
index 0f956e0..0000000
--- a/api/tmp-check-opp-schema.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Client } from "pg";
-
-const url = process.env.DATABASE_URL ?? "postgresql://optima:123web123@localhost:5432/optima";
-const c = new Client(url);
-await c.connect();
-
-const r = await c.query(
-  "SELECT column_name, is_nullable, column_default FROM information_schema.columns WHERE table_name = 'Opportunity' ORDER BY ordinal_position"
-);
-
-console.log("Opportunity columns:");
-for (const row of r.rows) {
-  const nullable = row.is_nullable === "YES" ? "nullable" : "NOT NULL";
-  console.log(`  ${row.column_name}: ${nullable}${row.column_default ? ` (default: ${row.column_default})` : ""}`);
-}
-
-await c.end();
diff --git a/ui/src/components/CreateOpportunityModal.svelte b/ui/src/components/CreateOpportunityModal.svelte
index 9733462..c3c8703 100644
--- a/ui/src/components/CreateOpportunityModal.svelte
+++ b/ui/src/components/CreateOpportunityModal.svelte
@@ -83,10 +83,50 @@
   let dropdownStyle = "";
   let primaryRepSelect: HTMLSelectElement;
 
+  // ── Schedule Entry state ──
+  type ScheduleEntryType = "Follow-Up" | "Appointment" | "Admin";
+  let scheduleEntryType: ScheduleEntryType = "Follow-Up";
+  let scheduleEntryStartDate = "";
+  let scheduleEntryStartHour = "";
+  let scheduleEntryEndDate = "";
+  let scheduleEntryEndHour = "";
+  let scheduleEntryNote = "";
+  let skipScheduleEntry = false;
+  let scheduleEntryTimeError = "";
+
+  function toCoLocalDate(d: Date): string {
+    const y = d.getFullYear();
+    const mo = String(d.getMonth() + 1).padStart(2, "0");
+    const da = String(d.getDate()).padStart(2, "0");
+    return `${y}-${mo}-${da}`;
+  }
+
+  function toCoLocalTime(d: Date): string {
+    const h = String(d.getHours()).padStart(2, "0");
+    const mi = String(d.getMinutes()).padStart(2, "0");
+    return `${h}:${mi}`;
+  }
+
+  function initScheduleEntryTimes() {
+    if (scheduleEntryStartDate && scheduleEntryStartHour && scheduleEntryEndDate && scheduleEntryEndHour) return; // already set
+    const now = new Date();
+    const mins = now.getMinutes();
+    const roundedUp = Math.ceil(mins / 15) * 15;
+    const ended = new Date(now);
+    ended.setMinutes(roundedUp, 0, 0);
+    if (ended <= now) ended.setHours(ended.getHours() + 1);
+    const started = new Date(ended.getTime() - 60 * 60 * 1000);
+    scheduleEntryStartDate = toCoLocalDate(started);
+    scheduleEntryStartHour = toCoLocalTime(started);
+    scheduleEntryEndDate = toCoLocalDate(ended);
+    scheduleEntryEndHour = toCoLocalTime(ended);
+  }
+
   // ── Opportunity types state ──
   let loadedTypes: OpportunityType[] = [];
   let isLoadingTypes = false;
   $: resolvedTypes = opportunityTypes.length > 0 ? opportunityTypes : loadedTypes;
+  $: selectedTypeName = resolvedTypes.find((t) => String(t.id) === typeId)?.name ?? null;
 
   async function loadOpportunityTypes() {
     if (opportunityTypes.length > 0) return;
@@ -110,14 +150,16 @@
     { label: "Details", icon: "document" },
     { label: "Assignment", icon: "people" },
     { label: "Contact & Site", icon: "contact" },
+    { label: "Schedule Entry", icon: "calendar" },
     { label: "Review", icon: "check" },
   ];
 
   // ── Validation ──
-  $: isStep0Valid = name.trim().length > 0 && expectedCloseDate.length > 0;
+  $: isStep0Valid = name.trim().length > 0 && expectedCloseDate.length > 0 && typeId.length > 0;
   $: isStep1Valid = primarySalesRepId.length > 0 && companyId.length > 0;
   $: isStep2Valid = true; // Contact & Site step is optional
-  $: isValid = isStep0Valid && isStep1Valid && isStep2Valid;
+  $: isStep3Valid = true; // Schedule Entry step is optional (can be skipped)
+  $: isValid = isStep0Valid && isStep1Valid && isStep2Valid && isStep3Valid;
 
   $: selectedPrimaryRep = $cwMembers.find(
     (m) => String(m.id) === primarySalesRepId,
@@ -321,10 +363,11 @@
   function nextStep() {
     if (currentStep === 0 && !isStep0Valid) return;
     if (currentStep === 1 && !isStep1Valid) return;
-    if (currentStep < 3) {
+    if (currentStep < 4) {
       currentStep++;
       if (currentStep === 1) loadMembers();
       if (currentStep === 2) loadCompanyDetails();
+      if (currentStep === 3) initScheduleEntryTimes();
     }
   }
 
@@ -349,6 +392,11 @@
     }
     if (step === 3 && isStep0Valid && isStep1Valid && isStep2Valid) {
       currentStep = 3;
+      initScheduleEntryTimes();
+      return;
+    }
+    if (step === 4 && isStep0Valid && isStep1Valid && isStep2Valid && isStep3Valid) {
+      currentStep = 4;
       return;
     }
   }
@@ -391,6 +439,33 @@
         console.warn("No opportunity ID in response:", JSON.stringify(result));
       }
 
+      // ── Create schedule entry if the user filled one in ────────────────────
+      if (!skipScheduleEntry && newId) {
+        try {
+          await clientFetch(`/sales/opportunity/${newId}/workflow`, {
+            method: "POST",
+            headers: { "Content-Type": "application/json" },
+            body: JSON.stringify({
+              action: "createScheduleEntry",
+              payload: {
+                activityTypeValue: scheduleEntryType,
+                ...(scheduleEntryEndDate ? { dueDate: scheduleEntryEndDate } : {}),
+                ...(scheduleEntryStartDate && scheduleEntryStartHour
+                  ? { startTime: new Date(`${scheduleEntryStartDate}T${scheduleEntryStartHour}`).toISOString() }
+                  : {}),
+                ...(scheduleEntryEndDate && scheduleEntryEndHour
+                  ? { endTime: new Date(`${scheduleEntryEndDate}T${scheduleEntryEndHour}`).toISOString() }
+                  : {}),
+                ...(scheduleEntryNote.trim() ? { note: scheduleEntryNote.trim() } : {}),
+              },
+            }),
+          });
+        } catch {
+          // Non-fatal: the opportunity was created; log and continue.
+          console.warn("[CreateOpportunityModal] Schedule entry creation failed after opportunity was created.");
+        }
+      }
+
       reset();
 
       if (shouldNavigate) {
@@ -433,6 +508,14 @@
     submitError = "";
     currentStep = 0;
     navigateOnCreate = true;
+    scheduleEntryType = "Follow-Up";
+    scheduleEntryStartDate = "";
+    scheduleEntryStartHour = "";
+    scheduleEntryEndDate = "";
+    scheduleEntryEndHour = "";
+    scheduleEntryNote = "";
+    skipScheduleEntry = false;
+    scheduleEntryTimeError = "";
   }
 
   function handleClose() {
@@ -517,11 +600,13 @@
             class:completed={i < currentStep}
             class:disabled={(i > 0 && !isStep0Valid) ||
               (i > 1 && !isStep1Valid) ||
-              (i > 2 && !isStep2Valid)}
+              (i > 2 && !isStep2Valid) ||
+              (i > 3 && !isStep3Valid)}
             on:click={() => goToStep(i)}
             disabled={(i > 0 && !isStep0Valid) ||
               (i > 1 && !isStep1Valid) ||
-              (i > 2 && !isStep2Valid)}
+              (i > 2 && !isStep2Valid) ||
+              (i > 3 && !isStep3Valid)}
           >
             <span class="co-step-number" class:check={i < currentStep}>
               {#if i < currentStep}
@@ -632,6 +717,21 @@
                   />
                 </div>
 
+                <div class="co-form-group">
+                  <label for="co-type">Opportunity Type <span class="co-req">*</span></label>
+                  <select
+                    id="co-type"
+                    bind:value={typeId}
+                    disabled={isSubmitting || isLoadingTypes}
+                    class:has-value={!!typeId}
+                  >
+                    <option value="">— Select type —</option>
+                    {#each resolvedTypes as t (t.id)}
+                      <option value={String(t.id)}>{t.name}</option>
+                    {/each}
+                  </select>
+                </div>
+
                 <div class="co-form-group">
                   <label for="co-source">Source</label>
                   <input
@@ -653,21 +753,6 @@
                     disabled={isSubmitting}
                   />
                 </div>
-
-                <div class="co-form-group">
-                  <label for="co-type">Opportunity Type</label>
-                  <select
-                    id="co-type"
-                    bind:value={typeId}
-                    disabled={isSubmitting || isLoadingTypes}
-                    class:has-value={!!typeId}
-                  >
-                    <option value="">— Select type (optional) —</option>
-                    {#each resolvedTypes as t (t.id)}
-                      <option value={String(t.id)}>{t.name}</option>
-                    {/each}
-                  </select>
-                </div>
               </div>
             </div>
 
@@ -1227,8 +1312,136 @@
             </div>
           </div>
 
-          <!-- Step 3: Review -->
+          <!-- Step 3: Schedule Entry -->
         {:else if currentStep === 3}
+          <div class="co-step-content" style="animation: coFadeIn 0.2s ease">
+            <div class="co-section">
+              <h3 class="co-section-title">
+                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15">
+                  <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
+                  <line x1="16" y1="2" x2="16" y2="6" />
+                  <line x1="8" y1="2" x2="8" y2="6" />
+                  <line x1="3" y1="10" x2="21" y2="10" />
+                </svg>
+                Initial Schedule Entry
+              </h3>
+              <p class="co-step-hint">
+                Optionally create a schedule entry for this opportunity right away. You can skip this and add entries later.
+              </p>
+
+              <label class="co-skip-check">
+                <input type="checkbox" bind:checked={skipScheduleEntry} />
+                <span>Skip — I'll add a schedule entry later</span>
+              </label>
+
+              {#if !skipScheduleEntry}
+                <div class="co-form-grid co-schedule-grid">
+                  <!-- Activity type -->
+                  <div class="co-form-group co-full-width">
+                    <label for="co-se-type">Activity Type <span class="co-req">*</span></label>
+                    <div class="co-se-type-pills">
+                      {#each (["Follow-Up", "Appointment", "Admin"] as const) as t}
+                        <button
+                          type="button"
+                          class="co-se-pill"
+                          class:co-se-pill-active={scheduleEntryType === t}
+                          on:click={() => (scheduleEntryType = t)}
+                        >
+                          {#if t === "Follow-Up"}
+                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13">
+                              <path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.908.339 1.85.573 2.81.7A2 2 0 0122 16.92z"/>
+                            </svg>
+                          {:else if t === "Appointment"}
+                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13">
+                              <rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
+                              <line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
+                            </svg>
+                          {:else}
+                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13">
+                              <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
+                            </svg>
+                          {/if}
+                          {t}
+                        </button>
+                      {/each}
+                    </div>
+                  </div>
+
+                  <!-- Start date -->
+                  <div class="co-form-group">
+                    <label for="co-se-start-date">Start Date</label>
+                    <input
+                      id="co-se-start-date"
+                      type="date"
+                      class:co-input-error={!!scheduleEntryTimeError}
+                      bind:value={scheduleEntryStartDate}
+                      disabled={isSubmitting}
+                      on:change={() => { scheduleEntryTimeError = ""; }}
+                    />
+                  </div>
+
+                  <!-- Start time -->
+                  <div class="co-form-group">
+                    <label for="co-se-start-hour">Start Time</label>
+                    <input
+                      id="co-se-start-hour"
+                      type="time"
+                      class:co-input-error={!!scheduleEntryTimeError}
+                      bind:value={scheduleEntryStartHour}
+                      disabled={isSubmitting}
+                      on:change={() => { scheduleEntryTimeError = ""; }}
+                    />
+                  </div>
+
+                  <!-- End date -->
+                  <div class="co-form-group">
+                    <label for="co-se-end-date">End Date</label>
+                    <input
+                      id="co-se-end-date"
+                      type="date"
+                      class:co-input-error={!!scheduleEntryTimeError}
+                      bind:value={scheduleEntryEndDate}
+                      disabled={isSubmitting}
+                      on:change={() => { scheduleEntryTimeError = ""; }}
+                    />
+                  </div>
+
+                  <!-- End time -->
+                  <div class="co-form-group">
+                    <label for="co-se-end-hour">End Time</label>
+                    <input
+                      id="co-se-end-hour"
+                      type="time"
+                      class:co-input-error={!!scheduleEntryTimeError}
+                      bind:value={scheduleEntryEndHour}
+                      disabled={isSubmitting}
+                      on:change={() => { scheduleEntryTimeError = ""; }}
+                    />
+                  </div>
+
+                  {#if scheduleEntryTimeError}
+                    <div class="co-full-width co-field-error">{scheduleEntryTimeError}</div>
+                  {/if}
+
+                  <!-- Notes -->
+                  <div class="co-form-group co-full-width">
+                    <label for="co-se-notes">Notes <span class="co-optional-label">(optional)</span></label>
+                    <textarea
+                      id="co-se-notes"
+                      class="co-notes-textarea"
+                      bind:value={scheduleEntryNote}
+                      placeholder="Add any notes for this schedule entry…"
+                      rows="3"
+                      disabled={isSubmitting}
+                    ></textarea>
+                  </div>
+                </div>
+              {/if}
+            </div>
+          </div>
+
+          <!-- Step 4: Review -->
+        {:else if currentStep === 4}
           <div class="co-step-content" style="animation: coFadeIn 0.2s ease">
             <div class="co-review">
               <div class="co-review-header">
@@ -1280,6 +1493,12 @@
                         })}
                       </dd>
                     </div>
+                    {#if selectedTypeName}
+                      <div class="co-review-item">
+                        <dt>Type</dt>
+                        <dd>{selectedTypeName}</dd>
+                      </div>
+                    {/if}
                     {#if source}
                       <div class="co-review-item">
                         <dt>Source</dt>
@@ -1390,6 +1609,39 @@
                 </div>
               {/if}
 
+              <!-- Schedule entry review card -->
+              {#if !skipScheduleEntry}
+                <div class="co-review-notes co-review-schedule">
+                  <h4>
+                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
+                      <rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
+                      <line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
+                    </svg>
+                    Schedule Entry
+                  </h4>
+                  <dl class="co-review-schedule-dl">
+                    <div class="co-review-item">
+                      <dt>Type</dt>
+                      <dd>{scheduleEntryType}</dd>
+                    </div>
+                    {#if scheduleEntryStartDate && scheduleEntryStartHour}
+                      <div class="co-review-item">
+                        <dt>Start</dt>
+                        <dd>{new Date(`${scheduleEntryStartDate}T${scheduleEntryStartHour}`).toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}</dd>
+                      </div>
+                    {/if}
+                    {#if scheduleEntryEndDate && scheduleEntryEndHour}
+                      <div class="co-review-item">
+                        <dt>End</dt>
+                        <dd>{new Date(`${scheduleEntryEndDate}T${scheduleEntryEndHour}`).toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}</dd>
+                      </div>
+                    {/if}
+                  </dl>
+                </div>
+              {:else}
+                <p class="co-review-skipped">No schedule entry — will be added later.</p>
+              {/if}
+
               <label class="co-navigate-check">
                 <input type="checkbox" bind:checked={navigateOnCreate} />
                 <span>Open opportunity after creating</span>
@@ -1433,20 +1685,23 @@
           Cancel
         </button>
 
-        {#if currentStep < 3}
+        {#if currentStep < 4}
           <button
             class="co-btn-next"
             on:click={nextStep}
             disabled={(currentStep === 0 && !isStep0Valid) ||
               (currentStep === 1 && !isStep1Valid) ||
-              (currentStep === 2 && !isStep2Valid)}
+              (currentStep === 2 && !isStep2Valid) ||
+              (currentStep === 3 && !isStep3Valid)}
             type="button"
           >
             {currentStep === 0
               ? "Next: Assignment"
               : currentStep === 1
                 ? "Next: Contact & Site"
-                : "Review"}
+                : currentStep === 2
+                  ? "Next: Schedule Entry"
+                  : "Review"}
             <svg
               viewBox="0 0 24 24"
               fill="none"
@@ -1531,7 +1786,7 @@
   .co-modal {
     display: flex;
     flex-direction: column;
-    width: 640px;
+    width: 740px;
     max-width: 94vw;
     max-height: 88vh;
     background: var(--bg-surface, #ffffff);
@@ -1618,21 +1873,23 @@
     align-items: center;
     justify-content: center;
     gap: 0;
-    padding: 16px 24px;
+    padding: 12px 16px;
     border-bottom: 1px solid var(--border-subtle, #e5e5e5);
     background: var(--bg-inset, #fafafa);
+    overflow: hidden;
   }
 
   .co-step {
     display: flex;
     align-items: center;
-    gap: 8px;
-    padding: 6px 14px;
+    gap: 5px;
+    padding: 5px 8px;
     border: none;
     background: none;
     cursor: pointer;
     border-radius: 8px;
     transition: all 0.15s;
+    white-space: nowrap;
   }
 
   .co-step:hover:not(.disabled) {
@@ -1673,7 +1930,7 @@
   }
 
   .co-step-label {
-    font-size: 12.5px;
+    font-size: 11.5px;
     font-weight: 550;
     color: var(--text-secondary, #666);
     transition: color 0.15s;
@@ -1684,11 +1941,12 @@
   }
 
   .co-step-connector {
-    width: 32px;
+    width: 20px;
     height: 2px;
     background: var(--border-subtle, #ddd);
     border-radius: 1px;
     transition: background 0.2s;
+    flex-shrink: 0;
   }
 
   .co-step-connector.filled {
@@ -1762,6 +2020,7 @@
 
   .co-form-group input[type="text"],
   .co-form-group input[type="date"],
+  .co-form-group input[type="time"],
   .co-form-group select {
     width: 100%;
     padding: 8px 12px;
@@ -2255,6 +2514,136 @@
     color: var(--text-secondary, #666);
   }
 
+  .co-navigate-check {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 10px 14px;
+    border-radius: 8px;
+    background: var(--bg-inset, #fafafa);
+    border: 1px solid var(--border-subtle, #e5e5e5);
+    cursor: pointer;
+    transition: background 0.1s;
+  }
+
+  .co-navigate-check:hover {
+    background: var(--card-hover-bg, #f0f0f0);
+  }
+
+  .co-navigate-check input {
+    accent-color: #3b82f6;
+  }
+
+  .co-navigate-check span {
+    font-size: 12.5px;
+    font-weight: 500;
+    color: var(--text-secondary, #666);
+  }
+
+  /* ── Schedule Entry step ── */
+  .co-step-hint {
+    font-size: 12.5px;
+    color: var(--text-muted, #888);
+    margin-bottom: 12px;
+    line-height: 1.5;
+  }
+
+  .co-skip-check {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 10px 14px;
+    border-radius: 8px;
+    background: var(--bg-inset, #1a1a1a);
+    border: 1px solid var(--border-subtle, #333);
+    cursor: pointer;
+    margin-bottom: 16px;
+    transition: background 0.1s;
+  }
+  .co-skip-check:hover {
+    background: var(--card-hover-bg, #222);
+  }
+  .co-skip-check input {
+    accent-color: #3b82f6;
+  }
+  .co-skip-check span {
+    font-size: 12.5px;
+    font-weight: 500;
+    color: var(--text-secondary, #aaa);
+  }
+
+  .co-schedule-grid {
+    margin-top: 4px;
+  }
+
+  .co-se-type-pills {
+    display: flex;
+    gap: 8px;
+    flex-wrap: wrap;
+  }
+
+  .co-se-pill {
+    display: inline-flex;
+    align-items: center;
+    gap: 5px;
+    padding: 6px 14px;
+    border-radius: 20px;
+    border: 1px solid var(--border-color, #444);
+    background: transparent;
+    color: var(--text-muted, #888);
+    font-size: 13px;
+    cursor: pointer;
+    transition: border-color 0.15s, color 0.15s, background 0.15s;
+  }
+
+  .co-se-pill:hover {
+    border-color: var(--accent-color, #4f8ef7);
+    color: var(--accent-color, #4f8ef7);
+  }
+
+  .co-se-pill.co-se-pill-active {
+    border-color: var(--accent-color, #4f8ef7);
+    background: var(--accent-bg, rgba(79, 142, 247, 0.1));
+    color: var(--accent-color, #4f8ef7);
+    font-weight: 600;
+  }
+
+  .co-optional-label {
+    font-weight: 400;
+    color: var(--text-muted, #888);
+    font-size: 11px;
+  }
+
+  .co-input-error {
+    border-color: #ef4444 !important;
+  }
+
+  .co-field-error {
+    font-size: 11.5px;
+    color: #ef4444;
+    margin-top: -4px;
+    margin-bottom: 4px;
+  }
+
+  /* ── Schedule entry review card ── */
+  .co-review-schedule {
+    margin-top: 12px;
+  }
+
+  .co-review-schedule-dl {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px 20px;
+    margin-top: 8px;
+  }
+
+  .co-review-skipped {
+    font-size: 12.5px;
+    color: var(--text-muted, #888);
+    font-style: italic;
+    margin: 8px 0;
+  }
+
   /* ── Error banner ── */
   .co-error-banner {
     display: flex;
@@ -2517,6 +2906,7 @@
 
     .co-form-group input[type="text"],
     .co-form-group input[type="date"],
+    .co-form-group input[type="time"],
     .co-form-group select {
       font-size: 16px;
       padding: 10px 12px;
diff --git a/ui/src/lib/optima-api/modules/sales.ts b/ui/src/lib/optima-api/modules/sales.ts
index 4eb84ef..cd3f70f 100644
--- a/ui/src/lib/optima-api/modules/sales.ts
+++ b/ui/src/lib/optima-api/modules/sales.ts
@@ -111,7 +111,9 @@ export type WorkflowAction =
   | "beginRevision"
   | "resendQuote"
   | "cancel"
-  | "reopen";
+  | "reopen"
+  | "sendBackForRevision"
+  | "createScheduleEntry";
 
 export type ReviewDecision = "approve" | "reject" | "send" | "cancel";
 
@@ -129,6 +131,11 @@ export interface WorkflowActionPayload {
   // finalize
   outcome?: "won" | "lost";
   finalize?: boolean;
+  // createScheduleEntry
+  activityTypeValue?: "Follow-Up" | "Appointment" | "Admin";
+  dueDate?: string;
+  startTime?: string;
+  endTime?: string;
 }
 
 export interface WorkflowAvailableAction {
@@ -273,6 +280,24 @@ export const REOPENABLE_STATUSES: ReadonlySet<WorkflowStatusKey> = new Set([
   "Canceled",
 ]);
 
+/** Statuses where opportunity data is fully editable (frontend + backend). */
+export const EDITABLE_STATUSES: ReadonlySet<WorkflowStatusKey> = new Set([
+  "New",
+  "Active",
+]);
+
+/**
+ * Statuses where the "Add Time" button is hidden.
+ * PendingWon, PendingLost, Won, Lost, Canceled.
+ */
+export const ADD_TIME_EXCLUDED_STATUSES: ReadonlySet<WorkflowStatusKey> = new Set([
+  "PendingWon",
+  "PendingLost",
+  "Won",
+  "Lost",
+  "Canceled",
+]);
+
 /** Statuses where the quote has been confirmed (finalize is allowed) */
 export const QUOTE_CONFIRMED_STATUSES: ReadonlySet<WorkflowStatusKey> = new Set(
   ["ConfirmedQuote", "ReadyToSend", "Active", "PendingWon", "PendingLost"]
@@ -1162,4 +1187,42 @@ export const sales = {
       successful?: boolean;
     };
   },
+
+  async fetchOpenActivities(accessToken: string, identifier: string) {
+    const response = await api.get(
+      `/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/activities`,
+      { headers: { Authorization: `Bearer ${accessToken}` } }
+    );
+    return response.data as {
+      successful?: boolean;
+      data: {
+        activities: Array<{
+          cwActivityId: number;
+          name: string;
+          optimaType: string | null;
+          parentActivityId: number | null;
+          status: { id?: number; name?: string } | null;
+          dateStart: string | null;
+          dateEnd: string | null;
+        }>;
+      };
+    };
+  },
+
+  async addTimeToActivity(
+    accessToken: string,
+    identifier: string,
+    payload: { activityId: number; timeStarted: string; timeEnded: string; notes?: string }
+  ) {
+    const response = await api.post(
+      `/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/time`,
+      payload,
+      { headers: { Authorization: `Bearer ${accessToken}` } }
+    );
+    return response.data as {
+      successful?: boolean;
+      message?: string;
+      data: { cwTimeEntryId: number | null; activityId: number; activityWasClosed: boolean };
+    };
+  },
 };
diff --git a/ui/src/routes/sales/opportunity/[id]/+page.svelte b/ui/src/routes/sales/opportunity/[id]/+page.svelte
index 681aa49..41c4477 100644
--- a/ui/src/routes/sales/opportunity/[id]/+page.svelte
+++ b/ui/src/routes/sales/opportunity/[id]/+page.svelte
@@ -6,6 +6,7 @@
   import {
     STATUS_ID_TO_KEY,
     TERMINAL_STATUSES,
+    EDITABLE_STATUSES,
     type WorkflowResult,
     type WorkflowStatusResponse,
   } from "$lib/optima-api/modules/sales";
@@ -44,6 +45,9 @@
   $: if (data.opportunity) localActivities = null; // reset when server data refreshes
   $: effectiveActivities = localActivities ?? opportunity?.activities ?? [];
 
+  // ActivityTab ref — used to trigger a refresh after workflow actions create new activities
+  let activityTabComponent: ActivityTab;
+
   // Closed opportunity lockdown – no edits except admin delete.
   // Also immediately reflects Won/Lost/Canceled workflow status without needing a reload.
   $: isClosedOpportunity = (() => {
@@ -62,6 +66,16 @@
     );
   })();
 
+  // Workflow read-only: only New and Active are editable (when in Optima workflow).
+  $: isReadOnly = (() => {
+    if (isClosedOpportunity) return true;
+    const wfStatus = workflowStatus;
+    if (!wfStatus?.isOptimaStage) return false;
+    const key = STATUS_ID_TO_KEY[wfStatus.currentStatusId] ?? null;
+    if (!key) return false;
+    return !EDITABLE_STATUSES.has(key);
+  })();
+
   /** Handle a completed workflow action: update UI immediately without a full page reload. */
   function handleWorkflowChanged(
     e: CustomEvent<{ result: WorkflowResult; freshWorkflowStatus: WorkflowStatusResponse | null }>,
@@ -88,6 +102,7 @@
         ...result.activitiesCreated,
         ...(localActivities ?? opportunity?.activities ?? []),
       ];
+      activityTabComponent?.refresh();
     }
   }
 
@@ -229,7 +244,7 @@
     {isMobile}
     {mobileActiveTab}
     {permissions}
-    {isClosedOpportunity}
+    isClosedOpportunity={isReadOnly}
     {workflowStatus}
     opportunityTypes={data.opportunityTypes ?? []}
     accessToken={data.accessToken}
@@ -420,7 +435,7 @@
           productSequence={localProductSequence}
           initialProductId={pendingProductId}
           {permissions}
-          {isClosedOpportunity}
+          isClosedOpportunity={isReadOnly}
           salesTaxRate={opportunity?.expectedSalesTaxRate ?? null}
           taxCodeDescription={opportunity?.taxCodeDescription ?? null}
           bind:isEditing={productsEditing}
@@ -436,7 +451,7 @@
           initialQuotes={quotes}
           initialQuoteId={pendingQuoteId}
           {permissions}
-          {isClosedOpportunity}
+          isClosedOpportunity={isReadOnly}
           on:quotesChanged={handleQuotesChanged}
         />
       {:else if activeTab === "Notes"}
@@ -444,13 +459,14 @@
           {notes}
           {permissions}
           {opportunityId}
-          {isClosedOpportunity}
+          isClosedOpportunity={isReadOnly}
           on:notesChanged={() => {
             invalidateAll();
           }}
         />
       {:else if activeTab === "Activity"}
         <ActivityTab
+          bind:this={activityTabComponent}
           {opportunityId}
           activities={effectiveActivities}
           on:viewQuote={handleViewQuote}
diff --git a/ui/src/routes/sales/opportunity/[id]/activities/+server.ts b/ui/src/routes/sales/opportunity/[id]/activities/+server.ts
new file mode 100644
index 0000000..92d6a21
--- /dev/null
+++ b/ui/src/routes/sales/opportunity/[id]/activities/+server.ts
@@ -0,0 +1,24 @@
+import { optima } from "$lib";
+import { json, error } from "@sveltejs/kit";
+import type { RequestHandler } from "./$types";
+
+/** GET /sales/opportunity/[id]/activities — fetch open activities for time logging */
+export const GET: RequestHandler = async ({ params, locals }) => {
+  const accessToken = locals.session?.accessToken;
+  if (!accessToken) throw error(401, "Unauthorized");
+
+  try {
+    const result = await optima.sales.fetchOpenActivities(
+      accessToken,
+      params.id,
+    );
+    return json(result);
+  } catch (err: unknown) {
+    console.error("[Activities] Failed to fetch:", err);
+    const status =
+      err && typeof err === "object" && "status" in err
+        ? (err as { status: number }).status
+        : 500;
+    throw error(status, "Failed to fetch activities");
+  }
+};
diff --git a/ui/src/routes/sales/opportunity/[id]/components/ActivityTab.svelte b/ui/src/routes/sales/opportunity/[id]/components/ActivityTab.svelte
index d47ec42..cda3bf9 100644
--- a/ui/src/routes/sales/opportunity/[id]/components/ActivityTab.svelte
+++ b/ui/src/routes/sales/opportunity/[id]/components/ActivityTab.svelte
@@ -40,6 +40,16 @@
     fetchHistory();
   }
 
+  /** Extract the Parent_Activity ID (custom field 50) */
+  function getParentActivityId(activity: OpportunityActivity): number | null {
+    const cf = activity.customFields?.find(
+      (f) => f.id === 50 || f.caption === "Parent_Activity",
+    );
+    if (!cf?.value) return null;
+    const parsed = parseInt(cf.value, 10);
+    return isNaN(parsed) ? null : parsed;
+  }
+
   /** Extract the Optima_Type from custom fields */
   function getOptimaType(activity: OpportunityActivity): string | null {
     const cf = activity.customFields?.find(
@@ -173,6 +183,41 @@
       return dateB.localeCompare(dateA);
     });
   })();
+
+  /** Build a flat tree: root items first, then their children immediately after (for parent hierarchy display) */
+  $: flatTree = (() => {
+    const idToItem = new Map(
+      timelineItems.map((item) => [item.activity.cwActivityId, item]),
+    );
+    const childrenMap = new Map<number, (typeof timelineItems)[0][]>();
+    const roots: (typeof timelineItems)[0][] = [];
+
+    for (const item of timelineItems) {
+      const parentId = getParentActivityId(item.activity);
+      if (
+        parentId != null &&
+        item.activity.cwActivityId != null &&
+        idToItem.has(parentId)
+      ) {
+        if (!childrenMap.has(parentId)) childrenMap.set(parentId, []);
+        childrenMap.get(parentId)!.push(item);
+      } else {
+        roots.push(item);
+      }
+    }
+
+    const flat: Array<(typeof timelineItems)[0] & { depth: number }> = [];
+    for (const root of roots) {
+      flat.push({ ...root, depth: 0 });
+      const rootId = root.activity.cwActivityId;
+      if (rootId != null) {
+        for (const child of childrenMap.get(rootId) ?? []) {
+          flat.push({ ...child, depth: 1 });
+        }
+      }
+    }
+    return flat;
+  })();
 </script>
 
 <div class="activity-tab">
@@ -254,11 +299,11 @@
     </div>
   {:else}
     <div class="at-timeline">
-      {#each timelineItems as item, i (item.activity.cwActivityId ?? i)}
+      {#each flatTree as item, i ((item.activity.cwActivityId ?? i) + "-d" + item.depth)}
         {@const act = item.activity}
         {@const transition = parseStatusTransition(act.notes)}
         {@const open = item.closed != null ? !item.closed : isOpenActivity(act)}
-        <div class="at-entry" class:at-entry-open={open}>
+        <div class="at-entry" class:at-entry-open={open} class:at-entry-child={item.depth > 0}>
           <!-- Timeline connector -->
           <div class="at-connector">
             <div
@@ -375,13 +420,22 @@
                 {assignedDisplay(act)}
               </span>
 
-              {#if act.cwDateEntered}
+              {#if act.cwDateEntered && item.optimaType !== "Schedule Entry"}
                 <span class="at-timestamp"
                   >{formatDateTime(act.cwDateEntered)}</span
                 >
-              {:else if act.dateStart}
-                <span class="at-timestamp">{formatDateTime(act.dateStart)}</span
-                >
+              {/if}
+
+              {#if item.optimaType === "Schedule Entry" && (act.dateStart || act.dateEnd)}
+                <span class="at-timestamp at-schedule-time">
+                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="11" height="11">
+                    <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
+                    <line x1="16" y1="2" x2="16" y2="6" />
+                    <line x1="8" y1="2" x2="8" y2="6" />
+                    <line x1="3" y1="10" x2="21" y2="10" />
+                  </svg>
+                  {#if act.dateStart}{formatDateTime(act.dateStart)}{/if}{#if act.dateStart && act.dateEnd}&nbsp;–&nbsp;{/if}{#if act.dateEnd}{formatDateTime(act.dateEnd)}{/if}
+                </span>
               {/if}
 
               {#if item.closedAt}
@@ -392,7 +446,7 @@
                 <span class="at-timestamp at-closed-at">
                   Closed: {formatDateTime(act.closedAt)}
                 </span>
-              {:else if act.dateEnd}
+              {:else if act.dateEnd && item.optimaType !== "Schedule Entry"}
                 <span class="at-timestamp at-closed-at">
                   Closed: {formatDateTime(act.dateEnd)}
                 </span>
diff --git a/ui/src/routes/sales/opportunity/[id]/components/AddTimeModal.svelte b/ui/src/routes/sales/opportunity/[id]/components/AddTimeModal.svelte
new file mode 100644
index 0000000..df9938d
--- /dev/null
+++ b/ui/src/routes/sales/opportunity/[id]/components/AddTimeModal.svelte
@@ -0,0 +1,548 @@
+<script lang="ts">
+  import { createEventDispatcher } from "svelte";
+  import { clientFetch } from "$lib/client-fetch";
+
+  const dispatch = createEventDispatcher<{
+    success: void;
+    close: void;
+    createScheduleEntry: void;
+  }>();
+
+  export let isOpen = false;
+  export let opportunityId: string;
+
+  type Activity = { cwActivityId: number; name: string; optimaType: string | null };
+
+  let activities: Activity[] = [];
+  let isLoadingActivities = false;
+  let selectedActivityId: number | null = null;
+  let startDate = "";
+  let startHour = "";
+  let endDate = "";
+  let endHour = "";
+  let notes = "";
+  let isSubmitting = false;
+  let error: string | null = null;
+  let activityIdError = "";
+  let timeError = "";
+
+  function toLocalDate(d: Date): string {
+    const y = d.getFullYear();
+    const mo = String(d.getMonth() + 1).padStart(2, "0");
+    const da = String(d.getDate()).padStart(2, "0");
+    return `${y}-${mo}-${da}`;
+  }
+
+  function toLocalTime(d: Date): string {
+    const h = String(d.getHours()).padStart(2, "0");
+    const mi = String(d.getMinutes()).padStart(2, "0");
+    return `${h}:${mi}`;
+  }
+
+  function initTimes() {
+    const now = new Date();
+    const mins = now.getMinutes();
+    const roundedUp = Math.ceil(mins / 15) * 15;
+    const ended = new Date(now);
+    ended.setMinutes(roundedUp, 0, 0);
+    const started = new Date(ended.getTime() - 15 * 60 * 1000);
+    startDate = toLocalDate(started);
+    startHour = toLocalTime(started);
+    endDate = toLocalDate(ended);
+    endHour = toLocalTime(ended);
+  }
+  initTimes();
+
+  $: if (!isLoadingActivities && activities.length === 1) {
+    selectedActivityId = activities[0]?.cwActivityId ?? null;
+  }
+
+  async function loadActivities() {
+    isLoadingActivities = true;
+    activities = [];
+    selectedActivityId = null;
+    try {
+      const result = await clientFetch<{
+        successful: boolean;
+        data: { activities: Activity[] };
+      }>(`/sales/opportunity/${opportunityId}/activities`);
+      activities = result.data?.activities ?? [];
+    } catch {
+      activities = [];
+    } finally {
+      isLoadingActivities = false;
+    }
+  }
+
+  $: if (isOpen) loadActivities();
+
+  function validate(): boolean {
+    activityIdError = "";
+    timeError = "";
+    if (!selectedActivityId) {
+      activityIdError = "Please select an activity.";
+      return false;
+    }
+    if (!startDate || !startHour || !endDate || !endHour) {
+      timeError = "Start and end date/time are required.";
+      return false;
+    }
+    const start = new Date(`${startDate}T${startHour}`);
+    const end = new Date(`${endDate}T${endHour}`);
+    if (end <= start) {
+      timeError = "End time must be after start time.";
+      return false;
+    }
+    return true;
+  }
+
+  async function handleSubmit() {
+    if (!validate()) return;
+    isSubmitting = true;
+    error = null;
+    try {
+      const result = await clientFetch<{
+        successful: boolean;
+        message?: string;
+      }>(`/sales/opportunity/${opportunityId}/time`, {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({
+          activityId: selectedActivityId,
+          timeStarted: new Date(`${startDate}T${startHour}`).toISOString(),
+          timeEnded: new Date(`${endDate}T${endHour}`).toISOString(),
+          notes: notes.trim() || undefined,
+        }),
+      });
+
+      if (result.successful === false) {
+        error = result.message ?? "Failed to submit time entry.";
+        return;
+      }
+      dispatch("success");
+      handleClose();
+    } catch (err) {
+      error = err instanceof Error ? err.message : "Failed to submit time entry.";
+    } finally {
+      isSubmitting = false;
+    }
+  }
+
+  function handleClose() {
+    if (isSubmitting) return;
+    error = null;
+    activityIdError = "";
+    timeError = "";
+    notes = "";
+    activities = [];
+    isLoadingActivities = false;
+    initTimes();
+    dispatch("close");
+  }
+
+  function handleBackdropClick(e: MouseEvent) {
+    if ((e.target as HTMLElement).classList.contains("wf-modal-backdrop")) {
+      handleClose();
+    }
+  }
+
+  function handleKeydown(e: KeyboardEvent) {
+    if (e.key === "Escape") handleClose();
+  }
+</script>
+
+{#if isOpen}
+  <div
+    class="wf-modal-backdrop"
+    on:click={handleBackdropClick}
+    on:keydown={handleKeydown}
+    role="dialog"
+    aria-modal="true"
+    aria-label="Add Time"
+    tabindex="-1"
+  >
+    <div class="wf-modal" on:click|stopPropagation>
+      <div class="wf-modal-header">
+        <h3 class="wf-modal-title">Add Time</h3>
+        <button
+          class="wf-modal-close"
+          on:click={handleClose}
+          disabled={isSubmitting}
+          aria-label="Close"
+        >
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
+            <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
+          </svg>
+        </button>
+      </div>
+
+      <div class="wf-modal-body">
+        {#if isLoadingActivities}
+          <div class="at-loading">
+            <span class="at-spinner"></span>
+            <span>Loading activities…</span>
+          </div>
+        {:else if activities.length === 0}
+          <div class="at-empty-state">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="36" height="36" class="at-empty-icon">
+              <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="13"/><circle cx="12" cy="16" r="0.5" fill="currentColor"/>
+            </svg>
+            <p class="at-empty-title">No open activities</p>
+            <p class="at-empty-sub">There are no open activities on this opportunity to log time against. You can create a schedule entry instead.</p>
+          </div>
+        {:else}
+
+        {#if error}
+          <div class="wf-inline-error">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
+              <circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
+            </svg>
+            {error}
+          </div>
+        {/if}
+
+        <!-- Activity selector -->
+        {#if activities.length > 1}
+          <div class="at-form-group">
+            <label class="at-label" for="at-activity-select">Activity <span class="at-req">*</span></label>
+            <select
+              id="at-activity-select"
+              class="at-select"
+              class:at-input-error={!!activityIdError}
+              bind:value={selectedActivityId}
+              disabled={isSubmitting}
+            >
+              <option value={null} disabled>Select an activity…</option>
+              {#each activities as act}
+                <option value={act.cwActivityId}>
+                  {act.name}{act.optimaType ? ` · ${act.optimaType}` : ""}
+                </option>
+              {/each}
+            </select>
+            {#if activityIdError}
+              <span class="at-field-error">{activityIdError}</span>
+            {/if}
+          </div>
+        {:else}
+          <div class="at-form-group">
+            <label class="at-label">Activity</label>
+            <div class="at-activity-chip">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13">
+                <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
+              </svg>
+              {activities[0].name}{activities[0].optimaType ? ` · ${activities[0].optimaType}` : ""}
+            </div>
+          </div>
+        {/if}
+
+        <!-- Date/time grid -->
+        <div class="at-grid">
+          <div class="at-form-group">
+            <label class="at-label" for="at-start-date">Start Date <span class="at-req">*</span></label>
+            <input
+              id="at-start-date"
+              type="date"
+              class="at-input"
+              class:at-input-error={!!timeError}
+              bind:value={startDate}
+              disabled={isSubmitting}
+              on:change={() => (timeError = "")}
+            />
+          </div>
+          <div class="at-form-group">
+            <label class="at-label" for="at-start-time">Start Time <span class="at-req">*</span></label>
+            <input
+              id="at-start-time"
+              type="time"
+              class="at-input"
+              class:at-input-error={!!timeError}
+              bind:value={startHour}
+              disabled={isSubmitting}
+              on:change={() => (timeError = "")}
+            />
+          </div>
+          <div class="at-form-group">
+            <label class="at-label" for="at-end-date">End Date <span class="at-req">*</span></label>
+            <input
+              id="at-end-date"
+              type="date"
+              class="at-input"
+              class:at-input-error={!!timeError}
+              bind:value={endDate}
+              disabled={isSubmitting}
+              on:change={() => (timeError = "")}
+            />
+          </div>
+          <div class="at-form-group">
+            <label class="at-label" for="at-end-time">End Time <span class="at-req">*</span></label>
+            <input
+              id="at-end-time"
+              type="time"
+              class="at-input"
+              class:at-input-error={!!timeError}
+              bind:value={endHour}
+              disabled={isSubmitting}
+              on:change={() => (timeError = "")}
+            />
+          </div>
+        </div>
+        {#if timeError}
+          <span class="at-field-error">{timeError}</span>
+        {/if}
+
+        <!-- Notes -->
+        <div class="at-form-group">
+          <label class="at-label" for="at-notes">Notes <span class="at-optional">(optional)</span></label>
+          <textarea
+            id="at-notes"
+            class="at-textarea"
+            rows={3}
+            placeholder="Describe the work performed…"
+            bind:value={notes}
+            disabled={isSubmitting}
+          ></textarea>
+        </div>
+
+        {/if}<!-- end else (has activities) -->
+      </div>
+
+      {#if isLoadingActivities}
+        <!-- no footer while loading -->
+      {:else if activities.length === 0}
+        <div class="wf-modal-footer">
+          <button class="wf-action-btn wf-btn-ghost" on:click={handleClose}>
+            Close
+          </button>
+          <button class="wf-action-btn wf-btn-primary" on:click={() => { handleClose(); dispatch('createScheduleEntry'); }}>
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
+              <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
+            </svg>
+            Create Schedule Entry
+          </button>
+        </div>
+      {:else}
+        <div class="wf-modal-footer">
+          <button class="wf-action-btn wf-btn-ghost" on:click={handleClose} disabled={isSubmitting}>
+            Cancel
+          </button>
+          <button
+            class="wf-action-btn wf-btn-primary"
+            on:click={handleSubmit}
+            disabled={isSubmitting}
+          >
+            {#if isSubmitting}
+              <span class="wf-spinner"></span> Saving…
+            {:else}
+              Log Time
+            {/if}
+          </button>
+        </div>
+      {/if}
+    </div>
+  </div>
+{/if}
+
+<style>
+  /* ── Layout ── */
+  .at-grid {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 12px;
+    margin-bottom: 14px;
+  }
+
+  .at-form-group {
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+    margin-bottom: 14px;
+  }
+
+  /* ── Labels ── */
+  .at-label {
+    font-size: 12px;
+    font-weight: 600;
+    color: var(--text-muted, #888);
+    text-transform: uppercase;
+    letter-spacing: 0.05em;
+  }
+
+  .at-req {
+    color: #ef4444;
+    font-weight: 700;
+  }
+
+  .at-optional {
+    font-weight: 400;
+    text-transform: none;
+    letter-spacing: 0;
+    font-size: 11px;
+  }
+
+  /* ── Inputs ── */
+  .at-input,
+  .at-select {
+    width: 100%;
+    padding: 8px 12px;
+    border: 1px solid var(--border-subtle, #ddd);
+    border-radius: 8px;
+    background: var(--bg-inset, #fafafa);
+    font-size: 13px;
+    color: var(--text-primary, #1a1a1a);
+    box-sizing: border-box;
+    font-family: inherit;
+    transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
+  }
+
+  .at-input:focus,
+  .at-select:focus {
+    outline: none;
+    border-color: #3b82f6;
+    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
+    background: var(--bg-surface, #fff);
+  }
+
+  .at-input:disabled,
+  .at-select:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
+
+  .at-input-error {
+    border-color: #ef4444 !important;
+  }
+
+  .at-field-error {
+    font-size: 11.5px;
+    color: #ef4444;
+    margin-top: -8px;
+    margin-bottom: 8px;
+    display: block;
+  }
+
+  /* ── Select chevron ── */
+  .at-select {
+    appearance: none;
+    -webkit-appearance: none;
+    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2.5'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
+    background-repeat: no-repeat;
+    background-position: right 10px center;
+    padding-right: 30px;
+  }
+
+  /* ── Activity chip (single activity) ── */
+  .at-activity-chip {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    padding: 7px 12px;
+    border-radius: 8px;
+    background: rgba(59, 130, 246, 0.06);
+    border: 1px solid rgba(59, 130, 246, 0.18);
+    font-size: 13px;
+    font-weight: 500;
+    color: var(--text-primary, #1a1a1a);
+  }
+
+  .at-activity-chip svg {
+    color: #3b82f6;
+    flex-shrink: 0;
+  }
+
+  /* ── Textarea ── */
+  .at-textarea {
+    width: 100%;
+    padding: 10px 12px;
+    border: 1px solid var(--border-subtle, #ddd);
+    border-radius: 8px;
+    background: var(--bg-inset, #fafafa);
+    font-size: 13px;
+    color: var(--text-primary, #1a1a1a);
+    box-sizing: border-box;
+    resize: vertical;
+    font-family: inherit;
+    line-height: 1.5;
+    transition: border-color 0.15s, box-shadow 0.15s;
+  }
+
+  .at-textarea:focus {
+    outline: none;
+    border-color: #3b82f6;
+    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
+    background: var(--bg-surface, #fff);
+  }
+
+  .at-textarea::placeholder {
+    color: var(--text-muted, #aaa);
+  }
+
+  .at-textarea:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
+
+  /* ── Loading state ── */
+  .at-loading {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 10px;
+    padding: 40px 20px;
+    color: var(--text-muted, #888);
+    font-size: 13px;
+  }
+
+  .at-spinner {
+    display: inline-block;
+    width: 18px;
+    height: 18px;
+    border: 2px solid rgba(59, 130, 246, 0.2);
+    border-top-color: #3b82f6;
+    border-radius: 50%;
+    animation: at-spin 0.7s linear infinite;
+    flex-shrink: 0;
+  }
+
+  @keyframes at-spin {
+    to { transform: rotate(360deg); }
+  }
+
+  /* ── Empty state ── */
+  .at-empty-state {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    text-align: center;
+    padding: 32px 24px 24px;
+    gap: 8px;
+  }
+
+  .at-empty-icon {
+    color: var(--text-muted, #aaa);
+    margin-bottom: 4px;
+  }
+
+  .at-empty-title {
+    font-size: 15px;
+    font-weight: 600;
+    color: var(--text-primary, #1a1a1a);
+    margin: 0;
+  }
+
+  .at-empty-sub {
+    font-size: 13px;
+    color: var(--text-muted, #888);
+    margin: 0;
+    max-width: 320px;
+    line-height: 1.5;
+  }
+
+  /* ── Footer ── */
+  .wf-modal-footer {
+    display: flex;
+    justify-content: flex-end;
+    gap: 0.5rem;
+    padding: 0.75rem 1rem;
+    border-top: 1px solid var(--border-color, #333);
+  }
+</style>
diff --git a/ui/src/routes/sales/opportunity/[id]/components/CreateScheduleEntryModal.svelte b/ui/src/routes/sales/opportunity/[id]/components/CreateScheduleEntryModal.svelte
new file mode 100644
index 0000000..42678e9
--- /dev/null
+++ b/ui/src/routes/sales/opportunity/[id]/components/CreateScheduleEntryModal.svelte
@@ -0,0 +1,412 @@
+<script lang="ts">
+  import { createEventDispatcher } from "svelte";
+  import type { WorkflowActionPayload } from "$lib/optima-api/modules/sales";
+
+  const dispatch = createEventDispatcher<{
+    submit: WorkflowActionPayload;
+    close: void;
+  }>();
+
+  export let isOpen = false;
+  export let isSubmitting = false;
+  export let error: string | null = null;
+
+  type ActivityTypeValue = "Follow-Up" | "Appointment" | "Admin";
+
+  let activityTypeValue: ActivityTypeValue = "Follow-Up";
+  let startDate = "";
+  let startHour = "";
+  let endDate = "";
+  let endHour = "";
+  let notes = "";
+  let timeError = "";
+
+  function toLocalDate(d: Date): string {
+    const y = d.getFullYear();
+    const mo = String(d.getMonth() + 1).padStart(2, "0");
+    const da = String(d.getDate()).padStart(2, "0");
+    return `${y}-${mo}-${da}`;
+  }
+
+  function toLocalTime(d: Date): string {
+    const h = String(d.getHours()).padStart(2, "0");
+    const mi = String(d.getMinutes()).padStart(2, "0");
+    return `${h}:${mi}`;
+  }
+
+  function initTimes() {
+    const now = new Date();
+    const mins = now.getMinutes();
+    const roundedUp = Math.ceil(mins / 15) * 15;
+    const ended = new Date(now);
+    ended.setMinutes(roundedUp, 0, 0);
+    if (ended <= now) ended.setHours(ended.getHours() + 1);
+    const started = new Date(ended.getTime() - 60 * 60 * 1000);
+    startDate = toLocalDate(started);
+    startHour = toLocalTime(started);
+    endDate = toLocalDate(ended);
+    endHour = toLocalTime(ended);
+  }
+  initTimes();
+
+  function validate(): boolean {
+    timeError = "";
+    if (startDate && startHour && endDate && endHour) {
+      const s = new Date(`${startDate}T${startHour}`);
+      const e = new Date(`${endDate}T${endHour}`);
+      if (e <= s) {
+        timeError = "End time must be after start time.";
+        return false;
+      }
+    }
+    return true;
+  }
+
+  function handleSubmit() {
+    if (!validate()) return;
+    const startIso = startDate && startHour ? new Date(`${startDate}T${startHour}`).toISOString() : undefined;
+    const endIso   = endDate   && endHour   ? new Date(`${endDate}T${endHour}`).toISOString()     : undefined;
+    const payload: WorkflowActionPayload = {
+      activityTypeValue,
+      dueDate: endDate || undefined,
+      startTime: startIso,
+      endTime: endIso,
+      note: notes.trim() || undefined,
+    };
+    dispatch("submit", payload);
+  }
+
+  function handleClose() {
+    if (isSubmitting) return;
+    timeError = "";
+    notes = "";
+    initTimes();
+    dispatch("close");
+  }
+
+  function handleBackdropClick(e: MouseEvent) {
+    if ((e.target as HTMLElement).classList.contains("wf-modal-backdrop")) {
+      handleClose();
+    }
+  }
+
+  function handleKeydown(e: KeyboardEvent) {
+    if (e.key === "Escape") handleClose();
+  }
+</script>
+
+{#if isOpen}
+  <div
+    class="wf-modal-backdrop"
+    on:click={handleBackdropClick}
+    on:keydown={handleKeydown}
+    role="dialog"
+    aria-modal="true"
+    aria-label="Create Schedule Entry"
+    tabindex="-1"
+  >
+    <div class="wf-modal" on:click|stopPropagation>
+      <div class="wf-modal-header">
+        <h3 class="wf-modal-title">Create Schedule Entry</h3>
+        <button
+          class="wf-modal-close"
+          on:click={handleClose}
+          disabled={isSubmitting}
+          aria-label="Close"
+        >
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
+            <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
+          </svg>
+        </button>
+      </div>
+
+      <div class="wf-modal-body">
+        {#if error}
+          <div class="wf-inline-error">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
+              <circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
+            </svg>
+            {error}
+          </div>
+        {/if}
+
+        <!-- Activity Type pills -->
+        <div class="se-form-group se-full-width">
+          <label class="se-label">Activity Type <span class="se-req">*</span></label>
+          <div class="se-type-pills">
+            {#each (["Follow-Up", "Appointment", "Admin"] as const) as t}
+              <button
+                type="button"
+                class="se-pill"
+                class:se-pill-active={activityTypeValue === t}
+                on:click={() => (activityTypeValue = t)}
+                disabled={isSubmitting}
+              >
+                {#if t === "Follow-Up"}
+                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13">
+                    <path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.908.339 1.85.573 2.81.7A2 2 0 0122 16.92z"/>
+                  </svg>
+                {:else if t === "Appointment"}
+                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13">
+                    <rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
+                    <line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
+                  </svg>
+                {:else}
+                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13">
+                    <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
+                  </svg>
+                {/if}
+                {t}
+              </button>
+            {/each}
+          </div>
+        </div>
+
+        <!-- Date/time grid -->
+        <div class="se-grid">
+          <div class="se-form-group">
+            <label class="se-label" for="se-start-date">Start Date</label>
+            <input
+              id="se-start-date"
+              type="date"
+              class="se-input"
+              class:se-input-error={!!timeError}
+              bind:value={startDate}
+              disabled={isSubmitting}
+              on:change={() => (timeError = "")}
+            />
+          </div>
+          <div class="se-form-group">
+            <label class="se-label" for="se-start-time">Start Time</label>
+            <input
+              id="se-start-time"
+              type="time"
+              class="se-input"
+              class:se-input-error={!!timeError}
+              bind:value={startHour}
+              disabled={isSubmitting}
+              on:change={() => (timeError = "")}
+            />
+          </div>
+          <div class="se-form-group">
+            <label class="se-label" for="se-end-date">End Date</label>
+            <input
+              id="se-end-date"
+              type="date"
+              class="se-input"
+              class:se-input-error={!!timeError}
+              bind:value={endDate}
+              disabled={isSubmitting}
+              on:change={() => (timeError = "")}
+            />
+          </div>
+          <div class="se-form-group">
+            <label class="se-label" for="se-end-time">End Time</label>
+            <input
+              id="se-end-time"
+              type="time"
+              class="se-input"
+              class:se-input-error={!!timeError}
+              bind:value={endHour}
+              disabled={isSubmitting}
+              on:change={() => (timeError = "")}
+            />
+          </div>
+        </div>
+        {#if timeError}
+          <span class="se-field-error">{timeError}</span>
+        {/if}
+
+        <!-- Notes -->
+        <div class="se-form-group se-full-width">
+          <label class="se-label" for="se-notes">Notes <span class="se-optional">(optional)</span></label>
+          <textarea
+            id="se-notes"
+            class="se-textarea"
+            rows={3}
+            placeholder="Add any notes for this schedule entry…"
+            bind:value={notes}
+            disabled={isSubmitting}
+          ></textarea>
+        </div>
+      </div>
+
+      <div class="wf-modal-footer">
+        <button class="wf-action-btn wf-btn-ghost" on:click={handleClose} disabled={isSubmitting}>
+          Cancel
+        </button>
+        <button
+          class="wf-action-btn wf-btn-primary"
+          on:click={handleSubmit}
+          disabled={isSubmitting}
+        >
+          {#if isSubmitting}
+            <span class="wf-spinner"></span> Creating…
+          {:else}
+            Create Entry
+          {/if}
+        </button>
+      </div>
+    </div>
+  </div>
+{/if}
+
+<style>
+  /* ── Layout ── */
+  .se-grid {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 12px;
+    margin-bottom: 14px;
+  }
+
+  .se-form-group {
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+    margin-bottom: 14px;
+  }
+
+  .se-full-width {
+    grid-column: 1 / -1;
+  }
+
+  /* ── Label ── */
+  .se-label {
+    font-size: 12px;
+    font-weight: 600;
+    color: var(--text-muted, #888);
+    text-transform: uppercase;
+    letter-spacing: 0.05em;
+  }
+
+  .se-req {
+    color: #ef4444;
+    font-weight: 700;
+  }
+
+  .se-optional {
+    font-weight: 400;
+    text-transform: none;
+    letter-spacing: 0;
+    font-size: 11px;
+  }
+
+  /* ── Inputs ── */
+  .se-input {
+    width: 100%;
+    padding: 8px 12px;
+    border: 1px solid var(--border-subtle, #ddd);
+    border-radius: 8px;
+    background: var(--bg-inset, #fafafa);
+    font-size: 13px;
+    color: var(--text-primary, #1a1a1a);
+    box-sizing: border-box;
+    font-family: inherit;
+    transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
+  }
+
+  .se-input:focus {
+    outline: none;
+    border-color: #3b82f6;
+    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
+    background: var(--bg-surface, #fff);
+  }
+
+  .se-input:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
+
+  .se-input-error {
+    border-color: #ef4444 !important;
+  }
+
+  .se-field-error {
+    font-size: 11.5px;
+    color: #ef4444;
+    margin-top: -8px;
+    margin-bottom: 8px;
+    display: block;
+  }
+
+  /* ── Type pills ── */
+  .se-type-pills {
+    display: flex;
+    gap: 8px;
+    flex-wrap: wrap;
+  }
+
+  .se-pill {
+    display: inline-flex;
+    align-items: center;
+    gap: 5px;
+    padding: 6px 14px;
+    border-radius: 20px;
+    border: 1px solid var(--border-subtle, #ddd);
+    background: transparent;
+    color: var(--text-muted, #888);
+    font-size: 13px;
+    font-family: inherit;
+    cursor: pointer;
+    transition: border-color 0.15s, color 0.15s, background 0.15s;
+  }
+
+  .se-pill:hover:not(:disabled) {
+    border-color: #3b82f6;
+    color: #3b82f6;
+  }
+
+  .se-pill.se-pill-active {
+    border-color: #3b82f6;
+    background: rgba(59, 130, 246, 0.08);
+    color: #3b82f6;
+    font-weight: 600;
+  }
+
+  .se-pill:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
+
+  /* ── Textarea ── */
+  .se-textarea {
+    width: 100%;
+    padding: 10px 12px;
+    border: 1px solid var(--border-subtle, #ddd);
+    border-radius: 8px;
+    background: var(--bg-inset, #fafafa);
+    font-size: 13px;
+    color: var(--text-primary, #1a1a1a);
+    box-sizing: border-box;
+    resize: vertical;
+    font-family: inherit;
+    line-height: 1.5;
+    transition: border-color 0.15s, box-shadow 0.15s;
+  }
+
+  .se-textarea:focus {
+    outline: none;
+    border-color: #3b82f6;
+    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
+    background: var(--bg-surface, #fff);
+  }
+
+  .se-textarea::placeholder {
+    color: var(--text-muted, #aaa);
+  }
+
+  .se-textarea:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+  }
+
+  /* ── Footer ── */
+  .wf-modal-footer {
+    display: flex;
+    justify-content: flex-end;
+    gap: 0.5rem;
+    padding: 0.75rem 1rem;
+    border-top: 1px solid var(--border-color, #333);
+  }
+</style>
diff --git a/ui/src/routes/sales/opportunity/[id]/components/WorkflowPanel.svelte b/ui/src/routes/sales/opportunity/[id]/components/WorkflowPanel.svelte
index b70b087..21f773d 100644
--- a/ui/src/routes/sales/opportunity/[id]/components/WorkflowPanel.svelte
+++ b/ui/src/routes/sales/opportunity/[id]/components/WorkflowPanel.svelte
@@ -15,6 +15,7 @@
     TERMINAL_STATUSES,
     REOPENABLE_STATUSES,
     QUOTE_CONFIRMED_STATUSES,
+    ADD_TIME_EXCLUDED_STATUSES,
   } from "$lib/optima-api/modules/sales";
   import type { PermissionMap } from "$lib/permissions";
   import type {
@@ -25,6 +26,9 @@
   import SendQuoteModal from "./SendQuoteModal.svelte";
   import ReviewDecisionModal from "./ReviewDecisionModal.svelte";
   import FinalizeModal from "./FinalizeModal.svelte";
+  import AddTimeModal from "./AddTimeModal.svelte";
+  import CreateScheduleEntryModal from "./CreateScheduleEntryModal.svelte";
+  import { formatDateTime } from "../types";
 
   const dispatch = createEventDispatcher<{
     workflowChanged: { result: WorkflowResult; freshWorkflowStatus: WorkflowStatusResponse | null };
@@ -44,6 +48,16 @@
   let isSubmitting = false;
   let preselectedOutcome: "won" | "lost" | null = null;
 
+  // Add Time modal state
+  let showAddTimeModal = false;
+  let addTimeActivities: Array<{ cwActivityId: number; name: string; optimaType: string | null }> = [];
+  let isLoadingActivities = false;
+
+  // Create Schedule Entry modal state
+  let showScheduleEntryModal = false;
+  let scheduleEntryError: string | null = null;
+  let isScheduleEntrySubmitting = false;
+
   // Derived state
   $: statusKey = workflowStatus
     ? (STATUS_ID_TO_KEY[workflowStatus.currentStatusId] ?? null)
@@ -76,6 +90,21 @@
   $: canReopen = permissions["sales.opportunity.reopen"] !== false;
   $: hasQuotes = quotes.length > 0;
 
+  /** "Add Time" is available on all statuses except terminal/pending-outcome/canceled */
+  $: canShowAddTime = statusKey
+    ? !ADD_TIME_EXCLUDED_STATUSES.has(statusKey) && isOptimaStage
+    : false;
+
+  /** Open (unclosed) schedule entries for this opportunity */
+  $: openScheduleEntries = activities
+    .filter((a) => {
+      const optimaType = a.customFields?.find(
+        (f) => f.caption === "Optima_Type" || f.id === 45,
+      )?.value;
+      return optimaType === "Schedule Entry" && a.status?.id !== 2;
+    })
+    .sort((a, b) => (a.dateStart ?? "").localeCompare(b.dateStart ?? ""));
+
   /** For resendQuote: check that a quote was generated after the last revision opening */
   $: hasQuoteSinceRevision = (() => {
     if (!hasQuotes) return false;
@@ -209,6 +238,8 @@
       cancel: "Cancel",
       reopen: "Reopen",
       acceptNew: "Accept",
+      sendBackForRevision: "Send Back for Revision",
+      createScheduleEntry: "Create Schedule Entry",
     };
     return overrides[action.action] ?? action.label;
   }
@@ -299,6 +330,72 @@
     activeModal = e.detail.action;
   }
 
+  /** Open the Add Time modal */
+  function openAddTimeModal() {
+    showAddTimeModal = true;
+  }
+
+  /** Handle successful time submission from AddTimeModal */
+  function handleAddTimeSuccess() {
+    showAddTimeModal = false;
+    dispatch("workflowChanged", {
+      result: {
+        success: true,
+        previousStatusId: workflowStatus?.currentStatusId ?? null,
+        newStatusId: workflowStatus?.currentStatusId ?? null,
+        previousStatus: null,
+        newStatus: null,
+        activitiesCreated: [],
+        coldCheck: null,
+        error: null,
+      },
+      freshWorkflowStatus: null,
+    });
+  }
+
+  /** Submit a Create Schedule Entry action */
+  async function submitScheduleEntry(payload: WorkflowActionPayload) {
+    isScheduleEntrySubmitting = true;
+    scheduleEntryError = null;
+    try {
+      const json = await clientFetch<{
+        successful: boolean;
+        message?: string;
+        data?: { error?: string };
+      }>(`/sales/opportunity/${opportunityId}/workflow`, {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({ action: "createScheduleEntry", payload }),
+      });
+
+      if (json.successful === false) {
+        scheduleEntryError = json.message ?? json.data?.error ?? "Failed to create schedule entry.";
+        return;
+      }
+
+      showScheduleEntryModal = false;
+      const result = json.data as WorkflowResult;
+      if (result?.activitiesCreated?.length) {
+        activities = [...result.activitiesCreated, ...activities];
+      }
+
+      // Fetch fresh workflow status
+      let freshWorkflowStatus: WorkflowStatusResponse | null = null;
+      try {
+        const statusJson = await clientFetch<{ data: WorkflowStatusResponse }>(
+          `/sales/opportunity/${opportunityId}/workflow`,
+        );
+        freshWorkflowStatus = statusJson.data ?? null;
+      } catch { /* ignore */ }
+
+      dispatch("workflowChanged", { result: result ?? { success: true, previousStatusId: null, newStatusId: null, previousStatus: null, newStatus: null, activitiesCreated: [], coldCheck: null, error: null }, freshWorkflowStatus });
+    } catch (err) {
+      scheduleEntryError = err instanceof Error ? err.message : "Failed to create schedule entry.";
+    } finally {
+      isScheduleEntrySubmitting = false;
+    }
+  }
+
   /** Determine if action needs a special modal */
   function isSpecialModal(action: WorkflowAction): boolean {
     return ["sendQuote", "resendQuote", "reviewDecision", "finalize"].includes(
@@ -494,6 +591,68 @@
             </button>
           {/if}
         {/if}
+
+        <!-- Add Time + Create Schedule Entry buttons (right side) -->
+        {#if canShowAddTime || true}
+          <span class="wf-action-sep" aria-hidden="true"></span>
+          <!-- Create Schedule Entry (calendar icon) -->
+          <span class="wf-tooltip-wrap" data-tooltip="Create Schedule Entry">
+            <button
+              class="wf-action-btn wf-btn-icon"
+              on:click={() => { showScheduleEntryModal = true; scheduleEntryError = null; }}
+              disabled={isSubmitting || isScheduleEntrySubmitting}
+              aria-label="Create Schedule Entry"
+            >
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15">
+                <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
+                <line x1="16" y1="2" x2="16" y2="6" />
+                <line x1="8" y1="2" x2="8" y2="6" />
+                <line x1="3" y1="10" x2="21" y2="10" />
+              </svg>
+            </button>
+          </span>
+          <!-- Add Time (clock icon) -->
+          {#if canShowAddTime}
+            <span class="wf-tooltip-wrap" data-tooltip="Add Time">
+              <button
+                class="wf-action-btn wf-btn-icon"
+                on:click={openAddTimeModal}
+                disabled={isSubmitting || isLoadingActivities}
+                aria-label="Add Time"
+              >
+                {#if isLoadingActivities}
+                  <span class="wf-spinner wf-spinner-sm"></span>
+                {:else}
+                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15">
+                    <circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
+                  </svg>
+                {/if}
+              </button>
+            </span>
+          {/if}
+        {/if}
+      </div>
+    {/if}
+
+    <!-- Pending schedule entries -->
+    {#if openScheduleEntries.length > 0}
+      <div class="wf-schedule-entries">
+        {#each openScheduleEntries as entry}
+          <div class="wf-se-item">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12" class="wf-se-icon">
+              <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
+              <line x1="16" y1="2" x2="16" y2="6" />
+              <line x1="8" y1="2" x2="8" y2="6" />
+              <line x1="3" y1="10" x2="21" y2="10" />
+            </svg>
+            <span class="wf-se-name">{entry.name ?? "Schedule Entry"}</span>
+            {#if entry.dateStart}
+              <span class="wf-se-time">
+                {formatDateTime(entry.dateStart)}{#if entry.dateEnd}&nbsp;–&nbsp;{formatDateTime(entry.dateEnd)}{/if}
+              </span>
+            {/if}
+          </div>
+        {/each}
       </div>
     {/if}
 
@@ -586,3 +745,21 @@
     on:close={closeModal}
   />
 {/if}
+
+<!-- Add Time modal -->
+<AddTimeModal
+  isOpen={showAddTimeModal}
+  {opportunityId}
+  on:success={handleAddTimeSuccess}
+  on:close={() => { showAddTimeModal = false; }}
+  on:createScheduleEntry={() => { showAddTimeModal = false; showScheduleEntryModal = true; }}
+/>
+
+<!-- Create Schedule Entry modal -->
+<CreateScheduleEntryModal
+  isOpen={showScheduleEntryModal}
+  isSubmitting={isScheduleEntrySubmitting}
+  error={scheduleEntryError}
+  on:submit={(e) => submitScheduleEntry(e.detail)}
+  on:close={() => { showScheduleEntryModal = false; scheduleEntryError = null; }}
+/>
diff --git a/ui/src/routes/sales/opportunity/[id]/time/+server.ts b/ui/src/routes/sales/opportunity/[id]/time/+server.ts
new file mode 100644
index 0000000..7b14729
--- /dev/null
+++ b/ui/src/routes/sales/opportunity/[id]/time/+server.ts
@@ -0,0 +1,32 @@
+import { optima } from "$lib";
+import { json, error } from "@sveltejs/kit";
+import type { RequestHandler } from "./$types";
+
+/** POST /sales/opportunity/[id]/time — log time against an activity */
+export const POST: RequestHandler = async ({ params, request, locals }) => {
+  const accessToken = locals.session?.accessToken;
+  if (!accessToken) throw error(401, "Unauthorized");
+
+  const body = await request.json();
+
+  try {
+    const result = await optima.sales.addTimeToActivity(
+      accessToken,
+      params.id,
+      body,
+    );
+    return json(result);
+  } catch (err: unknown) {
+    console.error("[Time] Failed to log time:", err);
+    const status =
+      err && typeof err === "object" && "status" in err
+        ? (err as { status: number }).status
+        : 500;
+    const message =
+      err && typeof err === "object" && "response" in err
+        ? ((err as { response?: { data?: { message?: string } } }).response
+            ?.data?.message ?? "Failed to log time")
+        : "Failed to log time";
+    throw error(status, message);
+  }
+};
diff --git a/ui/src/styles/sales/opportunitydetail.css b/ui/src/styles/sales/opportunitydetail.css
index 59071e6..17caf0d 100644
--- a/ui/src/styles/sales/opportunitydetail.css
+++ b/ui/src/styles/sales/opportunitydetail.css
@@ -4668,6 +4668,52 @@
   flex-shrink: 0;
 }
 
+/* ── Pending Schedule Entries ── */
+.wf-schedule-entries {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  margin-top: 8px;
+  padding: 7px 10px;
+  border-radius: 8px;
+  background: rgba(59, 130, 246, 0.05);
+  border: 1px solid rgba(59, 130, 246, 0.15);
+}
+
+.wf-se-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 11.5px;
+  color: var(--text-primary);
+  min-width: 0;
+}
+
+.wf-se-icon {
+  flex-shrink: 0;
+  color: #3b82f6;
+}
+
+.wf-se-name {
+  font-weight: 500;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  flex: 1;
+  min-width: 0;
+}
+
+.wf-se-time {
+  color: var(--text-muted);
+  white-space: nowrap;
+  flex-shrink: 0;
+  font-size: 11px;
+}
+
+.wf-panel-inline .wf-schedule-entries {
+  display: none;
+}
+
 /* ── Error Banner ── */
 .wf-error-banner {
   display: flex;
@@ -4801,6 +4847,35 @@
   box-shadow: 0 2px 4px rgba(22, 163, 74, 0.1);
 }
 
+/* Icon-only square button (calendar / clock) */
+.wf-action-btn.wf-btn-icon {
+  width: 32px;
+  height: 32px;
+  padding: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid var(--border-color, #444);
+  border-radius: 6px;
+  background: transparent;
+  color: var(--text-muted, #888);
+  flex-shrink: 0;
+  cursor: pointer;
+  transition: border-color 0.15s, color 0.15s, background 0.15s;
+}
+.wf-action-btn.wf-btn-icon:hover:not(:disabled) {
+  border-color: var(--accent-color, #4f8ef7);
+  color: var(--accent-color, #4f8ef7);
+  background: var(--accent-bg, rgba(79, 142, 247, 0.08));
+}
+
+/* Small spinner for icon buttons */
+.wf-spinner.wf-spinner-sm {
+  width: 14px;
+  height: 14px;
+  border-width: 2px;
+}
+
 /* Action group separator */
 .wf-action-sep {
   width: 1px;
@@ -6022,19 +6097,34 @@
   /* Subtle highlight for open/active activities */
 }
 
+/* Nested child activities (parent-child hierarchy via Parent_Activity field) */
+.at-entry.at-entry-child {
+  margin-left: 28px;
+  position: relative;
+}
+.at-entry.at-entry-child::before {
+  content: "";
+  position: absolute;
+  left: -16px;
+  top: 12px;
+  width: 12px;
+  height: 2px;
+  background: var(--card-border);
+}
+
 /* ── Connector (dots + lines) ── */
 .at-connector {
   display: flex;
   flex-direction: column;
   align-items: center;
-  width: 24px;
+  width: 20px;
   flex-shrink: 0;
   padding-top: 2px;
 }
 
 .at-dot {
-  width: 24px;
-  height: 24px;
+  width: 20px;
+  height: 20px;
   border-radius: 50%;
   background: rgba(59, 130, 246, 0.12);
   color: #3b82f6;
@@ -6047,7 +6137,6 @@
 .at-dot.at-dot-open {
   background: rgba(34, 197, 94, 0.15);
   color: #16a34a;
-  box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
 }
 
 .at-dot.at-dot-system {
@@ -6266,3 +6355,11 @@
 .at-closed-at {
   opacity: 0.7;
 }
+
+.at-schedule-time {
+  display: inline-flex;
+  align-items: center;
+  gap: 3px;
+  font-size: 11px;
+  color: var(--accent-color, #4f8ef7);
+}
