Commit Graph

8 Commits

Author SHA1 Message Date
225161249e feat(chat+push): image attachments, day-labeled inbox with reply state, sub self-heal
- MessageInput: paperclip button (team-only, controlled by teamUserIds
  from ThreadDetail) uploads through the media service and hands the URL
  to the WebSocket; Enter now inserts a newline — sending is button-only
  since accidental Enter-sends were noisy for staff replies.
- MessageBubble: renders image messages inline (tap to open), shows the
  coworker's username at the top of team-side bubbles so staff can tell
  who typed what.
- MessageList / ThreadChatPage: any message from a store team member
  (owner or active staff) renders on the "our" side of the bubble even
  for a viewer who isn't the sender, using team_user_ids from
  ThreadDetail. Delete option stays gated to the actual sender.
- ThreadsPage: inbox now shows smart-relative timestamps
  (today HH:MM / دیروز HH:MM / N روز پیش / Persian date), plus a
  reply-state icon per row (red bell = customer waiting, blue double-check
  = you replied and they've seen it, nothing = you replied not yet seen).
  Image-only last messages preview as "📷 تصویر".
- Push notifications: on mount we re-POST the current PushSubscription so
  a browser-rotated sub or a backend row deleted after a 410 auto-heals.
- useWebSocket: SendMessagePayload and NewMessageEvent carry image_url so
  the server relay can round-trip images.
- API types: MessageList.imageUrl, LastMessage.imageUrl,
  ThreadList.lastMessageFromTeam/otherPartySeenLast, ThreadDetail.teamUserIds.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-19 13:22:44 +00:00
6a300b8760 fix(store): don't bounce owners home, stop cross-user myStores cache
- app/hooks/useStoreOwnership.ts: the old redirect logic sent owners to /
  the moment useMyStores briefly resolved to []. That empty array came from
  useMyStores's dead `if (!token) return []` inside the queryFn, so any
  transient auth blip left the store page thinking the user has no stores.
  Now compares usernames case-insensitively, only redirects while the query
  is settled (!isLoading && !isFetching && !isError), and never bounces to
  home on empty — worst case the seller sees an empty dashboard rather than
  a silent kick-out.
- app/requestHandler/use-seller-hooks.ts: removed the unreachable
  `if (!token) return []` in useMyStores and added the token to the
  queryKey so a login switch stops serving the previous user's cached list.
- app/routes/store.$storeId._index.tsx: mount StorePushPromptCard at the
  top of the store dashboard.
- app/routes/store.$storeId.tsx: sidebar "پیام‌ها" badge reads
  badges.sellerChat[storeId] (per-store) instead of the buyer-side
  badges.chat, matching the badge-serializer split.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-18 13:18:23 +00:00
0b18438a97 fix(notif+auth): camelCase badge fields, seller push prompt, only 401 signs out
- app/components/MyLayout.tsx / app/components/home/HomePageTopBar.tsx /
  app/routes/profile._index.tsx: the BadgeCounts type was declared with
  snake_case fields (order_updates, seller_orders) while the API's global
  camelCase renderer sends orderUpdates / sellerOrders on the wire — every
  read went to undefined, so the "buyer activity" dot on the profile icon
  and the order-badge on the store dashboard were silently stuck at 0.
  Fixed by renaming the interface to match the wire and adding sellerChat
  (per-store roll-up) so the store dashboard sidebar reads
  badges.sellerChat[storeId] instead of the buyer-side chat count.
- app/hooks/useBadgeCounts.ts: interface + EMPTY constant updated to
  camelCase + new sellerChat map.
- app/utils/api-error-handler.ts: 403 no longer force-redirects to /logout.
  It used to force-log-out on ANY 403/401, so an approved-but-unbadged
  owner (Ehsan / Noelabel) whose store dashboard fired the Instagram-tab
  query got a 403 → forced logout → the store page vanished mid-load
  and the seller landed on the home screen convinced they'd been kicked
  off their own store. Only 401 signs the session out now.
- app/components/store/StorePushPromptCard.tsx: new dashboard-level card
  that nudges the store owner/staff to enable browser push. Unlike the
  global buyer banner it's not dismissible-forever; "بعداً یادآوری کن"
  hides for 24h and it comes back until the user actually subscribes.
  Handles the permission="denied" path with an in-browser-settings hint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-18 13:18:08 +00:00
bb44ebf0e5 fix(seller-team): guard owner-only routes + robust useMyStores
Review fixes:
- useMyStores throws on a failed response instead of resolving to [] — a
  transient 401/blip no longer looks like "no stores" and bounces an owner off
  their own dashboard (query stays in error/retry, keeps prior data).
- Add useRequireOwner() guard and apply it to the owner-only routes
  (financial-dashboard + its cash-funds/reports/transactions sub-routes and
  shipping-method) so staff who hit those URLs directly are redirected to the
  dashboard. (Financial data was already backend-protected; this is the matching
  client-side gate.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:04:06 +03:30
a642fc0fae feat(seller-team): multi-user dashboard — store switcher, role gating, team UI
- useMyStores + team hooks (invite by phone / list / remove) in use-seller-hooks.
- useStoreOwnership rewritten to allow staff (not just the owner) via my-stores;
  exposes `role` so callers can gate owner-only UI, and no longer redirects
  members away from a store they help manage.
- Store settings: owner-only items (financial, shipping, edit store, instagram
  sync, team) hidden from staff; new "مدیریت اعضای تیم" entry (owner-only).
- New /store/:storeId/team page: invite by phone + members list with pending/
  active status + remove (owner-only, redirects staff). MyLayout keeps store mode
  on the team route.
- StoreForm edit is owner-only (redirects staff). Profile "enter my store" now
  routes owners to their store and staff to the store they manage.
- Chat already split earlier: seller inbox vs personal buyer inbox.

Depends on the seller-team backend; ships on this branch, not main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:43:53 +03:30
66b14cfa74 fix(notifications): read camelCase publicKey from VAPID endpoint
The backend view returns {"public_key": ...} but the global
CamelCaseJSONRenderer rewrites it to {"publicKey": ...} on the wire,
so destructuring `public_key` always resolved to undefined and the
subscribe path bailed silently. Browsers never registered with the
backend, no PushSubscription rows were ever created, and no push
notifications fired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-17 12:06:35 +00:00
fef53fd003 feat(notifications): web push opt-in + unread nav badges
Surfaces three events without a full notification center:
- service worker (public/sw.js) gains push + notificationclick handlers
  (RTL, focuses an existing tab or opens the target URL).
- usePushNotifications hook: permission + VAPID subscribe/unsubscribe
  against the backend push endpoints.
- opt-in UI both ways: a one-time dismissible post-login banner
  (PushPermissionBanner, rendered in MyLayout) and a permanent toggle
  (PushSettingToggle) on buyer profile settings and seller store settings.
- useBadgeCounts polls /api/notif/v1/badges/; small red badges on the
  bottom-nav profile icon (chat + order updates), the profile sidebar
  (پیام‌ها / سفارش‌ها) and the seller dashboard sidebar. Visiting orders
  clears its badge.

Degrades gracefully: if the backend endpoints are absent the counts fall
back to zero and nothing throws.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:02:56 +03:30
0293655fc1 first 2026-04-29 01:44:16 +03:30