docs: add prior-archive cutoff plan and ignore worktrees
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -32,3 +32,7 @@ playwright-report/
|
||||
graphify-out/
|
||||
**/graphify-out/
|
||||
.cursor/rules/graphify.mdc
|
||||
|
||||
# Local git worktrees + SDD scratch
|
||||
.worktrees/
|
||||
.superpowers/
|
||||
|
||||
@@ -0,0 +1,586 @@
|
||||
# Prior Archive Cutoff Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Split deals/activities into current vs archive by an org-wide Jalali cutoff date, with a one-way promote tag that never mutates other fields.
|
||||
|
||||
**Architecture:** Reuse `Organization.priorArchiveEnabledAt` as the cutoff (nullable = no filter). Add `promotedFromPriorAt` on `Deal` and `Activity`. Rewrite `prior-archive.ts` to filter only by `createdAt` + promote tag (drop Didar/`importSource` and the open-future exception). Expose cutoff on auth + org APIs; owner sets date and multi-selects users in Teams settings; promote action lives in the existing three-dot menus when lens is archive.
|
||||
|
||||
**Tech Stack:** NestJS, Prisma, node:test, `@mento/shared`, Vue 3, existing `JalaliDateInput`, `UiDropdownMenu`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-17-prior-data-archive-cutoff-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- npm workspaces only (`npm run … -w @mento/…`); never pnpm/yarn
|
||||
- Prisma only from `apps/api` (needs `apps/api/.env`)
|
||||
- Shell is PowerShell: use `;` not `&&`
|
||||
- Transfer never changes `createdAt` or any field except `promotedFromPriorAt`
|
||||
- Filter applies only to deals and activities; people/leads/rules/etc. stay unfiltered
|
||||
- Without user prior-archive grant: no switch, no filter (full environment)
|
||||
- Without cutoff date: no filter even if user is granted
|
||||
- After code edits: `graphify update .` (may crash; non-blocking)
|
||||
- Do not start implementation commits unless the user asked to implement / commit
|
||||
|
||||
## File Map
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `apps/api/prisma/schema.prisma` | `promotedFromPriorAt` on Deal/Activity; comment on org cutoff |
|
||||
| `apps/api/prisma/migrations/*` | Migration |
|
||||
| `apps/api/src/common/prior-archive.ts` | Pure where builders for deal/activity |
|
||||
| `apps/api/test/prior-archive.test.ts` | Unit tests for new rules |
|
||||
| `packages/shared/src/index.ts` | DTO fields: cutoff, promote flag, auth priorArchive shape |
|
||||
| `apps/api/src/auth/jwt.strategy.ts` | `priorCutoff` only when grant + cutoff present |
|
||||
| `apps/api/src/auth/auth.service.ts` | Expose `priorArchive.cutoffAt` |
|
||||
| `apps/api/src/billing/billing.service.ts` | Stop auto-stamping cutoff on module enable |
|
||||
| `apps/api/src/organizations/*` | Owner PATCH cutoff date |
|
||||
| `apps/api/src/pipelines/*` + `activities/*` | Use new where; promote endpoints; DTO mapping |
|
||||
| `apps/api/src/contacts/people.service.ts` | Remove person prior filter |
|
||||
| `apps/api/src/contacts/companies.service.ts` | Remove company prior filter |
|
||||
| `apps/api/src/reports/reports.service.ts` | Drop person prior; pass cutoff to deal/activity |
|
||||
| `apps/api/src/ai/ai-ask.service.ts` | Same |
|
||||
| `apps/web/src/components/TeamsSettingsPanel.vue` | Date next to h3 + multi-select users |
|
||||
| `apps/web/src/views/ActivitiesView.vue` | Promote in three-dot menu |
|
||||
| `apps/web/src/views/PipelineBoardView.vue` | Promote in deal menu-pop |
|
||||
| `apps/web/src/views/PersonProfileView.vue` | Client split by cutoff + promote, not Didar |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Schema + shared DTO fields
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/api/prisma/schema.prisma`
|
||||
- Create: migration under `apps/api/prisma/migrations/`
|
||||
- Modify: `packages/shared/src/index.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `Deal.promotedFromPriorAt DateTime?`
|
||||
- `Activity.promotedFromPriorAt DateTime?`
|
||||
- `Organization.priorArchiveEnabledAt` comment = cutoff date (start of selected day)
|
||||
- `AuthUserDto.priorArchive?: { lens: 'current' \| 'archive'; cutoffAt: string | null } | null`
|
||||
- `OrganizationDto.priorArchiveCutoffAt?: string | null`
|
||||
- `DealDto.promotedFromPriorAt: string | null`
|
||||
- `ActivityDto.promotedFromPriorAt?: string | null`
|
||||
- `UpdatePriorArchiveCutoffRequest { cutoffAt: string | null }` (ISO date or null)
|
||||
|
||||
- [ ] **Step 1: Update Prisma models**
|
||||
|
||||
On `Organization`, change comment of `priorArchiveEnabledAt` to:
|
||||
`/// تاریخ قطع بایگانی اطلاعات قبل (شروع روز انتخابشده)؛ null = بدون فیلتر`
|
||||
|
||||
On `Deal` and `Activity`, after `importSource` (or near `createdAt`), add:
|
||||
```prisma
|
||||
/// اگر پر باشد، رکورد از بایگانی به کار جاری منتقل شده (فقط برچسب؛ بقیه فیلدها دستنخورده)
|
||||
promotedFromPriorAt DateTime?
|
||||
```
|
||||
|
||||
Add indexes optional: `@@index([organizationId, createdAt])` only if missing; do not over-index.
|
||||
|
||||
- [ ] **Step 2: Create migration**
|
||||
|
||||
```powershell
|
||||
cd apps/api; npx prisma migrate dev --name prior_archive_promote_cutoff
|
||||
```
|
||||
|
||||
Expected: migration applies; client generates.
|
||||
|
||||
- [ ] **Step 3: Shared types** in `packages/shared/src/index.ts`
|
||||
|
||||
```ts
|
||||
// AuthUserDto
|
||||
priorArchive?: { lens: "current" | "archive"; cutoffAt: string | null } | null;
|
||||
|
||||
// OrganizationDto
|
||||
priorArchiveCutoffAt?: string | null;
|
||||
|
||||
export interface UpdatePriorArchiveCutoffRequest {
|
||||
/** ISO datetime (start of selected Jalali day) or null to clear */
|
||||
cutoffAt: string | null;
|
||||
}
|
||||
|
||||
// DealDto
|
||||
promotedFromPriorAt: string | null;
|
||||
|
||||
// ActivityDto
|
||||
promotedFromPriorAt?: string | null;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build shared**
|
||||
|
||||
```powershell
|
||||
npm run build -w @mento/shared
|
||||
```
|
||||
|
||||
Expected: success.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Rewrite `prior-archive.ts` (TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/api/test/prior-archive.test.ts` (replace entirely)
|
||||
- Modify: `apps/api/src/common/prior-archive.ts` (replace filter logic)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: cutoff `Date | null | undefined`, lens
|
||||
- Produces:
|
||||
- `dealPriorWhere(lens, cutoff): Prisma.DealWhereInput`
|
||||
- `activityPriorWhere(lens, cutoff): Prisma.ActivityWhereInput`
|
||||
- Keep `actorPriorLens`, `PriorLens`, `listPriorLens` if still used; `personPriorWhere` / `companyPriorWhere` become always `{}` (or delete and fix imports in Task 3)
|
||||
|
||||
- [ ] **Step 1: Write failing tests** — replace `apps/api/test/prior-archive.test.ts` with:
|
||||
|
||||
```ts
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
activityPriorWhere,
|
||||
dealPriorWhere,
|
||||
personPriorWhere,
|
||||
} from '../src/common/prior-archive';
|
||||
|
||||
const cutoff = new Date('2026-10-01T00:00:00.000Z');
|
||||
|
||||
describe('بایگانی اطلاعات قبل — تاریخ قطع', () => {
|
||||
it('بدون لنز یا بدون تاریخ فیلتر نمیگذارد', () => {
|
||||
assert.deepEqual(dealPriorWhere('off', cutoff), {});
|
||||
assert.deepEqual(dealPriorWhere('current', null), {});
|
||||
assert.deepEqual(activityPriorWhere('archive', undefined), {});
|
||||
assert.deepEqual(personPriorWhere('current'), {});
|
||||
});
|
||||
|
||||
it('کار جاری: بعد از قطع یا منتقلشده', () => {
|
||||
assert.deepEqual(dealPriorWhere('current', cutoff), {
|
||||
OR: [
|
||||
{ createdAt: { gte: cutoff } },
|
||||
{ promotedFromPriorAt: { not: null } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('بایگانی: قبل از قطع و بدون برچسب انتقال', () => {
|
||||
assert.deepEqual(activityPriorWhere('archive', cutoff), {
|
||||
createdAt: { lt: cutoff },
|
||||
promotedFromPriorAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('استثنای موعد آینده دیگر نیست', () => {
|
||||
const w = JSON.stringify(dealPriorWhere('current', cutoff));
|
||||
assert.equal(w.includes('expectedCloseAt'), false);
|
||||
assert.equal(w.includes('didar'), false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests — expect FAIL**
|
||||
|
||||
```powershell
|
||||
cd apps/api; npx tsx --test test/prior-archive.test.ts
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Implement** `apps/api/src/common/prior-archive.ts`:
|
||||
|
||||
```ts
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import type { Actor } from './record-scope';
|
||||
|
||||
export type PriorLens = 'off' | 'current' | 'archive';
|
||||
|
||||
export function actorPriorLens(actor: Pick<Actor, 'priorLens'> | undefined): PriorLens {
|
||||
return actor?.priorLens ?? 'off';
|
||||
}
|
||||
|
||||
function hasCutoff(cutoff?: Date | null): cutoff is Date {
|
||||
return cutoff instanceof Date && !Number.isNaN(cutoff.getTime());
|
||||
}
|
||||
|
||||
/** شخص/شرکت دیگر با این لنز فیلتر نمیشوند. */
|
||||
export function personPriorWhere(_lens?: PriorLens): Prisma.PersonWhereInput {
|
||||
return {};
|
||||
}
|
||||
|
||||
export function companyPriorWhere(_lens?: PriorLens): Prisma.CompanyWhereInput {
|
||||
return {};
|
||||
}
|
||||
|
||||
export function dealPriorWhere(
|
||||
lens: PriorLens | undefined,
|
||||
cutoff?: Date | null,
|
||||
): Prisma.DealWhereInput {
|
||||
if (lens !== 'current' && lens !== 'archive') return {};
|
||||
if (!hasCutoff(cutoff)) return {};
|
||||
if (lens === 'current') {
|
||||
return {
|
||||
OR: [
|
||||
{ createdAt: { gte: cutoff } },
|
||||
{ promotedFromPriorAt: { not: null } },
|
||||
],
|
||||
};
|
||||
}
|
||||
return { createdAt: { lt: cutoff }, promotedFromPriorAt: null };
|
||||
}
|
||||
|
||||
export function activityPriorWhere(
|
||||
lens: PriorLens | undefined,
|
||||
cutoff?: Date | null,
|
||||
): Prisma.ActivityWhereInput {
|
||||
if (lens !== 'current' && lens !== 'archive') return {};
|
||||
if (!hasCutoff(cutoff)) return {};
|
||||
if (lens === 'current') {
|
||||
return {
|
||||
OR: [
|
||||
{ createdAt: { gte: cutoff } },
|
||||
{ promotedFromPriorAt: { not: null } },
|
||||
],
|
||||
};
|
||||
}
|
||||
return { createdAt: { lt: cutoff }, promotedFromPriorAt: null };
|
||||
}
|
||||
|
||||
/** فهرست شخص/شرکت: همیشه بدون فیلتر لنز (سازگاری با call-siteهای قدیمی). */
|
||||
export function listPriorLens(
|
||||
_lens: PriorLens | undefined,
|
||||
_q: string | undefined,
|
||||
_explicitSource: boolean,
|
||||
): PriorLens {
|
||||
return 'off';
|
||||
}
|
||||
```
|
||||
|
||||
Remove `PRIOR_IMPORT_SOURCE`, `liveDeal`, `liveActivity`, `historicalActivity`, `stillOpenAhead`.
|
||||
|
||||
- [ ] **Step 4: Run tests — expect PASS**
|
||||
|
||||
```powershell
|
||||
cd apps/api; npx tsx --test test/prior-archive.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Wire API call-sites + auth cutoff + stop auto-stamp
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/api/src/auth/jwt.strategy.ts`
|
||||
- Modify: `apps/api/src/auth/auth.service.ts` (`priorArchiveFor`)
|
||||
- Modify: `apps/api/src/billing/billing.service.ts` (do not set `priorArchiveEnabledAt` on module enable)
|
||||
- Modify call sites of `dealPriorWhere` / `activityPriorWhere` / person+company filters:
|
||||
- `apps/api/src/pipelines/pipelines.service.ts`
|
||||
- `apps/api/src/pipelines/deals-excel.service.ts`
|
||||
- `apps/api/src/activities/activities.service.ts`
|
||||
- `apps/api/src/reports/reports.service.ts`
|
||||
- `apps/api/src/ai/ai-ask.service.ts`
|
||||
- `apps/api/src/contacts/people.service.ts`
|
||||
- `apps/api/src/contacts/companies.service.ts`
|
||||
- Modify DTO mappers: `pipelines.service.ts` `toDealDto`, `activities.service.ts` `toDto`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `actor.priorCutoff`, new `dealPriorWhere(lens, cutoff)` signature (no `now`)
|
||||
- Produces: auth payload includes `cutoffAt`; DTOs include `promotedFromPriorAt`
|
||||
|
||||
- [ ] **Step 1: jwt.strategy** — set cutoff only when granted:
|
||||
|
||||
```ts
|
||||
priorLens: priorGranted ? priorLens : 'off',
|
||||
priorCutoff:
|
||||
priorGranted && membership.organization.priorArchiveEnabledAt
|
||||
? membership.organization.priorArchiveEnabledAt
|
||||
: null,
|
||||
```
|
||||
|
||||
- [ ] **Step 2: auth.service `priorArchiveFor`** — return:
|
||||
|
||||
```ts
|
||||
return {
|
||||
lens: membership.priorArchiveLens === 'archive' ? 'archive' : 'current',
|
||||
cutoffAt: orgCutoff ? orgCutoff.toISOString() : null,
|
||||
};
|
||||
```
|
||||
|
||||
Load org cutoff in the membership select used by `me` / login (same field already on org via jwt; ensure `me` path also has it).
|
||||
|
||||
- [ ] **Step 3: Fix every `dealPriorWhere(actor.priorLens, now)` → `dealPriorWhere(actor.priorLens, actor.priorCutoff)`** and `activityPriorWhere(lens, now, cutoff)` → `activityPriorWhere(lens, cutoff)`.
|
||||
|
||||
- [ ] **Step 4: Remove person/company prior filtering** from people/companies list and reports/ai-ask (either delete spreads of `personPriorWhere` / `companyPriorWhere` or leave no-ops from Task 2).
|
||||
|
||||
- [ ] **Step 5: Map `promotedFromPriorAt`** in `toDealDto` / activity `toDto`:
|
||||
|
||||
```ts
|
||||
promotedFromPriorAt: row.promotedFromPriorAt?.toISOString() ?? null,
|
||||
```
|
||||
|
||||
- [ ] **Step 6: billing.service** — when enabling `prior_data_archive`, do **not** write `priorArchiveEnabledAt`. Leave existing values untouched. Owner sets cutoff via Task 4.
|
||||
|
||||
- [ ] **Step 7: Typecheck**
|
||||
|
||||
```powershell
|
||||
npm run typecheck -w @mento/api
|
||||
```
|
||||
|
||||
Expected: pass (fix any remaining old signatures).
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Owner API — set org cutoff date
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/api/src/organizations/dto.ts`
|
||||
- Modify: `apps/api/src/organizations/organizations.service.ts` (`toDto`, new method)
|
||||
- Modify: `apps/api/src/organizations/organizations.controller.ts`
|
||||
- Modify: `packages/shared` if `UpdatePriorArchiveCutoffRequest` not already added in Task 1
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `PATCH /organizations/current/prior-archive-cutoff` body `{ cutoffAt: string | null }`
|
||||
- Roles: `@Roles('owner')` only (same as user prior-archive toggle)
|
||||
- `getCurrent` includes `priorArchiveCutoffAt`
|
||||
|
||||
- [ ] **Step 1: Service method**
|
||||
|
||||
```ts
|
||||
async updatePriorArchiveCutoff(
|
||||
organizationId: string,
|
||||
cutoffAt: string | null,
|
||||
): Promise<OrganizationDto> {
|
||||
const modules = /* load enabledModules */;
|
||||
if (!modules.includes('prior_data_archive')) {
|
||||
throw new BadRequestException('ماژول بایگانی اطلاعات قبل برای این محیط فعال نیست.');
|
||||
}
|
||||
let value: Date | null = null;
|
||||
if (cutoffAt != null && cutoffAt !== '') {
|
||||
const d = new Date(cutoffAt);
|
||||
if (Number.isNaN(d.getTime())) {
|
||||
throw new BadRequestException('تاریخ قطع معتبر نیست.');
|
||||
}
|
||||
value = d;
|
||||
}
|
||||
await this.prisma.organization.update({
|
||||
where: { id: organizationId },
|
||||
data: { priorArchiveEnabledAt: value },
|
||||
});
|
||||
return this.getCurrent(organizationId);
|
||||
}
|
||||
```
|
||||
|
||||
Include `priorArchiveCutoffAt: org.priorArchiveEnabledAt?.toISOString() ?? null` in `toDto`.
|
||||
|
||||
- [ ] **Step 2: Controller**
|
||||
|
||||
```ts
|
||||
@Patch('current/prior-archive-cutoff')
|
||||
@Roles('owner')
|
||||
updatePriorArchiveCutoff(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
@Body() body: { cutoffAt?: string | null },
|
||||
) {
|
||||
return this.organizationsService.updatePriorArchiveCutoff(
|
||||
user.organizationId,
|
||||
body.cutoffAt === undefined ? null : body.cutoffAt,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Treat missing vs null carefully: prefer explicit `{ cutoffAt: null }` to clear; require property present.
|
||||
|
||||
- [ ] **Step 3: Manual smoke** — owner PATCH with ISO start-of-day; GET current shows field.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Promote endpoints (deal + activity)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/api/src/pipelines/deals.service.ts`
|
||||
- Modify: `apps/api/src/pipelines/deals.controller.ts`
|
||||
- Modify: `apps/api/src/activities/activities.service.ts`
|
||||
- Modify: `apps/api/src/activities/activities.controller.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `POST /deals/:id/promote-from-prior` → `DealDto`
|
||||
- `POST /activities/:id/promote-from-prior` → `ActivityDto`
|
||||
- Rules: actor `priorLens` is `current` or `archive` (grant on); cutoff set; record `createdAt < cutoff`; `promotedFromPriorAt` is null; access where passes; set **only** `promotedFromPriorAt: new Date()`.
|
||||
|
||||
- [ ] **Step 1: deals.service.promoteFromPrior**
|
||||
|
||||
```ts
|
||||
async promoteFromPrior(actor: Actor, id: string): Promise<DealDto> {
|
||||
if (actor.priorLens === 'off' || !actor.priorCutoff) {
|
||||
throw new ForbiddenException('بایگانی اطلاعات قبل برای شما فعال نیست.');
|
||||
}
|
||||
const row = await this.prisma.deal.findFirst({
|
||||
where: { id, organizationId: actor.organizationId, ...dealAccessWhere(actor) },
|
||||
});
|
||||
if (!row) throw new NotFoundException('معامله پیدا نشد');
|
||||
if (row.promotedFromPriorAt) {
|
||||
throw new BadRequestException('این معامله قبلاً منتقل شده است.');
|
||||
}
|
||||
if (row.createdAt >= actor.priorCutoff) {
|
||||
throw new BadRequestException('این معامله در بایگانی نیست.');
|
||||
}
|
||||
const updated = await this.prisma.deal.update({
|
||||
where: { id },
|
||||
data: { promotedFromPriorAt: new Date() },
|
||||
include: /* same as get */,
|
||||
});
|
||||
return this.pipelines.toDealDto(updated);
|
||||
}
|
||||
```
|
||||
|
||||
Mirror for activities with `activityAccessWhere`.
|
||||
|
||||
- [ ] **Step 2: Controllers** — `POST :id/promote-from-prior` before generic `:id` routes if needed (Nest order: static segments already fine as long as path is under `:id/...`).
|
||||
|
||||
- [ ] **Step 3: Verify** with a prior deal: after promote, appears in current lens lists; `createdAt` unchanged.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Teams settings UI — cutoff date + user multi-select
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/components/TeamsSettingsPanel.vue`
|
||||
- Possibly refresh auth after cutoff change so `priorArchive.cutoffAt` updates for the owner session
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `GET /organizations/current` → `priorArchiveCutoffAt`; `PATCH .../prior-archive-cutoff`; existing `PATCH /users/:id/prior-archive`
|
||||
- Produces: compact card UI
|
||||
|
||||
- [ ] **Step 1: Header row** — next to `<h3>بایگانی اطلاعات قبل</h3>`:
|
||||
|
||||
```vue
|
||||
<header class="prior-head">
|
||||
<h3>بایگانی اطلاعات قبل</h3>
|
||||
<JalaliDateInput
|
||||
:model-value="cutoffLocal"
|
||||
mode="date"
|
||||
:disabled="cutoffSaving"
|
||||
@update:model-value="onCutoffChange"
|
||||
/>
|
||||
</header>
|
||||
```
|
||||
|
||||
Load cutoff on mount from `/organizations/current`. On change, convert selected Jalali day to ISO **start of that day** (reuse helpers already used with `JalaliDateInput` elsewhere — same as deal expected-close date storage). PATCH cutoff. If cleared, send `null`.
|
||||
|
||||
- [ ] **Step 2: Replace checkbox list** with searchable multi-select:
|
||||
|
||||
- Show a `<select>` or combobox of active users not yet enabled; on pick, call `setPriorArchive(id, true)`.
|
||||
- Show selected users as removable chips/rows; remove → `setPriorArchive(id, false)`.
|
||||
- Keep existing API; only UX changes.
|
||||
- Hint text stays: only selected users see the switch.
|
||||
|
||||
- [ ] **Step 3: Style** — keep card compact; date aligned with title (flex, gap); chips wrap.
|
||||
|
||||
- [ ] **Step 4: Manual check** — with many users, card height stays small; date saves; selected users see sidebar switch after refresh/`auth` reload.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Three-dot promote UI + PersonProfile client rules
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/views/ActivitiesView.vue` (both desktop + mobile `UiDropdownMenu` blocks)
|
||||
- Modify: `apps/web/src/views/PipelineBoardView.vue` (`menu-pop`)
|
||||
- Modify: `apps/web/src/views/PersonProfileView.vue` (`isPriorDeal` / `isPriorActivity`)
|
||||
|
||||
**Interfaces:**
|
||||
- Show «انتقال به کار جاری» only when:
|
||||
- `auth.user?.priorArchive?.lens === 'archive'`
|
||||
- item has no `promotedFromPriorAt`
|
||||
- `createdAt < cutoffAt` (if cutoff missing, hide)
|
||||
- POST promote endpoint; refresh list / close menu; toast/ok optional
|
||||
|
||||
- [ ] **Step 1: ActivitiesView** — inside each `UiDropdownMenu`, after ویرایش:
|
||||
|
||||
```vue
|
||||
<button
|
||||
v-if="canPromotePrior(item)"
|
||||
type="button"
|
||||
@click="promotePriorActivity(item.id)"
|
||||
>
|
||||
انتقال به کار جاری
|
||||
</button>
|
||||
```
|
||||
|
||||
```ts
|
||||
function canPromotePrior(item: ActivityDto) {
|
||||
const pa = auth.user?.priorArchive;
|
||||
if (!pa || pa.lens !== 'archive' || !pa.cutoffAt) return false;
|
||||
if (item.promotedFromPriorAt) return false;
|
||||
return new Date(item.createdAt).getTime() < new Date(pa.cutoffAt).getTime();
|
||||
}
|
||||
|
||||
async function promotePriorActivity(id: string) {
|
||||
menuId.value = null;
|
||||
await api.post(`/activities/${id}/promote-from-prior`);
|
||||
// reload list the same way delete/complete does
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: PipelineBoardView** — same gate using deal from board data; `api.post(`/deals/${id}/promote-from-prior`)`; reload board.
|
||||
|
||||
- [ ] **Step 3: PersonProfileView** — replace Didar-based helpers:
|
||||
|
||||
```ts
|
||||
function isPriorDeal(row: DealDto) {
|
||||
const cutoff = auth.user?.priorArchive?.cutoffAt;
|
||||
if (!auth.user?.priorArchive || !cutoff) return false;
|
||||
if (row.promotedFromPriorAt) return false;
|
||||
return new Date(row.createdAt).getTime() < new Date(cutoff).getTime();
|
||||
}
|
||||
// same for activities
|
||||
```
|
||||
|
||||
When prior archive is off for user, show all deals/activities in the normal sections (existing `priorArchiveOn` false path).
|
||||
|
||||
- [ ] **Step 4: Manual QA checklist**
|
||||
1. User without grant: full deals/activities, no switch
|
||||
2. Grant + no cutoff: full lists
|
||||
3. Set cutoff: current empty of pre-cutoff deals/activities (even open future due)
|
||||
4. Archive lens: only pre-cutoff non-promoted
|
||||
5. Promote from menu: appears in current; dates unchanged; no reverse action
|
||||
6. People/leads unchanged across lenses
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Final verification + graphify
|
||||
|
||||
- [ ] **Step 1: Tests**
|
||||
|
||||
```powershell
|
||||
cd apps/api; npm test
|
||||
```
|
||||
|
||||
Expected: all green including `prior-archive.test.ts`.
|
||||
|
||||
- [ ] **Step 2: Builds**
|
||||
|
||||
```powershell
|
||||
npm run build -w @mento/shared; npm run build -w @mento/api; npm run build -w @mento/web
|
||||
```
|
||||
|
||||
- [ ] **Step 3: graphify**
|
||||
|
||||
```powershell
|
||||
graphify update .
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Mark old design pointer** already done in `2026-10-14-...`; ensure new spec status stays accurate.
|
||||
|
||||
---
|
||||
|
||||
## Spec coverage (self-review)
|
||||
|
||||
| Spec requirement | Task |
|
||||
|---|---|
|
||||
| Org-wide Jalali cutoff | 1, 4, 6 |
|
||||
| Filter only deals/activities by createdAt | 2, 3 |
|
||||
| Promote tag only, no field mutation | 5 |
|
||||
| One-way, any granted viewer | 5, 7 |
|
||||
| No filter without grant | 3, jwt |
|
||||
| No filter without cutoff | 2, 3 |
|
||||
| Default lens current when granted | existing auth; unchanged |
|
||||
| Teams UI date + multi-select | 6 |
|
||||
| Three-dot promote in archive lens | 7 |
|
||||
| Drop Didar + future-due exception | 2, 7 |
|
||||
| Person/lead/rules unfiltered | 2, 3 |
|
||||
|
||||
No TBD placeholders. Signatures: `dealPriorWhere(lens, cutoff)` and `activityPriorWhere(lens, cutoff)` consistently; promote field name `promotedFromPriorAt` everywhere.
|
||||
Reference in New Issue
Block a user