Skip to content

feat: notify staff on task complete#72

Merged
hmbanan666 merged 2 commits into
mainfrom
notifications-rework
Aug 18, 2025
Merged

feat: notify staff on task complete#72
hmbanan666 merged 2 commits into
mainfrom
notifications-rework

Conversation

@hmbanan666
Copy link
Copy Markdown
Collaborator

@hmbanan666 hmbanan666 commented Aug 18, 2025

Summary by CodeRabbit

  • New Features
    • Added notification bell with unread indicator (and “N” shortcut) in the header that opens a notifications drawer.
    • Introduced a notifications drawer showing author, title, description, and localized timestamps.
    • Notifications refresh on mount and every 30 seconds.
    • Staff members receive notifications when another staff member completes a task.
    • Notifications close when navigating to a new page.
    • Notifications list ordered newest-first and capped for performance.
    • Unread/read state (viewed tracking) supported.

@hmbanan666 hmbanan666 self-assigned this Aug 18, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 18, 2025

Walkthrough

Adds a notifications feature: UI (header bell, unread indicator, drawer), app state to open/close it, store/type refactors, server-side creation of task-completed notifications for staff, repository and DB schema changes, and updates notification polling/refresh timing.

Changes

Cohort / File(s) Summary
App lifecycle updates
apps/web-app/app/app.vue
Moves notification.update() out of initial Promise.all, adds it to onMounted Promise.all, and includes it in the 30s interval updates.
Header UI and trigger
apps/web-app/app/components/Header.vue, apps/web-app/app/components/HeaderMenuButton.vue
Adds notification bell with unread red chip (computed from unviewed notifications); clicking opens notifications drawer via useApp(); sets UButton square prop in menu button.
Notifications drawer UI
apps/web-app/app/components/NotificationsDrawer.vue, apps/web-app/app/layouts/default.vue
New NotificationsDrawer component (UDrawer) showing notifications list with author avatar, title, description, localized timestamp; drawer included in default layout and bound to isNotificationsOpened.
App composable state
apps/web-app/app/composables/useApp.ts
Adds isNotificationsOpened: Ref<boolean> state, resets it on route change, and exposes it from the composable.
Notification store refactor
apps/web-app/app/stores/notification.ts
Introduces TaskWithPerformer and updates NotificationWithEntities to include author and `task: TaskWithPerformer
Task completion notifications (API)
apps/web-app/server/api/task/id/[taskId]/complete.post.ts
After a staff user completes a task, creates per-recipient task_completed notifications for other staff users with authorId, userId, taskId, title and description (report or fallback).
Repository enhancements
packages/database/src/repository/notification.ts
Imports sql; listByUser orders by createdAt desc, limits 250, includes author relation; update sets updatedAt = sql\now()``.
Database schema and relations
packages/database/src/tables.ts
notifications table: adds viewedAt (timestamp), makes description nullable, adds authorId FK to users.id (cascade); taskRelations gains notifications: many(...); notificationRelations gains author relation.

Sequence Diagram(s)

sequenceDiagram
  actor Staff as Staff User
  participant UI as Web App UI
  participant API as Server API
  participant DB as Database

  Staff->>UI: Complete task
  UI->>API: POST /api/task/:id/complete
  API->>DB: Update task status
  API->>DB: Query staff users (exclude actor)
  loop per recipient
    API->>DB: Insert notification(authorId, userId, taskId, title, description)
  end
  API-->>UI: 200 OK
Loading
sequenceDiagram
  participant Header as Header Bell
  participant App as useApp()
  participant Drawer as NotificationsDrawer
  participant Store as NotificationStore

  Header->>App: set isNotificationsOpened = true
  App-->>Drawer: isNotificationsOpened (ref)
  Drawer->>Store: read notifications[]
  Drawer->>Drawer: render list (author, title, desc, time)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: tickets dir in atrium #53 — Modifies app startup/periodic store update logic similar to relocation/addition of store.update calls (tickets vs notifications).

Poem

A bell upon the header rings,
A drawer slides out on gentle springs,
Red chip blinks for news that's new,
Staff notes sent from me to you.
I twitch my whiskers—hop!—and view. 🐇🔔


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d4418d9 and c761a68.

📒 Files selected for processing (1)
  • apps/web-app/app/components/NotificationsDrawer.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web-app/app/components/NotificationsDrawer.vue
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch notifications-rework

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (16)
apps/web-app/app/components/HeaderMenuButton.vue (1)

2-8: Add an accessible label for the icon-only button

Icon-only buttons need an aria-label for screen readers. Consider adding a label (and optionally reflect expanded state if applicable).

Apply this diff:

   <UButton
     icon="i-lucide-menu"
     color="neutral"
     variant="outline"
     square
+    aria-label="Open menu"
     @click="isNavbarOpened = true"
   />
apps/web-app/app/composables/useApp.ts (1)

8-14: DRY up overlay closing with a helper

Minor: centralize closing logic (navbar + notifications) to avoid duplicating assignments and to enable reuse elsewhere (e.g., ESC handler).

   watch(
     () => route.fullPath,
-    () => {
-      isNavbarOpened.value = false
-      isNotificationsOpened.value = false
-    },
+    () => closeOverlays(),
   )
 
   return {
     isNavbarOpened,
     isNotificationsOpened,
     imagesMode,
+    closeOverlays,
   }
 }
 
 export const useApp = createSharedComposable(_useApp)
+
+function closeOverlays() {
+  isNavbarOpened.value = false
+  isNotificationsOpened.value = false
+}

Also applies to: 16-21

packages/database/src/tables.ts (1)

367-387: Add indexes for frequent queries (list by user ordered by createdAt)

Your repository queries notifications by userId ordered by createdAt. Adding a composite index improves latency at scale.

Add the extra config block:

// Place right under the notifications pgTable definition
import { index } from 'drizzle-orm/pg-core'

export const notifications = pgTable('notifications', {
  // columns...
}, (table) => {
  return {
    userCreatedAtIdx: index('notifications_user_id_created_at_idx').on(table.userId, table.createdAt),
  }
})
apps/web-app/app/stores/notification.ts (3)

17-25: Strongly type the fetch for safer assignments

Type the fetch result to ensure notifications.value stays consistent and catch backend/typing drift at compile time.

-      const data = await $fetch('/api/notification/my')
+      const data = await $fetch<NotificationWithEntities[]>('/api/notification/my')

37-69: Consider enriching the toast content

Optional: Use the author as the avatar fallback (if performer is absent) and set a meaningful alt. Also, your second action label is hard-coded to "0".

   toastContext.add({
     id: notification.id,
     title: notification.title,
     description: notification.description ?? '',
     avatar: {
-      src: notification.task?.performer?.avatarUrl ?? undefined,
-      alt: '',
+      src: notification.task?.performer?.avatarUrl ?? notification.author.avatarUrl ?? undefined,
+      alt: notification.author.name || '',
     },
     color: 'info',
     duration: 60000,
     actions: [{
       icon: 'i-lucide-thumbs-up',
       label: 'Лайк',
       color: 'neutral',
       variant: 'outline',
       size: 'md',
       onClick: (e) => {
         e?.stopPropagation()
       },
     }, {
       icon: 'fluent-emoji-flat:party-popper',
-      label: '0',
+      label: '0', // TODO: replace with dynamic metric if/when available
       color: 'info',
       variant: 'soft',
       size: 'md',
       ui: {
         label: 'font-semibold',
         leadingIcon: 'motion-preset-pulse motion-duration-1500',
       },
       disabled: true,
     }],
   })

98-109: Stale watcher block and outdated function name

The watcher is commented out and references showCompletedTaskToast which no longer exists. Either remove it or update the reference if you intend to re-enable.

Option A — remove the commented block (cleanest):

-  // watch(notifications, () => {
-  //   for (const notification of notifications.value) {
-  //     // already shown?
-  //     if (toastContext.toasts.value.find((toast) => toast.id === notification.id)) {
-  //       continue
-  //     }
-  //
-  //     if (notification.type === 'task_completed') {
-  //       showCompletedTaskToast(notification)
-  //     }
-  //   }
-  // })
+  // Intentionally left disabled: toast auto-display on updates.

Option B — fix the function name if you plan to re-enable:

-  //       showCompletedTaskToast(notification)
+  //       _showCompletedTaskToast(notification)
apps/web-app/app/app.vue (1)

68-92: Avoid overlapping polling cycles; fix interval typing for SSR/CSR

setInterval doesn’t await your async work; if requests exceed 30s, cycles overlap. Also, NodeJS.Timeout can be incorrect in browsers. Use a reentrancy guard and a portable interval type.

-// Auto Update Online
-let interval: NodeJS.Timeout
+// Auto Update Online
+let interval: ReturnType<typeof setInterval>
+let isRefreshing = false
+
+async function refreshAll() {
+  if (isRefreshing) return
+  isRefreshing = true
+  try {
+    await Promise.all([
+      user.updateOnline(),
+      user.update(),
+      task.update(),
+      ticket.update(),
+      notification.update(),
+    ])
+  } finally {
+    isRefreshing = false
+  }
+}
 
 onMounted(async () => {
-  await Promise.all([
-    user.updateOnline(),
-    user.update(),
-    task.update(),
-    ticket.update(),
-    notification.update(),
-  ])
+  await refreshAll()
 
   interval = setInterval(async () => {
-    await Promise.all([
-      user.updateOnline(),
-      user.update(),
-      task.update(),
-      ticket.update(),
-      notification.update(),
-    ])
+    await refreshAll()
   }, 30000)
 })
 
 onUnmounted(() => {
   clearInterval(interval)
 })
packages/database/src/repository/notification.ts (3)

16-18: Add an index to support where(userId)=… and order by createdAt desc

The query now filters by userId, sorts by createdAt desc, and limits 250. To keep it fast at scale, add an index compatible with this access pattern (e.g., composite on (user_id, created_at desc) or (user_id, created_at)). If you already have one, ignore.

Example migration (pseudo):

CREATE INDEX IF NOT EXISTS idx_notifications_user_created_at
  ON notifications (user_id, created_at DESC);

19-25: Trim related columns to reduce payload and PII surface

with: { author: true, task: { with: { performer: true } } } will hydrate full user/task/performer models. If the UI needs only a subset (e.g., id, name, surname, avatarUrl), select those columns to reduce response size and exposure.

Example (Drizzle style):

return useDatabase().query.notifications.findMany({
  where: (n, { eq }) => eq(n.userId, userId),
  orderBy: (n, { desc }) => desc(n.createdAt),
  limit: 250,
  with: {
    author: {
      columns: { id: true, name: true, surname: true, avatarUrl: true },
    },
    task: {
      columns: { id: true, name: true },
      with: {
        performer: {
          columns: { id: true, name: true, surname: true, avatarUrl: true },
        },
      },
    },
  },
})

16-18: Consider making the limit configurable and centralized

Hardcoding 250 is fine for now, but a constant or parameter improves reuse and testing.

-      limit: 250,
+      limit: MAX_NOTIFICATIONS,

Additionally:

// packages/database/src/repository/constants.ts
export const MAX_NOTIFICATIONS = 250
apps/web-app/server/api/task/id/[taskId]/complete.post.ts (2)

99-101: Avoid loading all users; fetch only active staff server-side

Loading all users and filtering in memory won’t scale. Prefer a repository method or query that returns only active staff (and excludes the actor) to minimize data and DB load.

For example, introduce repository.user.listActiveStaffExcept(userId) and use it here:

const recipients = await repository.user.listActiveStaffExcept(user.id)

Interim in-file improvement (still not ideal DB-wise):

-const users = await repository.user.list()
-const allStaffExceptUser = users.filter((u) => u.type === 'staff' && u.id !== user.id)
+const users = await repository.user.list()
+const allStaffExceptUser = users.filter((u) => u.type === 'staff' && u.isActive && u.id !== user.id)

102-111: Create notifications concurrently to reduce latency

Creating notifications one-by-one increases request time for large recipient sets. Batching or concurrency helps. Given current APIs, Promise.all is a simple win; for even better performance, consider a repository.notification.createMany to insert in bulk.

Apply this diff:

-for (const staff of allStaffExceptUser) {
-  await repository.notification.create({
-    authorId: user.id,
-    userId: staff.id,
-    taskId: updatedTask.id,
-    type: 'task_completed',
-    title: `${suffixByGender(['Завершил', 'Завершила'], user.gender)} задачу «${updatedTask.name}»`,
-    description: updatedTask.report ? updatedTask.report : 'Без отчета',
-  })
-}
+await Promise.all(
+  allStaffExceptUser.map((staff) =>
+    repository.notification.create({
+      authorId: user.id,
+      userId: staff.id,
+      taskId: updatedTask.id,
+      type: 'task_completed',
+      title: `${suffixByGender(['Завершил', 'Завершила'], user.gender)} задачу «${updatedTask.name}»`,
+      description: updatedTask.report ? updatedTask.report : 'Без отчета',
+    }),
+  ),
+)

If you’re concerned about connection saturation, chunk the array into small batches (e.g., size 10) and await each batch.

apps/web-app/app/components/NotificationsDrawer.vue (2)

41-43: Guard nullable descriptions to avoid rendering “null”

description is nullable in the schema. Rendering it directly may show “null”. Guard it or provide a fallback.

Apply this diff:

-<p class="text-sm text-dimmed">
-  {{ notification.description }}
-</p>
+<p v-if="notification.description" class="text-sm text-dimmed">
+  {{ notification.description }}
+</p>

17-21: Accessibility: add avatar alt text

If UAvatar supports an alt prop, pass the author’s name to improve accessibility.

Example:

-<UAvatar
-  :src="notification.author.avatarUrl ?? undefined"
-  size="lg"
-/>
+<UAvatar
+  :src="notification.author.avatarUrl ?? undefined"
+  :alt="`${notification.author.name} ${notification.author.surname}`"
+  size="lg"
/>
apps/web-app/app/components/Header.vue (2)

14-29: Hook up the “N” shortcut to actually open the drawer

Tooltip advertises a shortcut, but there’s no listener here. If there isn’t a global shortcut system elsewhere, attach a key listener.

You can add a simple listener:

 <script setup lang="ts">
 defineProps<{ title: string }>()
 
 const { isNotificationsOpened } = useApp()
 
 const notificationStore = useNotificationStore()
 const haveNotifications = computed(() => notificationStore.notifications.filter((notification) => !notification.viewedAt).length > 0)
+
+onMounted(() => {
+  const handler = (e: KeyboardEvent) => {
+    if ((e.key === 'n' || e.key === 'N') && !e.metaKey && !e.ctrlKey && !e.altKey) {
+      isNotificationsOpened.value = true
+    }
+  }
+  window.addEventListener('keydown', handler)
+  onBeforeUnmount(() => window.removeEventListener('keydown', handler))
+})
 </script>

If you already have a global shortcuts plugin, ignore and keep the tooltip label only.


47-49: Micro-optimization: use some() for unread check

some() short-circuits on first unread item and reads cleaner.

Apply this diff:

-const haveNotifications = computed(() => notificationStore.notifications.filter((notification) => !notification.viewedAt).length > 0)
+const haveNotifications = computed(() => notificationStore.notifications.some((n) => !n.viewedAt))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5bb8a1e and d4418d9.

📒 Files selected for processing (10)
  • apps/web-app/app/app.vue (2 hunks)
  • apps/web-app/app/components/Header.vue (2 hunks)
  • apps/web-app/app/components/HeaderMenuButton.vue (1 hunks)
  • apps/web-app/app/components/NotificationsDrawer.vue (1 hunks)
  • apps/web-app/app/composables/useApp.ts (1 hunks)
  • apps/web-app/app/layouts/default.vue (1 hunks)
  • apps/web-app/app/stores/notification.ts (3 hunks)
  • apps/web-app/server/api/task/id/[taskId]/complete.post.ts (1 hunks)
  • packages/database/src/repository/notification.ts (3 hunks)
  • packages/database/src/tables.ts (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
apps/web-app/app/app.vue (4)
apps/web-app/app/stores/ticket.ts (1)
  • update (12-30)
apps/web-app/app/stores/activity.ts (2)
  • update (10-25)
  • schedules (7-32)
apps/web-app/app/stores/user.ts (1)
  • update (25-54)
apps/web-app/app/stores/partner.ts (1)
  • update (28-48)
apps/web-app/app/composables/useApp.ts (1)
apps/web-app/app/composables/useTicket.ts (2)
  • _useTicket (1-31)
  • route (24-24)
packages/database/src/repository/notification.ts (4)
packages/database/src/tables.ts (1)
  • notifications (367-387)
packages/database/src/repository/activity.ts (2)
  • Activity (6-71)
  • updateSchedule (40-50)
packages/database/src/repository/user.ts (1)
  • update (80-90)
packages/database/src/repository/ticket.ts (1)
  • update (58-68)
apps/web-app/server/api/task/id/[taskId]/complete.post.ts (3)
packages/database/src/tables.ts (1)
  • users (83-100)
packages/database/src/repository/index.ts (1)
  • repository (57-57)
apps/web-app/shared/utils/gender.ts (1)
  • suffixByGender (3-11)
apps/web-app/app/stores/notification.ts (2)
packages/database/src/types.ts (3)
  • Task (69-69)
  • User (9-9)
  • Notification (78-78)
packages/database/src/repository/notification.ts (1)
  • Notification (6-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (15)
apps/web-app/app/composables/useApp.ts (1)

5-13: Good addition: notifications drawer state + route-change reset

Adding isNotificationsOpened and resetting it on route changes mirrors the navbar behavior and prevents sticky drawers across navigations. Looks consistent with the new UI.

packages/database/src/tables.ts (5)

371-371: Adding viewedAt enables unread state — LGTM

Nullable viewedAt is a clean way to model unread notifications and aligns with the UI.


374-374: Make description nullable — LGTM

This unblocks notifications without descriptions and matches the frontend’s null-safe usage.


867-869: Add inverse relation tasks -> notifications — LGTM

This simplifies loading notifications for a task and matches the FK on notifications.taskId.


888-891: Add author relation — LGTM

Aligns with UI requirements to show the author. Matches repository .with({ author: true }).


375-378: Reconsider authorId constraints and ensure safe migration/backfill

Please address the following before merging:

• Add or verify a migration that adds author_id to notifications with a safe backfill for existing rows (e.g. populate from audit log or default) so that the NOT NULL constraint won’t fail at deploy.
• Review all schema migration scripts (SQL or TS) and confirm they include the backfill and constraint change.
• Decide on the desired deletion behavior and update the foreign‐key accordingly:
onDelete: 'restrict' to prevent deleting authors with notifications (preserve history), or
– make authorId nullable with onDelete: 'set null' if you must allow user deletions and preserve notifications.
• Audit every insert(notifications).values({…}) call in the codebase to ensure authorId is always provided.

Suggested schema update (pick one):

-  authorId: cuid2('author_id').notNull().references(() => users.id, {
-    onDelete: 'cascade',
-    onUpdate: 'cascade',
-  }),
+  // Option A: Preserve history by preventing author deletion
+  authorId: cuid2('author_id').notNull().references(() => users.id, {
+    onDelete: 'restrict',
+    onUpdate: 'cascade',
+  }),
+  
+  // Option B: Allow author deletion and keep notifications
+  // authorId: cuid2('author_id').references(() => users.id, {
+  //   onDelete: 'set null',
+  //   onUpdate: 'cascade',
+  // }),
apps/web-app/app/stores/notification.ts (2)

3-10: Types aligned with backend shape — LGTM

Introducing TaskWithPerformer and adding author to NotificationWithEntities matches repository .with({ author, task: { performer } }).


41-44: Null-safe UI data access — LGTM

Using ?? '' for description and ?. for avatar prevents runtime errors on incomplete entities.

apps/web-app/app/app.vue (1)

55-66: Intentional move of notification update to onMounted — confirm SSR expectations

You moved notification.update() out of the initial pre-mount batch and into onMounted/interval. This avoids SSR-time fetches but means the drawer starts empty until mount. If that’s intentional, all good; otherwise consider adding it back to the initial Promise.all to hydrate earlier.

Also applies to: 70-78

apps/web-app/app/layouts/default.vue (1)

8-9: LGTM: Notifications drawer inclusion is well-placed

Rendering the NotificationsDrawer at the layout root (as a sibling to USlideover) is appropriate for overlays. No issues spotted here.

packages/database/src/repository/notification.ts (2)

2-2: Consistent use of sql tag import

Bringing in sql from drizzle-orm aligns this repo with other repositories’ update patterns.


37-40: Good: updatedAt is maintained on update

Merging data with updatedAt: sqlnow() mirrors other repos and keeps timestamps accurate.

apps/web-app/server/api/task/id/[taskId]/complete.post.ts (1)

97-113: Confirm intended trigger: notify staff only when staff completes, or always notify staff?

Current guard only notifies when user.type === 'staff'. If tasks can be completed by non-staff (e.g., vendors/clients) and staff should still be notified, adjust the condition.

Would you like me to patch the condition and tests if the intent is “notify staff on any task completion”?

apps/web-app/app/components/NotificationsDrawer.vue (1)

28-33: Timezone/SRR hydration sanity check

format(new Date(notification.createdAt), ...) runs at render time; ensure createdAt includes timezone (it does per schema) to avoid hydration drift. No change required if you’ve verified.

If hydration mismatches are observed, we can render the time client-only or normalize via UTC.

apps/web-app/app/components/Header.vue (1)

14-29: Nice addition: bell with unread indicator and tooltip

The UX is clear and consistent with the new drawer. Opening via isNotificationsOpened is straightforward.

Comment thread apps/web-app/app/components/NotificationsDrawer.vue
Comment on lines +97 to +113
// Notify all staff
if (user.type === 'staff') {
const users = await repository.user.list()
const allStaffExceptUser = users.filter((u) => u.type === 'staff' && u.id !== user.id)

for (const staff of allStaffExceptUser) {
await repository.notification.create({
authorId: user.id,
userId: staff.id,
taskId: updatedTask.id,
type: 'task_completed',
title: `${suffixByGender(['Завершил', 'Завершила'], user.gender)} задачу «${updatedTask.name}»`,
description: updatedTask.report ? updatedTask.report : 'Без отчета',
})
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing import: suffixByGender is used but not imported (will throw ReferenceError)

suffixByGender is referenced in the title construction here and within prepareBotMessage, but there’s no import in this module.

Apply this diff to the import block:

 import { getLocalizedResolution } from '~~/shared/utils/helpers'
+import { suffixByGender } from '~~/shared/utils/gender'
🤖 Prompt for AI Agents
In apps/web-app/server/api/task/id/[taskId]/complete.post.ts around lines
97-113, suffixByGender is used but not imported; add the appropriate import for
suffixByGender at the top import block (use the module where suffixByGender is
defined and match named/default export), then run typecheck/build to confirm the
import path and export style are correct so the ReferenceError is resolved.

@sonarqubecloud
Copy link
Copy Markdown

@hmbanan666 hmbanan666 merged commit 524af09 into main Aug 18, 2025
8 checks passed
@hmbanan666 hmbanan666 deleted the notifications-rework branch August 18, 2025 10:58
This was referenced Aug 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant