Compare commits
349
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de77066cb0 | ||
|
|
d23dd20237 | ||
|
|
8a4bd70ad4 | ||
|
|
d87321c7bf | ||
|
|
1a05eee46d | ||
|
|
ec9a7cf9c9 | ||
|
|
4f08ffae36 | ||
|
|
d7cb92bf81 | ||
|
|
871d7bba1a | ||
|
|
f13a1249af | ||
|
|
fdf1fbe4aa | ||
|
|
6df198321a | ||
|
|
4c8604d346 | ||
|
|
25a24772fc | ||
|
|
932dc3f1c1 | ||
|
|
5b26b9156d | ||
|
|
d12d7b91cb | ||
|
|
49459707cb | ||
|
|
ba7f942059 | ||
|
|
96105468d4 | ||
|
|
7a8007fff0 | ||
|
|
da851bd9e5 | ||
|
|
dae3ae3f7e | ||
|
|
b03d97dd9e | ||
|
|
8b8d01976d | ||
|
|
4764562bd4 | ||
|
|
0e1bc4a1fe | ||
|
|
3c9b906bd6 | ||
|
|
ef58922e13 | ||
|
|
06036b00d3 | ||
|
|
d33d88e76d | ||
|
|
5ef693990c | ||
|
|
6ebd45f2cb | ||
|
|
33f33f9521 | ||
|
|
97f00d7e6d | ||
|
|
b8c49293f7 | ||
|
|
f36711174e | ||
|
|
abb1a3a18e | ||
|
|
21d2c69227 | ||
|
|
0ca9540524 | ||
|
|
dc691c5192 | ||
|
|
1ef7f91771 | ||
|
|
94f0383c79 | ||
|
|
c976c0bcd6 | ||
|
|
57a0b379f6 | ||
|
|
7f6d9a84d6 | ||
|
|
6abbf8e8fd | ||
|
|
39cb5c2548 | ||
|
|
004d1dff93 | ||
|
|
78a7d6a68a | ||
|
|
5b18749005 | ||
|
|
08a278e1c8 | ||
|
|
d71c2dbef4 | ||
|
|
1ccbefa7a3 | ||
|
|
14f4e9d7d8 | ||
|
|
52edccf74e | ||
|
|
bffe174162 | ||
|
|
35d74d33ed | ||
|
|
c337b0b4c7 | ||
|
|
c26353624e | ||
|
|
534dae45b2 | ||
|
|
79733e6c96 | ||
|
|
fdabdc54a3 | ||
|
|
1b10ee30df | ||
|
|
c273dd6c58 | ||
|
|
acb5d51f24 | ||
|
|
06977d27ed | ||
|
|
43437b0280 | ||
|
|
28938e8f77 | ||
|
|
e6e90aabf3 | ||
|
|
a0936bf19f | ||
|
|
18a7cd3b5a | ||
|
|
4af248b06a | ||
|
|
d5dd13863a | ||
|
|
81edf61065 | ||
|
|
8e1e94a439 | ||
|
|
590f8114cc | ||
|
|
8d01bce0c2 | ||
|
|
8b2777be4e | ||
|
|
d7b5085096 | ||
|
|
cbc2e4ee8a | ||
|
|
a17c7dbabc | ||
|
|
99d686fefb | ||
|
|
b9bb2ff9fa | ||
|
|
0d5e1034e5 | ||
|
|
6f42440b3b | ||
|
|
63a1803adc | ||
|
|
2cc2d358e4 | ||
|
|
463a1652a6 | ||
|
|
f15c5b9141 | ||
|
|
1d3588a69d | ||
|
|
9812e4e5ba | ||
|
|
8ae6a928f4 | ||
|
|
2ff35e55fc | ||
|
|
15f89aec04 | ||
|
|
bebd1be29b | ||
|
|
0daaef0b39 | ||
|
|
91637cb3a6 | ||
|
|
546a7a7e46 | ||
|
|
48815852fc | ||
|
|
f8128ebe3a | ||
|
|
6ec7a1a942 | ||
|
|
2d690b4c04 | ||
|
|
1ba352f127 | ||
|
|
f50246dc46 | ||
|
|
7d9fc8447f | ||
|
|
5be0bc6f5e | ||
|
|
2ca2650b69 | ||
|
|
42b3fc72cc | ||
|
|
6d8ede8e2a | ||
|
|
ade43fea0f | ||
|
|
65cf4e9a66 | ||
|
|
80fb5bf1bb | ||
|
|
31f45a6a31 | ||
|
|
73c87b8be4 | ||
|
|
09a60b3b5a | ||
|
|
cb5944e767 | ||
|
|
23ae273232 | ||
|
|
1c2da34983 | ||
|
|
50d7cad66e | ||
|
|
29d99cbded | ||
|
|
ce27df20ca | ||
|
|
7ae6a5837a | ||
|
|
631a3267d1 | ||
|
|
b731fe43aa | ||
|
|
15cbd62997 | ||
|
|
e6aa4c64f0 | ||
|
|
9127fa334b | ||
|
|
9a86f63f48 | ||
|
|
29398a445b | ||
|
|
bb8fd489de | ||
|
|
86e9ee6c2c | ||
|
|
adb63447ca | ||
|
|
c1b38e1737 | ||
|
|
be7ae0a1d8 | ||
|
|
bbbedc6059 | ||
|
|
f5d68d83fc | ||
|
|
f15ebb72ea | ||
|
|
8590579605 | ||
|
|
3863c5e7d1 | ||
|
|
e6f9876177 | ||
|
|
fddf22d7fe | ||
|
|
ada9a1f21f | ||
|
|
2f11105628 | ||
|
|
36eda8653e | ||
|
|
8fdd5147a5 | ||
|
|
002f697dfe | ||
|
|
96befd3867 | ||
|
|
b25b5b125a | ||
|
|
4abab55a6b | ||
|
|
8981a552fc | ||
|
|
ab538b8304 | ||
|
|
61d17c5cea | ||
|
|
23f8da2c65 | ||
|
|
9325d879a5 | ||
|
|
d62e004e90 | ||
|
|
32c722f42a | ||
|
|
f7b07f6b7a | ||
|
|
52c7306e78 | ||
|
|
960e01938d | ||
|
|
3fb5f32ac3 | ||
|
|
0ce2533066 | ||
|
|
bc68582fe2 | ||
|
|
e86841fc60 | ||
|
|
5069647f79 | ||
|
|
008733d338 | ||
|
|
c5708ed59b | ||
|
|
65d66bb331 | ||
|
|
79ba146118 | ||
|
|
02841a5b8c | ||
|
|
a04743c6df | ||
|
|
27635c0752 | ||
|
|
03e1c01023 | ||
|
|
177e5cc31b | ||
|
|
2c7c943802 | ||
|
|
9611803567 | ||
|
|
a494b5308e | ||
|
|
582733e2ea | ||
|
|
6bff9d2888 | ||
|
|
6bc5e1bec7 | ||
|
|
3a8a684aa4 | ||
|
|
831c682eb3 | ||
|
|
2cc81bb596 | ||
|
|
c04582072d | ||
|
|
42f0573a5f | ||
|
|
13ee70764b | ||
|
|
cf326d1ed9 | ||
|
|
5f4cc47c41 | ||
|
|
0e420a443f | ||
|
|
6b4bd0b03e | ||
|
|
910c7f51b0 | ||
|
|
609ce3d006 | ||
|
|
9bed6cac03 | ||
|
|
fd6e79c15d | ||
|
|
9b42fec0a8 | ||
|
|
d00f362376 | ||
|
|
fd8966e13f | ||
|
|
91eb9be1d4 | ||
|
|
ffae63edbf | ||
|
|
cba4ad6a49 | ||
|
|
faac26d07d | ||
|
|
a2b01f787e | ||
|
|
8af78047d9 | ||
|
|
b43ed68e82 | ||
|
|
f4398cdb3e | ||
|
|
17638786a8 | ||
|
|
b3b92abae9 | ||
|
|
b30f68563d | ||
|
|
51ca23b398 | ||
|
|
60d797f184 | ||
|
|
b87ebb22e0 | ||
|
|
792b3d29fa | ||
|
|
0802c2f436 | ||
|
|
37157620e2 | ||
|
|
f27b4c264a | ||
|
|
e82bd2b2df | ||
|
|
fa3fe0d364 | ||
|
|
4e26658092 | ||
|
|
287f88abc0 | ||
|
|
09f05d4ecc | ||
|
|
46a7693ea0 | ||
|
|
e6755d0c2e | ||
|
|
2f3646d085 | ||
|
|
f5b6a47f3c | ||
|
|
678cb87de7 | ||
|
|
d683932d1b | ||
|
|
13668c4ba4 | ||
|
|
aa2f39256a | ||
|
|
b4ffef9b6f | ||
|
|
b3784818fc | ||
|
|
3de2d0fa67 | ||
|
|
2a2728e5ee | ||
|
|
e08f1452ef | ||
|
|
43717392b2 | ||
|
|
bc01384551 | ||
|
|
5074c6d791 | ||
|
|
0d7555f219 | ||
|
|
f71f09c85d | ||
|
|
f88786ef61 | ||
|
|
46dcf296d3 | ||
|
|
c8753a4c4e | ||
|
|
48de552f36 | ||
|
|
71b3a7255a | ||
|
|
df8fac187e | ||
|
|
3296b1c209 | ||
|
|
23c83ffbee | ||
|
|
75ff2c36c4 | ||
|
|
39e2eb24a6 | ||
|
|
8b37525f2f | ||
|
|
e144b2277a | ||
|
|
236f73de93 | ||
|
|
60c16053a4 | ||
|
|
cbfe470f33 | ||
|
|
ac57b8ac33 | ||
|
|
d3fb92a5e2 | ||
|
|
07bdc4a3b6 | ||
|
|
c629729a9a | ||
|
|
beae16437b | ||
|
|
17628722d3 | ||
|
|
c322a24424 | ||
|
|
2e315c9cfd | ||
|
|
f8313c0ea5 | ||
|
|
3b128bd980 | ||
|
|
9f97371391 | ||
|
|
1309da2281 | ||
|
|
989c03b381 | ||
|
|
5784b09232 | ||
|
|
5d5b3a3213 | ||
|
|
ae21d9bbc7 | ||
|
|
e94600a02e | ||
|
|
75b83b7f1b | ||
|
|
d3986da28b | ||
|
|
c7a60c524d | ||
|
|
3c0ed3369e | ||
|
|
a3ed6187e9 | ||
|
|
e1d456ad11 | ||
|
|
b978708487 | ||
|
|
4c8731122b | ||
|
|
f71e78f3a7 | ||
|
|
7c4b24fad6 | ||
|
|
e6fb5c3700 | ||
|
|
298b3316b4 | ||
|
|
58e89246d9 | ||
|
|
5bb8e68521 | ||
|
|
88dc7c5a1c | ||
|
|
83504816c5 | ||
|
|
c20dc0a7ca | ||
|
|
e20b904bbe | ||
|
|
7996fc7f73 | ||
|
|
70bcd15847 | ||
|
|
b0f05b6a27 | ||
|
|
d4972891f1 | ||
|
|
032eb70524 | ||
|
|
2309e2c5ab | ||
|
|
66af7e577c | ||
|
|
125976f3eb | ||
|
|
0670067e7f | ||
|
|
a1655432fc | ||
|
|
589c763ef8 | ||
|
|
d2c82f2115 | ||
|
|
319493171f | ||
|
|
f20206b045 | ||
|
|
32e27d45f7 | ||
|
|
8effd4cbc6 | ||
|
|
7e73ba9936 | ||
|
|
647503810d | ||
|
|
ec5c7639e3 | ||
|
|
3833788aa8 | ||
|
|
9edbb92b13 | ||
|
|
05f205cadf | ||
|
|
ac3ffb8ab2 | ||
|
|
c079295ff4 | ||
|
|
22980e5152 | ||
|
|
c5c903d4b4 | ||
|
|
c06c92b333 | ||
|
|
00f8b91d78 | ||
|
|
55a287563d | ||
|
|
a16c2f0a9b | ||
|
|
6a9e864b07 | ||
|
|
d133c8ca34 | ||
|
|
db65b55660 | ||
|
|
26085b54a9 | ||
|
|
1b160c8cd3 | ||
|
|
499d13d1df | ||
|
|
ef600d149d | ||
|
|
ccddd009e3 | ||
|
|
6463275c3d | ||
|
|
35a618d0af | ||
|
|
011b316b5b | ||
|
|
b7e3d1e290 | ||
|
|
7dc4344e0e | ||
|
|
99df327ded | ||
|
|
230d7b3bf6 | ||
|
|
58c3e25db6 | ||
|
|
75a73559b5 | ||
|
|
41f0025883 | ||
|
|
42db5919e1 | ||
|
|
d53136d91c | ||
|
|
dac0454e69 | ||
|
|
4b335aa7ff | ||
|
|
17fb33b3e7 | ||
|
|
be97a067f9 | ||
|
|
9249302eac | ||
|
|
5d9a572ff8 | ||
|
|
f4cb41d96f | ||
|
|
7a58b9d469 | ||
|
|
1adc217375 | ||
|
|
d7d82102af | ||
|
|
f5a4eb577b |
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
description: Hard governance for Ant Design Vue v4 migration — isolated page-by-page, no manual CSS, 24-col grid, default controlHeight 36
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ant Design Vue v4 — Strict Governance (locked)
|
||||||
|
|
||||||
|
Source of truth for migrated UI: **Ant Design Vue 4** + central theme [`apps/web/src/theme/antdTheme.ts`](apps/web/src/theme/antdTheme.ts).
|
||||||
|
|
||||||
|
Legacy CRM UI (`components/ui/*`, `.crm-*`, `.btn` / `.field` in `styles/`) remains for **unmigrated** screens only. Never mix systems inside one page.
|
||||||
|
|
||||||
|
## Hard rules
|
||||||
|
|
||||||
|
1. **Single-page migration lock**
|
||||||
|
Migrate **exactly one** route/view (or one clearly named page folder) per change set. Do not “clean up” neighboring pages, shared legacy primitives, or global CSS while migrating a page.
|
||||||
|
|
||||||
|
2. **No edits to legacy kit during Ant work**
|
||||||
|
Do **not** modify `apps/web/src/components/ui/**` or broaden changes across `views/` beyond the single page under migration, unless the user explicitly names that page.
|
||||||
|
|
||||||
|
2b. **Child components of a migrated View (mandatory)**
|
||||||
|
هنگام بازنویسی یک صفحه (View)، ایجنت موظف است علاوه بر خود فایل View، تمام کامپوننتهای فرزندی را که در آن View ایمپورت شدهاند و حاوی اینپوت/سلکت/دکمه هستند نیز بازبینی کرده و درون آنها را به کامپوننتهای رسمی `ant-design-vue` تبدیل کند. هیچ تگ `<input>`, `<select>`, `<textarea>`, `<button>` خامی نباید در صفحه یا کامپوننتهای فرزندِ مصرفی آن باقی بماند.
|
||||||
|
When rewriting a View, the agent **must** also review every component imported by that View that contains inputs/selects/buttons, and convert their internals to official `ant-design-vue` controls.
|
||||||
|
**No raw** `<input>`, `<select>`, `<textarea>`, or `<button>` may remain in the View **or** in those consumed child components. Prefer native Ant equivalents (`a-select` / `mode="multiple|tags"` / `a-time-picker` / …) over custom pickers when logic allows; if domain logic is required (e.g. Jalali), convert that child file to Ant chrome (`a-input` + `a-popover`, etc.) instead of leaving raw HTML. Do **not** leave legacy `UiInput` / `UiSelect` / `UiButton` / `UiField` / `UiTextarea` in the View or those children.
|
||||||
|
|
||||||
|
3. **Ant-only UI on migrated pages**
|
||||||
|
On a migrated page, build controls with Ant Design Vue (`a-button`, `a-form`, `a-table`, `a-card`, …) and `@ant-design/icons-vue`. Do **not** introduce new usage of `UiButton`, `UiInput`, `.btn`, `.field`, or ad-hoc HTML form controls for the same job.
|
||||||
|
|
||||||
|
4. **Ban manual CSS for layout/look**
|
||||||
|
On migrated pages:
|
||||||
|
- No new `<style scoped>` rules for color, spacing, radius, shadow, typography, or layout that Ant already covers.
|
||||||
|
- No hardcoded hex / `rgb()` for chrome UI (charts/data palettes may be excepted when not chrome).
|
||||||
|
- Prefer ConfigProvider theme tokens, component props, and Ant space/grid utilities.
|
||||||
|
- Inline `style` only for rare one-off values that tokens cannot express; prefer tokens first.
|
||||||
|
|
||||||
|
5. **Default Ant control size (`controlHeight: 36`) — no manual `size`**
|
||||||
|
- Global default comes from theme token `controlHeight: 36` in `antdTheme.ts` (Ant derives SM/LG from this).
|
||||||
|
- Do **not** set `size="small"`, `size="middle"`, `size="large"`, or `:size` on `a-button`, `a-input`, `a-select`, `a-input-number`, `a-date-picker`, `a-radio-group`, `a-pagination`, or similar controls on migrated pages.
|
||||||
|
- Do **not** override `controlHeight` / `controlHeightSM` / `controlHeightLG` inside `components.*` of the theme — inherit the token default.
|
||||||
|
- Exception: only if the user explicitly requests a one-off non-default size for a named control.
|
||||||
|
|
||||||
|
6. **24-column grid only**
|
||||||
|
Page layout on migrated screens uses Ant Grid: `a-row` / `a-col` with the **24-column** system (`:span`, `:xs`, `:sm`, `:md`, `:lg`, `:xl`). Do not invent parallel flex/grid CSS layouts for page chrome.
|
||||||
|
|
||||||
|
7. **RTL + locale**
|
||||||
|
App root is wrapped with `<a-config-provider direction="rtl" :locale="faIR" :theme="antdTheme">`. Do not override direction to LTR on whole pages; use local `direction="ltr"` only for inherently LTR fragments (e.g. phone numbers, codes).
|
||||||
|
|
||||||
|
8. **Auto-import**
|
||||||
|
`<a-*>` components are resolved via `unplugin-vue-components` + `AntDesignVueResolver({ importStyle: false })`. Do not add blanket `app.use(antd)` unless there is a documented exception. Do not per-component CSS imports (`importStyle` stays `false`; CSS-in-JS + theme).
|
||||||
|
|
||||||
|
9. **Theme is central**
|
||||||
|
Visual tokens (primary, radius, `controlHeight`, padding/margin scale, shadows, Form/Card overrides) live only in `antdTheme.ts`. Do not fork theme objects inside views.
|
||||||
|
|
||||||
|
10. **Icons**
|
||||||
|
Use `@ant-design/icons-vue` on migrated pages. Do not expand `UiIcon` for Ant pages.
|
||||||
|
|
||||||
|
11. **Isolation guarantee**
|
||||||
|
Infrastructure PRs (deps, vite plugin, theme, App `ConfigProvider`) must not change behavior of unmigrated pages. No `ant-design-vue/dist/reset.css` (or similar global reset) unless explicitly approved — it can break legacy CSS.
|
||||||
|
|
||||||
|
## Allowed on migrated pages
|
||||||
|
|
||||||
|
- Ant Design Vue 4 components and their props/slots (except banned manual `size` on controls)
|
||||||
|
- `a-row` / `a-col` (24 columns), `a-space`, `a-flex` where appropriate
|
||||||
|
- Theme tokens via ConfigProvider
|
||||||
|
- Minimal page-local script logic (data fetching, forms) unchanged in spirit
|
||||||
|
|
||||||
|
## Forbidden on migrated pages
|
||||||
|
|
||||||
|
- New legacy `components/ui` usage
|
||||||
|
- Parallel button/input CSS classes (`.btn`, `.field`, `.crm-*` for chrome that Ant replaces)
|
||||||
|
- Custom CSS that re-skins Ant components
|
||||||
|
- Manual `size` props on form/action controls (use theme default 36px)
|
||||||
|
- Multi-page refactors “while we’re here”
|
||||||
|
- Adding Tailwind/Bootstrap/other UI kits
|
||||||
|
|
||||||
|
## Agent checklist before finishing a migration PR
|
||||||
|
|
||||||
|
- [ ] Only one page/route touched for UI migration
|
||||||
|
- [ ] Imported child components with controls also converted to Ant (no raw HTML left)
|
||||||
|
- [ ] Grep verification: `<input|<select|<textarea|<button|UiInput|UiSelect|UiField|UiTextarea|UiButton` count is **0** in the View + those children
|
||||||
|
- [ ] No changes under `components/ui/`
|
||||||
|
- [ ] No new scoped CSS for look-and-feel
|
||||||
|
- [ ] No manual `size` on Ant controls
|
||||||
|
- [ ] Layout uses 24-col Ant grid
|
||||||
|
- [ ] Uses `antdTheme` / ConfigProvider / StyleProvider `hash-priority="high"` — no local theme fork
|
||||||
|
- [ ] `npm run build -w @mento/web` (or `vue-tsc`) passes
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
name: antd-system-architect
|
||||||
|
description: >-
|
||||||
|
Architect and migrate Montact web UI to Ant Design Vue v4 under strict
|
||||||
|
governance: single-page lock, no manual CSS, no manual size props, default
|
||||||
|
controlHeight 36, 24-column grid, central antdTheme, isolated from legacy
|
||||||
|
components/ui. Use when migrating a page to Ant Design, adding Ant components,
|
||||||
|
editing antdTheme, or reviewing Ant PRs.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ant Design Vue System Architect (Montact)
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
- Migrating any `apps/web` view/page to Ant Design Vue 4
|
||||||
|
- Extending [`apps/web/src/theme/antdTheme.ts`](../../../apps/web/src/theme/antdTheme.ts)
|
||||||
|
- Touching Ant ConfigProvider / vite Ant auto-import
|
||||||
|
- Reviewing whether a change violates isolation from legacy UI
|
||||||
|
|
||||||
|
Also read and obey [`.cursor/rules/antd-strict-governance.mdc`](../../rules/antd-strict-governance.mdc).
|
||||||
|
|
||||||
|
## Non-negotiables
|
||||||
|
|
||||||
|
1. **One page per migration** — never batch unrelated views.
|
||||||
|
2. **Do not edit** `apps/web/src/components/ui/**` during Ant migration work.
|
||||||
|
3. **Child components of the View are in scope (mandatory)** — هنگام بازنویسی یک صفحه (View)، ایجنت موظف است علاوه بر خود فایل View، تمام کامپوننتهای فرزندی را که در آن View ایمپورت شدهاند و حاوی اینپوت/سلکت/دکمه هستند نیز بازبینی کرده و درون آنها را به کامپوننتهای رسمی `ant-design-vue` تبدیل کند. هیچ تگ `<input>`, `<select>`, `<textarea>`, `<button>` خامی نباید در صفحه یا کامپوننتهای فرزندِ مصرفی آن باقی بماند. When rewriting a View, also open every imported child that renders inputs/selects/buttons and convert them to official `ant-design-vue` controls. No raw `<input>`, `<select>`, `<textarea>`, or `<button>` (and no `UiInput` / `UiSelect` / `UiButton` / `UiField` / `UiTextarea`) may remain in the View or those children. Prefer Ant natives (`a-select` multiple/tags, `a-time-picker`, …); for Jalali/domain pickers, convert the child file to Ant chrome (`a-input` + `a-popover`, etc.).
|
||||||
|
4. **No manual look CSS** on migrated pages (no new scoped colors/spacing/radius/shadows for chrome).
|
||||||
|
5. **24-column Ant grid** (`a-row` / `a-col`) for page layout.
|
||||||
|
6. **Theme only in** `apps/web/src/theme/antdTheme.ts`.
|
||||||
|
7. **Default control height is 36** via token `controlHeight: 36`. Do **not** pass `size` / `:size` on buttons, inputs, selects, or similar controls; do **not** set per-component `controlHeight*` in the theme `components` map.
|
||||||
|
8. **Auto-import** `<a-*>` via `AntDesignVueResolver({ importStyle: false })` — no per-file style imports. Root uses `StyleProvider` with `hash-priority="high"` so legacy `main.css` cannot override Ant.
|
||||||
|
9. **No global Ant reset CSS** that would restyle legacy pages.
|
||||||
|
|
||||||
|
## Stack facts (this repo)
|
||||||
|
|
||||||
|
| Piece | Location |
|
||||||
|
|---|---|
|
||||||
|
| Dependency | `ant-design-vue` ^4, `@ant-design/icons-vue`, `unplugin-vue-components` in `@mento/web` |
|
||||||
|
| Vite plugin | `apps/web/vite.config.ts` — `Components` + `AntDesignVueResolver` |
|
||||||
|
| Theme | `apps/web/src/theme/antdTheme.ts` (`controlHeight: 36`) |
|
||||||
|
| RTL + locale + theme | `apps/web/src/App.vue` — `<a-config-provider direction="rtl" :locale="faIR" :theme="antdTheme">` |
|
||||||
|
| Legacy kit (do not touch) | `apps/web/src/components/ui/*`, `styles/main.css` utilities |
|
||||||
|
|
||||||
|
## Migration workflow
|
||||||
|
|
||||||
|
1. Confirm the **single** route/view the user named.
|
||||||
|
2. Inventory current UI: legacy `Ui*`, `.btn`/`.field`, `.crm-*`, custom tables.
|
||||||
|
3. List all imported children that contain controls; convert them in the same pass (except `components/ui/**`).
|
||||||
|
4. Replace with Ant equivalents:
|
||||||
|
- Button → `a-button` (**no** `size` prop)
|
||||||
|
- Input/Select/Form field → `a-form` + `a-form-item` + `a-input` / `a-select` / … (**no** `size`)
|
||||||
|
- Modal/Drawer → `a-modal` / `a-drawer`
|
||||||
|
- Table → `a-table` (columns via Ant API)
|
||||||
|
- Page chrome → `a-card`, title + `a-space`, layout via `a-row`/`a-col`
|
||||||
|
- Icons → `@ant-design/icons-vue`
|
||||||
|
5. Remove page-local CSS that only existed to skin the old controls.
|
||||||
|
6. Keep business logic, API calls, and router behavior intact.
|
||||||
|
7. Grep-verify zero hits for `<input|<select|<textarea|<button|UiInput|UiSelect|UiField|UiTextarea|UiButton` in the View + converted children.
|
||||||
|
8. Run `npm run build -w @mento/web` (or at least `vue-tsc -b` in `apps/web`).
|
||||||
|
9. Smoke-check that **other** routes still render with legacy UI unchanged.
|
||||||
|
|
||||||
|
## Theme extension rules
|
||||||
|
|
||||||
|
- Change tokens in `antdTheme.ts` only when the user asks or when a migrated page proves a missing token.
|
||||||
|
- Keep `controlHeight: 36` as the single source for control sizing; let Ant derive SM/LG.
|
||||||
|
- Prefer `token` and limited `components.Card` / `components.Form` overrides — never per-control `controlHeight` forks.
|
||||||
|
- Do not duplicate `ThemeConfig` objects inside views.
|
||||||
|
|
||||||
|
## Anti-patterns (reject)
|
||||||
|
|
||||||
|
- Importing both `UiButton` and `a-button` on the same migrated page
|
||||||
|
- Adding `ant-design-vue/dist/reset.css` “to fix spacing”
|
||||||
|
- Editing `components/ui` to “look more like Ant”
|
||||||
|
- `size="small|middle|large"` on Ant controls
|
||||||
|
- Multi-page visual refactors in one PR
|
||||||
|
- Custom CSS overriding `.ant-btn` / `.ant-input` selectors
|
||||||
|
|
||||||
|
## Done criteria
|
||||||
|
|
||||||
|
- Migrated page uses Ant components + 24-col grid
|
||||||
|
- Imported control-bearing children also Ant-pure (grep = 0)
|
||||||
|
- Controls use theme default height (no manual `size`)
|
||||||
|
- No new manual chrome CSS
|
||||||
|
- Legacy kit untouched
|
||||||
|
- Build/typecheck clean
|
||||||
|
- Unmigrated pages untouched in the diff
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.git
|
||||||
|
node_modules
|
||||||
|
**/dist
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
apps/stt/.venv
|
||||||
|
uploads
|
||||||
|
docs
|
||||||
|
e2e
|
||||||
|
test-results
|
||||||
|
playwright-report
|
||||||
|
graphify-out
|
||||||
|
.worktrees
|
||||||
|
*.md
|
||||||
|
crm-node_modules.tgz
|
||||||
+1
-1
@@ -17,7 +17,7 @@ SMS_PASSWORD=
|
|||||||
SMS_SENDER=
|
SMS_SENDER=
|
||||||
# SMS_BASE_URL=http://smspanel.Trez.ir
|
# SMS_BASE_URL=http://smspanel.Trez.ir
|
||||||
# SMS_SEND_PATH=/SendMessageWithCode.ashx
|
# SMS_SEND_PATH=/SendMessageWithCode.ashx
|
||||||
# SMS_OTP_TEMPLATE=کد ورود پرتال منتکت: {code}
|
# SMS_OTP_TEMPLATE=کد ورود منتکت: {code}
|
||||||
# SMS_TIMEOUT_MS=20000
|
# SMS_TIMEOUT_MS=20000
|
||||||
|
|
||||||
# رمزنگاری توکنهای اتصال حسابداری (تهحساب) — ۳۲ بایت بهصورت hex (۶۴ کاراکتر)
|
# رمزنگاری توکنهای اتصال حسابداری (تهحساب) — ۳۲ بایت بهصورت hex (۶۴ کاراکتر)
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
name: Build & Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Start time
|
||||||
|
run: echo "BUILD_STARTED=$(date +%s)" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Free disk space
|
||||||
|
id: prune
|
||||||
|
run: |
|
||||||
|
df -h / /workspace /var/lib/containerd 2>/dev/null || df -h /
|
||||||
|
docker builder prune -af || true
|
||||||
|
docker image prune -af || true
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
df -h / /workspace /var/lib/containerd 2>/dev/null || df -h /
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
id: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Resolve image tag
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
else
|
||||||
|
TAG="$(git rev-parse --short HEAD)"
|
||||||
|
fi
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "registry=dock.monsys.ir" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build Docker images
|
||||||
|
id: build
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
tag="${{ steps.meta.outputs.tag }}"
|
||||||
|
reg="${{ steps.meta.outputs.registry }}"
|
||||||
|
docker build -f Dockerfile.api -t "$reg/montact-api:$tag" -t "$reg/montact-api:latest" .
|
||||||
|
docker build -f Dockerfile.web -t "$reg/montact-web:$tag" -t "$reg/montact-web:latest" .
|
||||||
|
docker build -f Dockerfile.stt -t "$reg/montact-stt:$tag" -t "$reg/montact-stt:latest" .
|
||||||
|
|
||||||
|
# Registry login is unsolved. These two steps stay off so a tag run
|
||||||
|
# only proves the three images build. Remove `if: false` on both when
|
||||||
|
# dock.monsys.ir accepts the runner.
|
||||||
|
- name: Login to registry
|
||||||
|
id: login
|
||||||
|
env:
|
||||||
|
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
USER_NAME="${REGISTRY_USERNAME:-admin}"
|
||||||
|
printf '%s\n' "$REGISTRY_PASSWORD" | docker login "dock.monsys.ir" -u "$USER_NAME" --password-stdin
|
||||||
|
|
||||||
|
- name: Push Docker images
|
||||||
|
id: push
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
tag="${{ steps.meta.outputs.tag }}"
|
||||||
|
reg="${{ steps.meta.outputs.registry }}"
|
||||||
|
for name in montact-api montact-web montact-stt; do
|
||||||
|
docker push "$reg/$name:$tag"
|
||||||
|
docker push "$reg/$name:latest"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Notify Bale
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
BALE_TOKEN: ${{ secrets.BALE_BOT_TOKEN || vars.BALE_BOT_TOKEN }}
|
||||||
|
BALE_CHAT_ID: ${{ vars.BALE_CHAT_ID }}
|
||||||
|
TAG: ${{ steps.meta.outputs.tag }}
|
||||||
|
REG: ${{ steps.meta.outputs.registry }}
|
||||||
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }}
|
||||||
|
STEP_OUTCOMES: |
|
||||||
|
Checkout code=${{ steps.checkout.outcome }}
|
||||||
|
Resolve image tag=${{ steps.meta.outcome }}
|
||||||
|
Build Docker images=${{ steps.build.outcome }}
|
||||||
|
Login to registry=${{ steps.login.outcome }}
|
||||||
|
Push Docker images=${{ steps.push.outcome }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$BALE_TOKEN" ] || [ -z "$BALE_CHAT_ID" ]; then
|
||||||
|
echo "BALE_BOT_TOKEN / BALE_CHAT_ID not set in Gitea — skipping notification"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
failed_step=$(printf '%s\n' "$STEP_OUTCOMES" | grep -E '=(failure|cancelled)$' | head -n1 | cut -d= -f1)
|
||||||
|
if [ -n "$failed_step" ]; then
|
||||||
|
title="❌ بیلد ناموفق"
|
||||||
|
elif printf '%s\n' "$STEP_OUTCOMES" | grep -q '^Push Docker images=success$'; then
|
||||||
|
title="✅ بیلد موفق"
|
||||||
|
elif printf '%s\n' "$STEP_OUTCOMES" | grep -q '^Build Docker images=success$'; then
|
||||||
|
title="✅ بیلد موفق؛ پوش رجیستری هنوز خاموش است"
|
||||||
|
else
|
||||||
|
title="⚠️ بیلد کامل نشد"
|
||||||
|
fi
|
||||||
|
|
||||||
|
commit=$(git log -1 --pretty='%h — %s' 2>/dev/null || echo "${GITHUB_SHA:0:7}")
|
||||||
|
elapsed=$(( $(date +%s) - ${BUILD_STARTED:-$(date +%s)} ))
|
||||||
|
|
||||||
|
text="${title}
|
||||||
|
پروژه: ${GITHUB_REPOSITORY}
|
||||||
|
تگ: ${TAG:-${GITHUB_REF_NAME}}"
|
||||||
|
if [ -z "$failed_step" ] && [ -n "$TAG" ]; then
|
||||||
|
text="${text}
|
||||||
|
image: ${REG}/montact-api:${TAG}
|
||||||
|
image: ${REG}/montact-web:${TAG}
|
||||||
|
image: ${REG}/montact-stt:${TAG}"
|
||||||
|
fi
|
||||||
|
[ -n "$failed_step" ] && text="${text}
|
||||||
|
مرحلهٔ خطا: ${failed_step}"
|
||||||
|
text="${text}
|
||||||
|
کامیت: ${commit}
|
||||||
|
توسط: ${GITHUB_ACTOR}
|
||||||
|
مدت: $((elapsed / 60))m$((elapsed % 60))s
|
||||||
|
لاگ: ${RUN_URL}"
|
||||||
|
msg_file=$(mktemp)
|
||||||
|
printf '%s\n' "$text" | sed 's/^[[:space:]]*//' > "$msg_file"
|
||||||
|
|
||||||
|
curl -sS -m 20 -X POST "https://tapi.bale.ai/bot${BALE_TOKEN}/sendMessage" \
|
||||||
|
--data-urlencode "chat_id=${BALE_CHAT_ID}" \
|
||||||
|
--data-urlencode "text@${msg_file}"
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# فعالسازی: npm install (اسکریپت prepare) یا git config core.hooksPath .githooks
|
||||||
|
npm run check
|
||||||
+15
@@ -21,3 +21,18 @@ docs/~$*
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
apps/stt/.venv/
|
apps/stt/.venv/
|
||||||
crm-node_modules.tgz
|
crm-node_modules.tgz
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
e2e/.auth/
|
||||||
|
e2e/.env
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
|
||||||
|
# Graphify (local architecture graph — do not commit)
|
||||||
|
graphify-out/
|
||||||
|
**/graphify-out/
|
||||||
|
.cursor/rules/graphify.mdc
|
||||||
|
|
||||||
|
# Local git worktrees + SDD scratch
|
||||||
|
.worktrees/
|
||||||
|
.superpowers/
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# NestJS API + Prisma. Build context is the monorepo root.
|
||||||
|
FROM node:20-bookworm-slim AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY apps/api/package.json apps/api/package.json
|
||||||
|
COPY apps/web/package.json apps/web/package.json
|
||||||
|
COPY packages/shared/package.json packages/shared/package.json
|
||||||
|
RUN npm ci
|
||||||
|
COPY apps/api apps/api
|
||||||
|
COPY packages/shared packages/shared
|
||||||
|
RUN npm run build -w @mento/shared \
|
||||||
|
&& npm run prisma:generate -w @mento/api \
|
||||||
|
&& npm run build -w @mento/api
|
||||||
|
|
||||||
|
FROM node:20-bookworm-slim
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends openssl ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=build /app/package.json /app/package-lock.json ./
|
||||||
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
|
COPY --from=build /app/packages/shared ./packages/shared
|
||||||
|
COPY --from=build /app/apps/api/package.json ./apps/api/package.json
|
||||||
|
COPY --from=build /app/apps/api/dist ./apps/api/dist
|
||||||
|
COPY --from=build /app/apps/api/prisma ./apps/api/prisma
|
||||||
|
WORKDIR /app/apps/api
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "dist/src/main.js"]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Local Faster-Whisper. Not published on Ingress.
|
||||||
|
FROM python:3.11-slim-bookworm
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ffmpeg \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /app
|
||||||
|
COPY apps/stt/requirements.txt ./requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY apps/stt/main.py ./main.py
|
||||||
|
ENV STT_MODEL=small \
|
||||||
|
STT_DEVICE=cpu \
|
||||||
|
STT_COMPUTE_TYPE=int8 \
|
||||||
|
HF_HOME=/models
|
||||||
|
EXPOSE 3040
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3040"]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Vue panel. The built app calls /api on the same host; Traefik sends that to the API.
|
||||||
|
FROM node:20-bookworm-slim AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY apps/api/package.json apps/api/package.json
|
||||||
|
COPY apps/web/package.json apps/web/package.json
|
||||||
|
COPY packages/shared/package.json packages/shared/package.json
|
||||||
|
RUN npm ci
|
||||||
|
COPY apps/web apps/web
|
||||||
|
COPY packages/shared packages/shared
|
||||||
|
RUN npm run build -w @mento/shared && npm run build -w @mento/web
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
COPY docker/web.nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/apps/web/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
@@ -80,7 +80,7 @@ $env:MENTO_DEV_API="http://127.0.0.1:3000"; npm run dev
|
|||||||
|
|
||||||
## فاز فعلی
|
## فاز فعلی
|
||||||
|
|
||||||
فاز ۱ هسته فروش/پشتیبانی و Smart Notes عملاً کامل است. جزئیات تیکخورده و باقیمانده:
|
فاز ۱ هسته فروش/پشتیبانی، Smart Notes، مالی سبک و اتصالها (API، ووکامرس، VoIP، پیامک گروهی) عملاً کامل است. جزئیات تیکخورده و باقیمانده:
|
||||||
|
|
||||||
- [`docs/وضعیت-پیشرفت.md`](docs/وضعیت-پیشرفت.md) ← **چکلیست زنده**
|
- [`docs/وضعیت-پیشرفت.md`](docs/وضعیت-پیشرفت.md) ← **چکلیست زنده**
|
||||||
- [`docs/قابلیتهای-منتکت.md`](docs/قابلیتهای-منتکت.md) ← فهرست کوتاه قابلیتها
|
- [`docs/قابلیتهای-منتکت.md`](docs/قابلیتهای-منتکت.md) ← فهرست کوتاه قابلیتها
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate": "prisma migrate dev",
|
"prisma:migrate": "prisma migrate dev",
|
||||||
"prisma:seed": "tsx prisma/seed.ts",
|
"prisma:seed": "tsx prisma/seed.ts",
|
||||||
"lint": "echo \"api lint placeholder\""
|
"lint": "echo \"api lint placeholder\"",
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p test/tsconfig.json",
|
||||||
|
"test": "tsx --test test/tahesab-methods.test.ts test/dates.test.ts test/birthdays.test.ts test/message-automation.test.ts test/record-scope.test.ts test/gamification-engine.test.ts test/automation-branches.test.ts test/pipeline-merge.test.ts test/ask-count.test.ts test/prior-archive.test.ts test/organizations.test.ts test/promote-from-prior.test.ts test/behavior.test.ts test/attendance-time.test.ts test/sales-factors.test.ts test/platform-admin-alert.test.ts test/person-name.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mento/shared": "*",
|
"@mento/shared": "*",
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ProductAccountingLink" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"productId" TEXT NOT NULL,
|
||||||
|
"connectionId" TEXT NOT NULL,
|
||||||
|
"externalName" TEXT NOT NULL,
|
||||||
|
"ayar" INTEGER,
|
||||||
|
"vaznSabet" DOUBLE PRECISION,
|
||||||
|
"matchMethod" TEXT NOT NULL DEFAULT 'import',
|
||||||
|
"linkedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ProductAccountingLink_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ProductAccountingLink_productId_idx" ON "ProductAccountingLink"("productId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ProductAccountingLink_connectionId_idx" ON "ProductAccountingLink"("connectionId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ProductAccountingLink_productId_connectionId_key" ON "ProductAccountingLink"("productId", "connectionId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ProductAccountingLink_connectionId_externalName_key" ON "ProductAccountingLink"("connectionId", "externalName");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ProductAccountingLink" ADD CONSTRAINT "ProductAccountingLink_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ProductAccountingLink" ADD CONSTRAINT "ProductAccountingLink_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "AccountingConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Person" ADD COLUMN "website" TEXT;
|
||||||
|
ALTER TABLE "Person" ADD COLUMN "referrer" TEXT;
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Activity" ADD COLUMN IF NOT EXISTS "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ProjectCard" ADD COLUMN IF NOT EXISTS "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE IF NOT EXISTS "AuditLog" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"actorUserId" TEXT,
|
||||||
|
"action" TEXT NOT NULL,
|
||||||
|
"method" TEXT NOT NULL,
|
||||||
|
"path" TEXT NOT NULL,
|
||||||
|
"entityType" TEXT,
|
||||||
|
"entityId" TEXT,
|
||||||
|
"summary" TEXT NOT NULL,
|
||||||
|
"meta" JSONB,
|
||||||
|
"ip" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE IF NOT EXISTS "SavedView" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"ownerUserId" TEXT,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"scope" TEXT NOT NULL,
|
||||||
|
"filters" JSONB NOT NULL DEFAULT '{}',
|
||||||
|
"isShared" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "SavedView_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable (implicit many-to-many ProjectCard <-> User assignees)
|
||||||
|
CREATE TABLE IF NOT EXISTS "_ProjectCardAssignees" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX IF NOT EXISTS "AuditLog_organizationId_createdAt_idx" ON "AuditLog"("organizationId", "createdAt");
|
||||||
|
CREATE INDEX IF NOT EXISTS "AuditLog_organizationId_actorUserId_createdAt_idx" ON "AuditLog"("organizationId", "actorUserId", "createdAt");
|
||||||
|
CREATE INDEX IF NOT EXISTS "AuditLog_organizationId_entityType_createdAt_idx" ON "AuditLog"("organizationId", "entityType", "createdAt");
|
||||||
|
CREATE INDEX IF NOT EXISTS "SavedView_organizationId_scope_idx" ON "SavedView"("organizationId", "scope");
|
||||||
|
CREATE INDEX IF NOT EXISTS "SavedView_organizationId_ownerUserId_idx" ON "SavedView"("organizationId", "ownerUserId");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "_ProjectCardAssignees_AB_unique" ON "_ProjectCardAssignees"("A", "B");
|
||||||
|
CREATE INDEX IF NOT EXISTS "_ProjectCardAssignees_B_index" ON "_ProjectCardAssignees"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_actorUserId_fkey" FOREIGN KEY ("actorUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SavedView" ADD CONSTRAINT "SavedView_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SavedView" ADD CONSTRAINT "SavedView_ownerUserId_fkey" FOREIGN KEY ("ownerUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "_ProjectCardAssignees" ADD CONSTRAINT "_ProjectCardAssignees_A_fkey" FOREIGN KEY ("A") REFERENCES "ProjectCard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "_ProjectCardAssignees" ADD CONSTRAINT "_ProjectCardAssignees_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "OrganizationApiKey" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"prefix" TEXT NOT NULL,
|
||||||
|
"keyHash" TEXT NOT NULL,
|
||||||
|
"lastUsedAt" TIMESTAMP(3),
|
||||||
|
"revokedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "OrganizationApiKey_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "WebhookEndpoint" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"url" TEXT NOT NULL,
|
||||||
|
"secret" TEXT NOT NULL,
|
||||||
|
"events" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "WebhookEndpoint_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "WebhookDelivery" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"endpointId" TEXT NOT NULL,
|
||||||
|
"event" TEXT NOT NULL,
|
||||||
|
"payload" JSONB NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
"statusCode" INTEGER,
|
||||||
|
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"lastError" TEXT,
|
||||||
|
"nextAttemptAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deliveredAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "WebhookDelivery_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "SmsCampaign" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"body" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'draft',
|
||||||
|
"scheduledAt" TIMESTAMP(3),
|
||||||
|
"totalCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"sentCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"failedCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"skippedCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdById" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "SmsCampaign_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "SmsCampaignRecipient" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"campaignId" TEXT NOT NULL,
|
||||||
|
"personId" TEXT,
|
||||||
|
"mobile" TEXT,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
"error" TEXT,
|
||||||
|
"sentAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "SmsCampaignRecipient_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "OrganizationApiKey_keyHash_key" ON "OrganizationApiKey"("keyHash");
|
||||||
|
CREATE INDEX "OrganizationApiKey_organizationId_revokedAt_idx" ON "OrganizationApiKey"("organizationId", "revokedAt");
|
||||||
|
CREATE INDEX "OrganizationApiKey_prefix_idx" ON "OrganizationApiKey"("prefix");
|
||||||
|
CREATE INDEX "WebhookEndpoint_organizationId_isActive_idx" ON "WebhookEndpoint"("organizationId", "isActive");
|
||||||
|
CREATE INDEX "WebhookDelivery_organizationId_createdAt_idx" ON "WebhookDelivery"("organizationId", "createdAt");
|
||||||
|
CREATE INDEX "WebhookDelivery_status_nextAttemptAt_idx" ON "WebhookDelivery"("status", "nextAttemptAt");
|
||||||
|
CREATE INDEX "WebhookDelivery_endpointId_createdAt_idx" ON "WebhookDelivery"("endpointId", "createdAt");
|
||||||
|
CREATE INDEX "SmsCampaign_organizationId_status_scheduledAt_idx" ON "SmsCampaign"("organizationId", "status", "scheduledAt");
|
||||||
|
CREATE INDEX "SmsCampaignRecipient_campaignId_status_idx" ON "SmsCampaignRecipient"("campaignId", "status");
|
||||||
|
|
||||||
|
ALTER TABLE "OrganizationApiKey" ADD CONSTRAINT "OrganizationApiKey_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "WebhookEndpoint" ADD CONSTRAINT "WebhookEndpoint_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "WebhookDelivery" ADD CONSTRAINT "WebhookDelivery_endpointId_fkey" FOREIGN KEY ("endpointId") REFERENCES "WebhookEndpoint"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "SmsCampaign" ADD CONSTRAINT "SmsCampaign_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "SmsCampaignRecipient" ADD CONSTRAINT "SmsCampaignRecipient_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "SmsCampaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Membership" ADD COLUMN "dataVisibility" TEXT NOT NULL DEFAULT 'own';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Team" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT NOT NULL DEFAULT '',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Team_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "TeamMember" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"teamId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"role" TEXT NOT NULL DEFAULT 'member',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "TeamMember_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Team_organizationId_idx" ON "Team"("organizationId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Team_organizationId_name_key" ON "Team"("organizationId", "name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TeamMember_userId_idx" ON "TeamMember"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TeamMember_teamId_idx" ON "TeamMember"("teamId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "TeamMember_teamId_userId_key" ON "TeamMember"("teamId", "userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Team" ADD CONSTRAINT "Team_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "WooCommerceConnection" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"label" TEXT NOT NULL,
|
||||||
|
"storeUrl" TEXT NOT NULL,
|
||||||
|
"consumerKeyCiphertext" TEXT NOT NULL,
|
||||||
|
"consumerKeyIv" TEXT NOT NULL,
|
||||||
|
"consumerKeyTag" TEXT NOT NULL,
|
||||||
|
"consumerKeyLast4" TEXT NOT NULL DEFAULT '',
|
||||||
|
"consumerSecretCiphertext" TEXT NOT NULL,
|
||||||
|
"consumerSecretIv" TEXT NOT NULL,
|
||||||
|
"consumerSecretTag" TEXT NOT NULL,
|
||||||
|
"webhookSecretCiphertext" TEXT,
|
||||||
|
"webhookSecretIv" TEXT,
|
||||||
|
"webhookSecretTag" TEXT,
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"lastSyncAt" TIMESTAMP(3),
|
||||||
|
"lastHealthAt" TIMESTAMP(3),
|
||||||
|
"lastHealthOk" BOOLEAN,
|
||||||
|
"lastError" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "WooCommerceConnection_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "WooCommerceOrderLink" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"connectionId" TEXT NOT NULL,
|
||||||
|
"wooOrderId" TEXT NOT NULL,
|
||||||
|
"wooOrderNumber" TEXT,
|
||||||
|
"invoiceId" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT '',
|
||||||
|
"total" DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||||
|
"currency" TEXT NOT NULL DEFAULT 'IRR',
|
||||||
|
"rawPayload" JSONB,
|
||||||
|
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "WooCommerceOrderLink_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "WooCommerceConnection_organizationId_enabled_idx" ON "WooCommerceConnection"("organizationId", "enabled");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "WooCommerceOrderLink_organizationId_syncedAt_idx" ON "WooCommerceOrderLink"("organizationId", "syncedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "WooCommerceOrderLink_invoiceId_idx" ON "WooCommerceOrderLink"("invoiceId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "WooCommerceOrderLink_connectionId_wooOrderId_key" ON "WooCommerceOrderLink"("connectionId", "wooOrderId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "WooCommerceConnection" ADD CONSTRAINT "WooCommerceConnection_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "WooCommerceOrderLink" ADD CONSTRAINT "WooCommerceOrderLink_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "WooCommerceConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "VoipConnection" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"label" TEXT NOT NULL,
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"webhookSecretCiphertext" TEXT,
|
||||||
|
"webhookSecretIv" TEXT,
|
||||||
|
"webhookSecretTag" TEXT,
|
||||||
|
"clickToCallUrl" TEXT,
|
||||||
|
"lastEventAt" TIMESTAMP(3),
|
||||||
|
"lastError" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "VoipConnection_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "VoipCallLog" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"connectionId" TEXT NOT NULL,
|
||||||
|
"externalCallId" TEXT NOT NULL,
|
||||||
|
"direction" TEXT NOT NULL DEFAULT 'inbound',
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'completed',
|
||||||
|
"fromNumber" TEXT,
|
||||||
|
"toNumber" TEXT,
|
||||||
|
"personId" TEXT,
|
||||||
|
"activityId" TEXT,
|
||||||
|
"durationSec" INTEGER,
|
||||||
|
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"endedAt" TIMESTAMP(3),
|
||||||
|
"rawPayload" JSONB,
|
||||||
|
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "VoipCallLog_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "VoipConnection_organizationId_enabled_idx" ON "VoipConnection"("organizationId", "enabled");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "VoipCallLog_organizationId_syncedAt_idx" ON "VoipCallLog"("organizationId", "syncedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "VoipCallLog_personId_idx" ON "VoipCallLog"("personId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "VoipCallLog_activityId_idx" ON "VoipCallLog"("activityId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "VoipCallLog_connectionId_externalCallId_key" ON "VoipCallLog"("connectionId", "externalCallId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "VoipConnection" ADD CONSTRAINT "VoipConnection_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "VoipCallLog" ADD CONSTRAINT "VoipCallLog_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "VoipConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ReservationService" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT NOT NULL DEFAULT '',
|
||||||
|
"durationMinutes" INTEGER NOT NULL DEFAULT 30,
|
||||||
|
"workDays" INTEGER[] DEFAULT ARRAY[0, 1, 2, 3, 4, 5, 6]::INTEGER[],
|
||||||
|
"dayStartHour" INTEGER NOT NULL DEFAULT 9,
|
||||||
|
"dayEndHour" INTEGER NOT NULL DEFAULT 17,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ReservationService_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ReservationAppointment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"serviceId" TEXT NOT NULL,
|
||||||
|
"personId" TEXT NOT NULL,
|
||||||
|
"assigneeId" TEXT,
|
||||||
|
"startsAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"endsAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
"notes" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ReservationAppointment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ReservationService_organizationId_isActive_idx" ON "ReservationService"("organizationId", "isActive");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ReservationAppointment_organizationId_startsAt_idx" ON "ReservationAppointment"("organizationId", "startsAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ReservationAppointment_organizationId_status_idx" ON "ReservationAppointment"("organizationId", "status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ReservationAppointment_serviceId_startsAt_idx" ON "ReservationAppointment"("serviceId", "startsAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ReservationAppointment_personId_idx" ON "ReservationAppointment"("personId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ReservationAppointment_assigneeId_startsAt_idx" ON "ReservationAppointment"("assigneeId", "startsAt");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ReservationService" ADD CONSTRAINT "ReservationService_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ReservationAppointment" ADD CONSTRAINT "ReservationAppointment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ReservationAppointment" ADD CONSTRAINT "ReservationAppointment_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "ReservationService"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ReservationAppointment" ADD CONSTRAINT "ReservationAppointment_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ReservationAppointment" ADD CONSTRAINT "ReservationAppointment_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Note" ADD COLUMN "visibility" TEXT NOT NULL DEFAULT 'team';
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Note_organizationId_visibility_idx" ON "Note"("organizationId", "visibility");
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "NoteRevision" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"noteId" TEXT NOT NULL,
|
||||||
|
"editorId" TEXT,
|
||||||
|
"title" TEXT,
|
||||||
|
"body" TEXT NOT NULL,
|
||||||
|
"noteType" TEXT NOT NULL,
|
||||||
|
"visibility" TEXT NOT NULL DEFAULT 'team',
|
||||||
|
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||||
|
"checklist" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "NoteRevision_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "NoteRevision_noteId_createdAt_idx" ON "NoteRevision"("noteId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "NoteRevision_organizationId_idx" ON "NoteRevision"("organizationId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "NoteRevision_editorId_idx" ON "NoteRevision"("editorId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "NoteRevision" ADD CONSTRAINT "NoteRevision_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "NoteRevision" ADD CONSTRAINT "NoteRevision_noteId_fkey" FOREIGN KEY ("noteId") REFERENCES "Note"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "NoteRevision" ADD CONSTRAINT "NoteRevision_editorId_fkey" FOREIGN KEY ("editorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_DealAssignees" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_DealAssignees_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_DealAssignees_B_index" ON "_DealAssignees"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_DealAssignees" ADD CONSTRAINT "_DealAssignees_A_fkey" FOREIGN KEY ("A") REFERENCES "Deal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_DealAssignees" ADD CONSTRAINT "_DealAssignees_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "VoipCallLog" ADD COLUMN "recordingUrl" TEXT;
|
||||||
|
ALTER TABLE "VoipCallLog" ADD COLUMN "transcript" TEXT;
|
||||||
|
ALTER TABLE "VoipCallLog" ADD COLUMN "summary" TEXT;
|
||||||
|
ALTER TABLE "VoipCallLog" ADD COLUMN "summaryStatus" TEXT NOT NULL DEFAULT 'none';
|
||||||
|
ALTER TABLE "VoipCallLog" ADD COLUMN "summaryNoteId" TEXT;
|
||||||
|
ALTER TABLE "VoipCallLog" ADD COLUMN "summaryError" TEXT;
|
||||||
|
ALTER TABLE "VoipCallLog" ADD COLUMN "summaryAttempts" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE "VoipCallLog" ADD COLUMN "summaryNextAttemptAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "VoipCallLog_summaryStatus_summaryNextAttemptAt_idx" ON "VoipCallLog"("summaryStatus", "summaryNextAttemptAt");
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LeadStage" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"position" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"color" TEXT NOT NULL DEFAULT 'teal',
|
||||||
|
"showOnBoard" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"isConverted" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isLost" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isUnqualified" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "LeadStage_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Lead" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"contactName" TEXT,
|
||||||
|
"mobile" TEXT NOT NULL,
|
||||||
|
"phone" TEXT,
|
||||||
|
"city" TEXT,
|
||||||
|
"region" TEXT,
|
||||||
|
"trade" TEXT,
|
||||||
|
"source" TEXT,
|
||||||
|
"notes" TEXT,
|
||||||
|
"estimatedValue" DECIMAL(18,2),
|
||||||
|
"stageId" TEXT NOT NULL,
|
||||||
|
"position" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"assigneeId" TEXT,
|
||||||
|
"createdById" TEXT,
|
||||||
|
"lastContactAt" TIMESTAMP(3),
|
||||||
|
"nextFollowUpAt" TIMESTAMP(3),
|
||||||
|
"convertedPersonId" TEXT,
|
||||||
|
"convertedAt" TIMESTAMP(3),
|
||||||
|
"archivedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Lead_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LeadEvent" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"leadId" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"body" TEXT,
|
||||||
|
"meta" JSONB,
|
||||||
|
"actorId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "LeadEvent_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LeadImportJob" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"createdById" TEXT,
|
||||||
|
"fileName" TEXT,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
"totalCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"processedCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"successCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"duplicateCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"errorCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"rows" JSONB NOT NULL DEFAULT '[]',
|
||||||
|
"errorSamples" JSONB NOT NULL DEFAULT '[]',
|
||||||
|
"startedAt" TIMESTAMP(3),
|
||||||
|
"finishedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "LeadImportJob_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LeadStage_organizationId_position_idx" ON "LeadStage"("organizationId", "position");
|
||||||
|
CREATE UNIQUE INDEX "LeadStage_organizationId_name_key" ON "LeadStage"("organizationId", "name");
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "Lead_organizationId_mobile_key" ON "Lead"("organizationId", "mobile");
|
||||||
|
CREATE INDEX "Lead_organizationId_stageId_position_idx" ON "Lead"("organizationId", "stageId", "position");
|
||||||
|
CREATE INDEX "Lead_organizationId_assigneeId_idx" ON "Lead"("organizationId", "assigneeId");
|
||||||
|
CREATE INDEX "Lead_organizationId_nextFollowUpAt_idx" ON "Lead"("organizationId", "nextFollowUpAt");
|
||||||
|
CREATE INDEX "Lead_organizationId_lastContactAt_idx" ON "Lead"("organizationId", "lastContactAt");
|
||||||
|
CREATE INDEX "Lead_organizationId_archivedAt_idx" ON "Lead"("organizationId", "archivedAt");
|
||||||
|
CREATE INDEX "Lead_organizationId_createdAt_idx" ON "Lead"("organizationId", "createdAt");
|
||||||
|
CREATE INDEX "Lead_organizationId_city_idx" ON "Lead"("organizationId", "city");
|
||||||
|
CREATE INDEX "Lead_organizationId_trade_idx" ON "Lead"("organizationId", "trade");
|
||||||
|
CREATE INDEX "Lead_organizationId_source_idx" ON "Lead"("organizationId", "source");
|
||||||
|
|
||||||
|
CREATE INDEX "LeadEvent_leadId_createdAt_idx" ON "LeadEvent"("leadId", "createdAt");
|
||||||
|
CREATE INDEX "LeadEvent_organizationId_createdAt_idx" ON "LeadEvent"("organizationId", "createdAt");
|
||||||
|
|
||||||
|
CREATE INDEX "LeadImportJob_organizationId_status_createdAt_idx" ON "LeadImportJob"("organizationId", "status", "createdAt");
|
||||||
|
CREATE INDEX "LeadImportJob_status_createdAt_idx" ON "LeadImportJob"("status", "createdAt");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LeadStage" ADD CONSTRAINT "LeadStage_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "Lead" ADD CONSTRAINT "Lead_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "Lead" ADD CONSTRAINT "Lead_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "LeadStage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "Lead" ADD CONSTRAINT "Lead_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "Lead" ADD CONSTRAINT "Lead_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "LeadEvent" ADD CONSTRAINT "LeadEvent_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "LeadEvent" ADD CONSTRAINT "LeadEvent_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "Lead"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "LeadImportJob" ADD CONSTRAINT "LeadImportJob_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Lead" ADD COLUMN IF NOT EXISTS "logoStorageKey" TEXT;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
-- AlterTable LeadForm: publish → Lead connection
|
||||||
|
ALTER TABLE "LeadForm" ADD COLUMN IF NOT EXISTS "createLead" BOOLEAN NOT NULL DEFAULT true;
|
||||||
|
ALTER TABLE "LeadForm" ADD COLUMN IF NOT EXISTS "leadStageId" TEXT;
|
||||||
|
ALTER TABLE "LeadForm" ADD COLUMN IF NOT EXISTS "duplicateMode" TEXT NOT NULL DEFAULT 'update';
|
||||||
|
ALTER TABLE "LeadForm" ADD COLUMN IF NOT EXISTS "viewCount" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE "LeadForm" ADD COLUMN IF NOT EXISTS "startCount" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE "LeadForm" ADD COLUMN IF NOT EXISTS "submitCount" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE "LeadForm" ADD COLUMN IF NOT EXISTS "leadCreateCount" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE "LeadForm" ADD COLUMN IF NOT EXISTS "duplicateCount" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Existing forms: keep person/deal behavior unless already lead-oriented
|
||||||
|
ALTER TABLE "LeadForm" ALTER COLUMN "createPerson" SET DEFAULT false;
|
||||||
|
ALTER TABLE "LeadForm" ALTER COLUMN "createDeal" SET DEFAULT false;
|
||||||
|
|
||||||
|
-- AlterTable LeadFormSubmission
|
||||||
|
ALTER TABLE "LeadFormSubmission" ADD COLUMN IF NOT EXISTS "leadId" TEXT;
|
||||||
|
ALTER TABLE "LeadFormSubmission" ADD COLUMN IF NOT EXISTS "status" TEXT NOT NULL DEFAULT 'created';
|
||||||
|
ALTER TABLE "LeadFormSubmission" ADD COLUMN IF NOT EXISTS "utm" JSONB;
|
||||||
|
ALTER TABLE "LeadFormSubmission" ADD COLUMN IF NOT EXISTS "meta" JSONB;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "LeadFormSubmission_leadId_idx" ON "LeadFormSubmission"("leadId");
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ConversationSettings" (
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"chatHost" TEXT NOT NULL,
|
||||||
|
"apiKeyCipher" TEXT NOT NULL,
|
||||||
|
"apiKeyIv" TEXT NOT NULL,
|
||||||
|
"apiKeyTag" TEXT NOT NULL,
|
||||||
|
"uploadUrl" TEXT NOT NULL DEFAULT '',
|
||||||
|
"downloadUrl" TEXT NOT NULL DEFAULT '',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ConversationSettings_pkey" PRIMARY KEY ("organizationId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ConversationRoom" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"externalRoomId" TEXT NOT NULL,
|
||||||
|
"kind" TEXT NOT NULL,
|
||||||
|
"name" TEXT,
|
||||||
|
"dmUserMinId" TEXT,
|
||||||
|
"dmUserMaxId" TEXT,
|
||||||
|
"createdByUserId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ConversationRoom_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ConversationRoom_organizationId_externalRoomId_key" ON "ConversationRoom"("organizationId", "externalRoomId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ConversationRoom_organizationId_dmUserMinId_dmUserMaxId_key" ON "ConversationRoom"("organizationId", "dmUserMinId", "dmUserMaxId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ConversationRoom_organizationId_kind_idx" ON "ConversationRoom"("organizationId", "kind");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ConversationSettings" ADD CONSTRAINT "ConversationSettings_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ConversationRoom" ADD CONSTRAINT "ConversationRoom_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ConversationHiddenMessage" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"externalRoomId" TEXT NOT NULL,
|
||||||
|
"messageId" TEXT NOT NULL,
|
||||||
|
"hiddenByUserId" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "ConversationHiddenMessage_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ConversationHiddenMessage_organizationId_externalRoomId_idx" ON "ConversationHiddenMessage"("organizationId", "externalRoomId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ConversationHiddenMessage_organizationId_externalRoomId_messageId_key" ON "ConversationHiddenMessage"("organizationId", "externalRoomId", "messageId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ConversationHiddenMessage" ADD CONSTRAINT "ConversationHiddenMessage_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ConversationHiddenMessage" ADD COLUMN "scope" TEXT NOT NULL DEFAULT 'all';
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "ConversationHiddenMessage_organizationId_externalRoomId_messageId_key";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ConversationHiddenMessage_organizationId_externalRoomId_messageId_scope_hiddenByUserId_key" ON "ConversationHiddenMessage"("organizationId", "externalRoomId", "messageId", "scope", "hiddenByUserId");
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ConversationPinnedMessage" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"externalRoomId" TEXT NOT NULL,
|
||||||
|
"messageId" TEXT NOT NULL,
|
||||||
|
"preview" TEXT NOT NULL DEFAULT '',
|
||||||
|
"pinnedByUserId" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ConversationPinnedMessage_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ConversationRoomUserPin" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"externalRoomId" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "ConversationRoomUserPin_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ConversationPinnedMessage_organizationId_externalRoomId_key" ON "ConversationPinnedMessage"("organizationId", "externalRoomId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ConversationPinnedMessage_organizationId_externalRoomId_idx" ON "ConversationPinnedMessage"("organizationId", "externalRoomId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ConversationRoomUserPin_organizationId_userId_externalRoomId_key" ON "ConversationRoomUserPin"("organizationId", "userId", "externalRoomId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ConversationRoomUserPin_organizationId_userId_idx" ON "ConversationRoomUserPin"("organizationId", "userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ConversationPinnedMessage" ADD CONSTRAINT "ConversationPinnedMessage_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ConversationRoomUserPin" ADD CONSTRAINT "ConversationRoomUserPin_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ConversationRoomMember" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"externalRoomId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"role" TEXT NOT NULL DEFAULT 'member',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ConversationRoomMember_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ConversationRoomMember_organizationId_externalRoomId_userId_key" ON "ConversationRoomMember"("organizationId", "externalRoomId", "userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ConversationRoomMember_organizationId_externalRoomId_idx" ON "ConversationRoomMember"("organizationId", "externalRoomId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ConversationRoomMember" ADD CONSTRAINT "ConversationRoomMember_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- Mute per-user (smart: mentions can still notify)
|
||||||
|
CREATE TABLE "ConversationRoomUserMute" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"externalRoomId" TEXT NOT NULL,
|
||||||
|
"allowMentions" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ConversationRoomUserMute_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "ConversationRoomUserMute_organizationId_userId_externalRoomId_key" ON "ConversationRoomUserMute"("organizationId", "userId", "externalRoomId");
|
||||||
|
CREATE INDEX "ConversationRoomUserMute_organizationId_userId_idx" ON "ConversationRoomUserMute"("organizationId", "userId");
|
||||||
|
ALTER TABLE "ConversationRoomUserMute" ADD CONSTRAINT "ConversationRoomUserMute_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Message emoji reactions
|
||||||
|
CREATE TABLE "ConversationMessageReaction" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"externalRoomId" TEXT NOT NULL,
|
||||||
|
"messageId" TEXT NOT NULL,
|
||||||
|
"emoji" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "ConversationMessageReaction_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "ConversationMessageReaction_organizationId_externalRoomId_messageId_userId_emoji_key" ON "ConversationMessageReaction"("organizationId", "externalRoomId", "messageId", "userId", "emoji");
|
||||||
|
CREATE INDEX "ConversationMessageReaction_organizationId_externalRoomId_idx" ON "ConversationMessageReaction"("organizationId", "externalRoomId");
|
||||||
|
ALTER TABLE "ConversationMessageReaction" ADD CONSTRAINT "ConversationMessageReaction_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
-- Extend AutomationTrigger
|
||||||
|
CREATE TYPE "AutomationTrigger_new" AS ENUM (
|
||||||
|
'person_created',
|
||||||
|
'lead_submitted',
|
||||||
|
'deal_created',
|
||||||
|
'deal_stage_changed',
|
||||||
|
'deal_won',
|
||||||
|
'deal_lost',
|
||||||
|
'ticket_created',
|
||||||
|
'ticket_resolved',
|
||||||
|
'invoice_issued',
|
||||||
|
'invoice_paid',
|
||||||
|
'activity_done',
|
||||||
|
'sms_link_clicked'
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "AutomationRule"
|
||||||
|
ALTER COLUMN "trigger" TYPE "AutomationTrigger_new"
|
||||||
|
USING ("trigger"::text::"AutomationTrigger_new");
|
||||||
|
|
||||||
|
ALTER TYPE "AutomationTrigger" RENAME TO "AutomationTrigger_old";
|
||||||
|
ALTER TYPE "AutomationTrigger_new" RENAME TO "AutomationTrigger";
|
||||||
|
DROP TYPE "AutomationTrigger_old";
|
||||||
|
|
||||||
|
-- Extend MessageAutomationTrigger
|
||||||
|
CREATE TYPE "MessageAutomationTrigger_new" AS ENUM (
|
||||||
|
'person_created',
|
||||||
|
'lead_submitted',
|
||||||
|
'deal_created',
|
||||||
|
'deal_stage_changed',
|
||||||
|
'deal_won',
|
||||||
|
'deal_lost',
|
||||||
|
'ticket_created',
|
||||||
|
'ticket_resolved',
|
||||||
|
'invoice_issued',
|
||||||
|
'invoice_paid',
|
||||||
|
'activity_created',
|
||||||
|
'activity_done',
|
||||||
|
'sms_link_clicked'
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "MessageAutomationRule"
|
||||||
|
ALTER COLUMN "trigger" TYPE "MessageAutomationTrigger_new"
|
||||||
|
USING ("trigger"::text::"MessageAutomationTrigger_new");
|
||||||
|
|
||||||
|
ALTER TABLE "MessageAutomationJob"
|
||||||
|
ALTER COLUMN "trigger" TYPE "MessageAutomationTrigger_new"
|
||||||
|
USING ("trigger"::text::"MessageAutomationTrigger_new");
|
||||||
|
|
||||||
|
ALTER TYPE "MessageAutomationTrigger" RENAME TO "MessageAutomationTrigger_old";
|
||||||
|
ALTER TYPE "MessageAutomationTrigger_new" RENAME TO "MessageAutomationTrigger";
|
||||||
|
DROP TYPE "MessageAutomationTrigger_old";
|
||||||
|
|
||||||
|
-- Recipient: optional leadId for future Lead campaigns
|
||||||
|
ALTER TABLE "SmsCampaignRecipient" ADD COLUMN "leadId" TEXT;
|
||||||
|
CREATE INDEX "SmsCampaignRecipient_personId_idx" ON "SmsCampaignRecipient"("personId");
|
||||||
|
CREATE INDEX "SmsCampaignRecipient_leadId_idx" ON "SmsCampaignRecipient"("leadId");
|
||||||
|
|
||||||
|
-- TrackingLink
|
||||||
|
CREATE TABLE "TrackingLink" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"originalUrl" TEXT NOT NULL,
|
||||||
|
"channel" TEXT NOT NULL DEFAULT 'sms',
|
||||||
|
"sourceType" TEXT NOT NULL,
|
||||||
|
"sourceId" TEXT NOT NULL,
|
||||||
|
"recipientType" TEXT,
|
||||||
|
"recipientId" TEXT,
|
||||||
|
"smsRecipientId" TEXT,
|
||||||
|
"personId" TEXT,
|
||||||
|
"leadId" TEXT,
|
||||||
|
"clickCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"firstClickedAt" TIMESTAMP(3),
|
||||||
|
"lastClickedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "TrackingLink_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "TrackingLink_token_key" ON "TrackingLink"("token");
|
||||||
|
CREATE INDEX "TrackingLink_organizationId_sourceType_sourceId_idx" ON "TrackingLink"("organizationId", "sourceType", "sourceId");
|
||||||
|
CREATE INDEX "TrackingLink_organizationId_personId_idx" ON "TrackingLink"("organizationId", "personId");
|
||||||
|
CREATE INDEX "TrackingLink_organizationId_leadId_idx" ON "TrackingLink"("organizationId", "leadId");
|
||||||
|
CREATE INDEX "TrackingLink_smsRecipientId_idx" ON "TrackingLink"("smsRecipientId");
|
||||||
|
CREATE INDEX "TrackingLink_token_idx" ON "TrackingLink"("token");
|
||||||
|
|
||||||
|
ALTER TABLE "TrackingLink" ADD CONSTRAINT "TrackingLink_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "TrackingLink" ADD CONSTRAINT "TrackingLink_smsRecipientId_fkey" FOREIGN KEY ("smsRecipientId") REFERENCES "SmsCampaignRecipient"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "TrackingLink" ADD CONSTRAINT "TrackingLink_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "TrackingLink" ADD CONSTRAINT "TrackingLink_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "Lead"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- TrackingEvent
|
||||||
|
CREATE TABLE "TrackingEvent" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"trackingLinkId" TEXT,
|
||||||
|
"eventType" TEXT NOT NULL,
|
||||||
|
"channel" TEXT NOT NULL DEFAULT 'sms',
|
||||||
|
"sourceType" TEXT,
|
||||||
|
"sourceId" TEXT,
|
||||||
|
"recipientType" TEXT,
|
||||||
|
"recipientId" TEXT,
|
||||||
|
"personId" TEXT,
|
||||||
|
"leadId" TEXT,
|
||||||
|
"occurredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"metadata" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "TrackingEvent_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "TrackingEvent_organizationId_eventType_occurredAt_idx" ON "TrackingEvent"("organizationId", "eventType", "occurredAt");
|
||||||
|
CREATE INDEX "TrackingEvent_organizationId_sourceType_sourceId_eventType_idx" ON "TrackingEvent"("organizationId", "sourceType", "sourceId", "eventType");
|
||||||
|
CREATE INDEX "TrackingEvent_organizationId_personId_eventType_idx" ON "TrackingEvent"("organizationId", "personId", "eventType");
|
||||||
|
CREATE INDEX "TrackingEvent_organizationId_leadId_eventType_idx" ON "TrackingEvent"("organizationId", "leadId", "eventType");
|
||||||
|
CREATE INDEX "TrackingEvent_trackingLinkId_occurredAt_idx" ON "TrackingEvent"("trackingLinkId", "occurredAt");
|
||||||
|
|
||||||
|
ALTER TABLE "TrackingEvent" ADD CONSTRAINT "TrackingEvent_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "TrackingEvent" ADD CONSTRAINT "TrackingEvent_trackingLinkId_fkey" FOREIGN KEY ("trackingLinkId") REFERENCES "TrackingLink"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "TrackingEvent" ADD CONSTRAINT "TrackingEvent_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "TrackingEvent" ADD CONSTRAINT "TrackingEvent_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "Lead"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "instagram" TEXT;
|
||||||
|
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "addresses" JSONB NOT NULL DEFAULT '[]';
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Lead" ADD COLUMN IF NOT EXISTS "instagram" TEXT;
|
||||||
|
ALTER TABLE "Lead" ADD COLUMN IF NOT EXISTS "addresses" JSONB NOT NULL DEFAULT '[]';
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- OrgRole hierarchy (parent / child sub-roles)
|
||||||
|
ALTER TABLE "OrgRole" ADD COLUMN IF NOT EXISTS "parentId" TEXT;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "OrgRole_parentId_idx" ON "OrgRole"("parentId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "OrgRole_organizationId_parentId_idx" ON "OrgRole"("organizationId", "parentId");
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'OrgRole_parentId_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "OrgRole"
|
||||||
|
ADD CONSTRAINT "OrgRole_parentId_fkey"
|
||||||
|
FOREIGN KEY ("parentId") REFERENCES "OrgRole"("id")
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- Multi-assignee support for activities (implicit many-to-many)
|
||||||
|
CREATE TABLE IF NOT EXISTS "_ActivityAssignees" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "_ActivityAssignees_AB_unique" ON "_ActivityAssignees"("A", "B");
|
||||||
|
CREATE INDEX IF NOT EXISTS "_ActivityAssignees_B_index" ON "_ActivityAssignees"("B");
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = '_ActivityAssignees_A_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "_ActivityAssignees"
|
||||||
|
ADD CONSTRAINT "_ActivityAssignees_A_fkey"
|
||||||
|
FOREIGN KEY ("A") REFERENCES "Activity"("id")
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = '_ActivityAssignees_B_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "_ActivityAssignees"
|
||||||
|
ADD CONSTRAINT "_ActivityAssignees_B_fkey"
|
||||||
|
FOREIGN KEY ("B") REFERENCES "User"("id")
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Backfill from legacy single assigneeId
|
||||||
|
INSERT INTO "_ActivityAssignees" ("A", "B")
|
||||||
|
SELECT a."id", a."assigneeId"
|
||||||
|
FROM "Activity" a
|
||||||
|
WHERE a."assigneeId" IS NOT NULL
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "previousFirstName" TEXT;
|
||||||
|
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "previousLastName" TEXT;
|
||||||
|
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "importSource" TEXT NOT NULL DEFAULT 'manual';
|
||||||
|
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "sourceRef" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX IF NOT EXISTS "Person_organizationId_importSource_idx" ON "Person"("organizationId", "importSource");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Person_organizationId_sourceRef_idx" ON "Person"("organizationId", "sourceRef");
|
||||||
|
|
||||||
|
-- Backfill from legacy customFields / notes
|
||||||
|
UPDATE "Person"
|
||||||
|
SET
|
||||||
|
"importSource" = 'didar',
|
||||||
|
"sourceRef" = COALESCE(NULLIF(BTRIM("sourceRef"), ''), NULLIF(BTRIM("customFields"->>'didarId'), ''))
|
||||||
|
WHERE COALESCE(BTRIM("customFields"->>'didarId'), '') <> '';
|
||||||
|
|
||||||
|
UPDATE "Person"
|
||||||
|
SET "importSource" = 'tahesab'
|
||||||
|
WHERE "importSource" = 'manual'
|
||||||
|
AND notes ILIKE '%تهحساب%';
|
||||||
|
|
||||||
|
UPDATE "Person"
|
||||||
|
SET "importSource" = 'excel'
|
||||||
|
WHERE "importSource" = 'manual'
|
||||||
|
AND COALESCE(BTRIM("customFields"->>'customerCode'), '') <> ''
|
||||||
|
AND COALESCE(BTRIM("customFields"->>'didarId'), '') = '';
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE IF NOT EXISTS "UserTablePreference" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"tableKey" TEXT NOT NULL,
|
||||||
|
"hiddenColumns" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "UserTablePreference_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "UserTablePreference_userId_organizationId_tableKey_key" ON "UserTablePreference"("userId", "organizationId", "tableKey");
|
||||||
|
CREATE INDEX IF NOT EXISTS "UserTablePreference_userId_organizationId_idx" ON "UserTablePreference"("userId", "organizationId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "UserTablePreference" ADD CONSTRAINT "UserTablePreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "previousImportSource" TEXT;
|
||||||
|
ALTER TABLE "Person" ADD COLUMN IF NOT EXISTS "previousSourceRef" TEXT;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "Person_organizationId_previousSourceRef_idx" ON "Person"("organizationId", "previousSourceRef");
|
||||||
|
|
||||||
|
-- اشخاصی که قبلاً از دیدار آمدهاند و بعد با تهحساب بازنویسی شدهاند: کد دیدار را بهعنوان کد قبلی بازیابی کن
|
||||||
|
UPDATE "Person"
|
||||||
|
SET "previousImportSource" = 'didar',
|
||||||
|
"previousSourceRef" = TRIM("customFields"->>'didarId')
|
||||||
|
WHERE "importSource" = 'tahesab'
|
||||||
|
AND "previousSourceRef" IS NULL
|
||||||
|
AND "customFields" IS NOT NULL
|
||||||
|
AND jsonb_typeof("customFields") = 'object'
|
||||||
|
AND COALESCE(TRIM("customFields"->>'didarId'), '') <> '';
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "code" TEXT;
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "importSource" TEXT NOT NULL DEFAULT 'manual';
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "sourceRef" TEXT;
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "economicCode" TEXT;
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "registrationNumber" TEXT;
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "otherPhones" TEXT;
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "fax" TEXT;
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "otherEmails" TEXT;
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "province" TEXT;
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "city" TEXT;
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "postalCode" TEXT;
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "groupName" TEXT;
|
||||||
|
ALTER TABLE "Company" ADD COLUMN IF NOT EXISTS "registeredAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "Company_organizationId_sourceRef_idx" ON "Company"("organizationId", "sourceRef");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Company_organizationId_code_idx" ON "Company"("organizationId", "code");
|
||||||
|
|
||||||
|
-- شرکتهایی که از ورود اشخاص دیدار ساخته شدهاند: کد دیدار شرکت را از customFields اشخاص بازیابی کن
|
||||||
|
UPDATE "Company" c
|
||||||
|
SET "sourceRef" = sub.didar_code,
|
||||||
|
"importSource" = 'didar'
|
||||||
|
FROM (
|
||||||
|
SELECT DISTINCT ON (p."companyId")
|
||||||
|
p."companyId" AS company_id,
|
||||||
|
TRIM(p."customFields"->>'companyDidarId') AS didar_code
|
||||||
|
FROM "Person" p
|
||||||
|
WHERE p."companyId" IS NOT NULL
|
||||||
|
AND jsonb_typeof(p."customFields") = 'object'
|
||||||
|
AND COALESCE(TRIM(p."customFields"->>'companyDidarId'), '') <> ''
|
||||||
|
ORDER BY p."companyId", p."createdAt"
|
||||||
|
) sub
|
||||||
|
WHERE c."id" = sub.company_id
|
||||||
|
AND c."sourceRef" IS NULL;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "Activity" ADD COLUMN IF NOT EXISTS "reminderSentAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "Activity_reminderAt_reminderSentAt_idx" ON "Activity"("reminderAt", "reminderSentAt");
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "Membership" ADD COLUMN IF NOT EXISTS "dataVisibilityScopes" JSONB NOT NULL DEFAULT '{}';
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
ALTER TABLE "Deal" ADD COLUMN IF NOT EXISTS "code" TEXT;
|
||||||
|
ALTER TABLE "Deal" ADD COLUMN IF NOT EXISTS "importSource" TEXT NOT NULL DEFAULT 'manual';
|
||||||
|
ALTER TABLE "Deal" ADD COLUMN IF NOT EXISTS "sourceRef" TEXT;
|
||||||
|
ALTER TABLE "Deal" ADD COLUMN IF NOT EXISTS "probability" INTEGER;
|
||||||
|
ALTER TABLE "Deal" ADD COLUMN IF NOT EXISTS "sourceDetail" TEXT;
|
||||||
|
ALTER TABLE "Deal" ADD COLUMN IF NOT EXISTS "referrer" TEXT;
|
||||||
|
ALTER TABLE "Deal" ADD COLUMN IF NOT EXISTS "lostNote" TEXT;
|
||||||
|
ALTER TABLE "Deal" ADD COLUMN IF NOT EXISTS "closedAt" TIMESTAMP(3);
|
||||||
|
ALTER TABLE "Deal" ADD COLUMN IF NOT EXISTS "createdById" TEXT;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Deal" ADD CONSTRAINT "Deal_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "Deal_organizationId_sourceRef_idx" ON "Deal"("organizationId", "sourceRef");
|
||||||
|
|
||||||
|
UPDATE "Deal"
|
||||||
|
SET "importSource" = 'didar', "sourceRef" = "customFields"->>'didarId'
|
||||||
|
WHERE "sourceRef" IS NULL AND COALESCE("customFields"->>'didarId', '') <> '';
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE IF NOT EXISTS "MontifyConnection" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"label" TEXT NOT NULL,
|
||||||
|
"baseUrl" TEXT NOT NULL,
|
||||||
|
"storeId" TEXT NOT NULL DEFAULT '',
|
||||||
|
"tokenCiphertext" TEXT NOT NULL,
|
||||||
|
"tokenIv" TEXT NOT NULL,
|
||||||
|
"tokenTag" TEXT NOT NULL,
|
||||||
|
"tokenLast4" TEXT NOT NULL DEFAULT '',
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"webhookRemoteId" TEXT,
|
||||||
|
"webhookSecretCiphertext" TEXT,
|
||||||
|
"webhookSecretIv" TEXT,
|
||||||
|
"webhookSecretTag" TEXT,
|
||||||
|
"customersCursor" TIMESTAMP(3),
|
||||||
|
"syncStatus" TEXT NOT NULL DEFAULT 'idle',
|
||||||
|
"syncStartedAt" TIMESTAMP(3),
|
||||||
|
"lastSyncAt" TIMESTAMP(3),
|
||||||
|
"lastSyncResult" JSONB,
|
||||||
|
"lastHealthAt" TIMESTAMP(3),
|
||||||
|
"lastHealthOk" BOOLEAN,
|
||||||
|
"lastError" TEXT,
|
||||||
|
"lastWebhookAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "MontifyConnection_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE IF NOT EXISTS "MontifyLink" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"connectionId" TEXT NOT NULL,
|
||||||
|
"entityType" TEXT NOT NULL,
|
||||||
|
"externalId" TEXT NOT NULL,
|
||||||
|
"entityId" TEXT NOT NULL,
|
||||||
|
"externalUpdatedAt" TIMESTAMP(3),
|
||||||
|
"ordersSyncedAt" TIMESTAMP(3),
|
||||||
|
"data" JSONB,
|
||||||
|
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "MontifyLink_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE IF NOT EXISTS "MontifyOrder" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"connectionId" TEXT NOT NULL,
|
||||||
|
"externalId" TEXT NOT NULL,
|
||||||
|
"customerExternalId" TEXT,
|
||||||
|
"personId" TEXT,
|
||||||
|
"orderReference" TEXT,
|
||||||
|
"orderStatus" TEXT,
|
||||||
|
"paymentStatus" TEXT,
|
||||||
|
"paymentType" TEXT,
|
||||||
|
"totalPrice" DECIMAL(20,2),
|
||||||
|
"discountTotalPrice" DECIMAL(20,2),
|
||||||
|
"totalDiscounts" DECIMAL(20,2),
|
||||||
|
"totalTax" DECIMAL(20,2),
|
||||||
|
"totalQty" DECIMAL(14,3),
|
||||||
|
"profit" DECIMAL(20,2),
|
||||||
|
"percentProfit" DECIMAL(10,3),
|
||||||
|
"sumGold" DECIMAL(14,3),
|
||||||
|
"sumSilver" DECIMAL(14,3),
|
||||||
|
"address" JSONB,
|
||||||
|
"note" TEXT,
|
||||||
|
"cancelReason" TEXT,
|
||||||
|
"cancelledAt" TIMESTAMP(3),
|
||||||
|
"closedAt" TIMESTAMP(3),
|
||||||
|
"codeDiscount" TEXT,
|
||||||
|
"items" JSONB NOT NULL DEFAULT '[]',
|
||||||
|
"externalCreatedAt" TIMESTAMP(3),
|
||||||
|
"externalUpdatedAt" TIMESTAMP(3),
|
||||||
|
"deletedAt" TIMESTAMP(3),
|
||||||
|
"raw" JSONB,
|
||||||
|
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "MontifyOrder_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE IF NOT EXISTS "MontifyTransaction" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"connectionId" TEXT NOT NULL,
|
||||||
|
"externalId" TEXT NOT NULL,
|
||||||
|
"customerExternalId" TEXT,
|
||||||
|
"personId" TEXT,
|
||||||
|
"type" TEXT,
|
||||||
|
"amount" DECIMAL(24,4),
|
||||||
|
"currency" TEXT,
|
||||||
|
"status" TEXT,
|
||||||
|
"referenceCode" TEXT,
|
||||||
|
"externalCreatedAt" TIMESTAMP(3),
|
||||||
|
"raw" JSONB,
|
||||||
|
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "MontifyTransaction_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE IF NOT EXISTS "MontifyWebhookDelivery" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"connectionId" TEXT NOT NULL,
|
||||||
|
"deliveryId" TEXT NOT NULL,
|
||||||
|
"event" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'received',
|
||||||
|
"error" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "MontifyWebhookDelivery_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX IF NOT EXISTS "MontifyConnection_organizationId_enabled_idx" ON "MontifyConnection"("organizationId", "enabled");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "MontifyLink_connectionId_entityType_externalId_key" ON "MontifyLink"("connectionId", "entityType", "externalId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "MontifyLink_organizationId_entityType_entityId_idx" ON "MontifyLink"("organizationId", "entityType", "entityId");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "MontifyOrder_connectionId_externalId_key" ON "MontifyOrder"("connectionId", "externalId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "MontifyOrder_organizationId_personId_idx" ON "MontifyOrder"("organizationId", "personId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "MontifyOrder_organizationId_externalCreatedAt_idx" ON "MontifyOrder"("organizationId", "externalCreatedAt");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "MontifyTransaction_connectionId_externalId_key" ON "MontifyTransaction"("connectionId", "externalId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "MontifyTransaction_organizationId_personId_idx" ON "MontifyTransaction"("organizationId", "personId");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "MontifyWebhookDelivery_connectionId_deliveryId_key" ON "MontifyWebhookDelivery"("connectionId", "deliveryId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "MontifyWebhookDelivery_connectionId_createdAt_idx" ON "MontifyWebhookDelivery"("connectionId", "createdAt");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MontifyConnection" ADD CONSTRAINT "MontifyConnection_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MontifyLink" ADD CONSTRAINT "MontifyLink_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "MontifyConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MontifyOrder" ADD CONSTRAINT "MontifyOrder_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "MontifyConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MontifyTransaction" ADD CONSTRAINT "MontifyTransaction_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "MontifyConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MontifyWebhookDelivery" ADD CONSTRAINT "MontifyWebhookDelivery_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "MontifyConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- ماژول جدید برای محیطهایی که فهرست ماژول ذخیرهشده دارند و ووکامرس فعال است
|
||||||
|
UPDATE "Organization"
|
||||||
|
SET "enabledModules" = array_append("enabledModules", 'montify')
|
||||||
|
WHERE cardinality("enabledModules") > 0
|
||||||
|
AND 'woocommerce' = ANY("enabledModules")
|
||||||
|
AND NOT ('montify' = ANY("enabledModules"));
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RoutineTaskStatus" AS ENUM ('todo', 'in_progress', 'completed', 'overdue', 'skipped', 'cancelled');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RoutineRecurrence" AS ENUM ('daily', 'weekdays', 'weekly', 'biweekly', 'monthly', 'every_n_days', 'specific_dates');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RoutineTaskSource" AS ENUM ('routine', 'manual', 'follow_up', 'lead', 'customer', 'deal');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RoutineCategory" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"color" TEXT NOT NULL DEFAULT '#14b8a6',
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "RoutineCategory_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Routine" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"assigneeId" TEXT,
|
||||||
|
"teamId" TEXT,
|
||||||
|
"categoryId" TEXT,
|
||||||
|
"priority" "ActivityPriority" NOT NULL DEFAULT 'medium',
|
||||||
|
"timeOfDay" TEXT NOT NULL DEFAULT '09:00',
|
||||||
|
"durationMinutes" INTEGER,
|
||||||
|
"startDate" DATE NOT NULL,
|
||||||
|
"endDate" DATE,
|
||||||
|
"recurrence" "RoutineRecurrence" NOT NULL DEFAULT 'daily',
|
||||||
|
"weekdays" INTEGER[] DEFAULT ARRAY[]::INTEGER[],
|
||||||
|
"intervalDays" INTEGER,
|
||||||
|
"monthDay" INTEGER,
|
||||||
|
"specificDates" DATE[],
|
||||||
|
"isMandatory" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"allowSkip" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"skipReasonRequired" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"reportRequired" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"allowAfterDeadline" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"carryOverOverdue" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"entityLink" TEXT NOT NULL DEFAULT 'none',
|
||||||
|
"reminderMinutesBefore" INTEGER,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdById" TEXT,
|
||||||
|
"deletedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Routine_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RoutineTask" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"routineId" TEXT,
|
||||||
|
"assigneeId" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"categoryId" TEXT,
|
||||||
|
"priority" "ActivityPriority" NOT NULL DEFAULT 'medium',
|
||||||
|
"status" "RoutineTaskStatus" NOT NULL DEFAULT 'todo',
|
||||||
|
"scheduledFor" DATE NOT NULL,
|
||||||
|
"dueAt" TIMESTAMP(3),
|
||||||
|
"startedAt" TIMESTAMP(3),
|
||||||
|
"completedAt" TIMESTAMP(3),
|
||||||
|
"resultNote" TEXT,
|
||||||
|
"skipReason" TEXT,
|
||||||
|
"source" "RoutineTaskSource" NOT NULL DEFAULT 'manual',
|
||||||
|
"isMandatory" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"leadId" TEXT,
|
||||||
|
"personId" TEXT,
|
||||||
|
"companyId" TEXT,
|
||||||
|
"dealId" TEXT,
|
||||||
|
"invoiceId" TEXT,
|
||||||
|
"activityId" TEXT,
|
||||||
|
"reminderAt" TIMESTAMP(3),
|
||||||
|
"reminderSentAt" TIMESTAMP(3),
|
||||||
|
"overdueNotifiedAt" TIMESTAMP(3),
|
||||||
|
"createdById" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "RoutineTask_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RoutineTaskEvent" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"taskId" TEXT NOT NULL,
|
||||||
|
"actorId" TEXT,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"fromStatus" "RoutineTaskStatus",
|
||||||
|
"toStatus" "RoutineTaskStatus",
|
||||||
|
"note" TEXT,
|
||||||
|
"meta" JSONB NOT NULL DEFAULT '{}',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "RoutineTaskEvent_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RoutineUserSettings" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"remindBeforeMinutes" INTEGER DEFAULT 30,
|
||||||
|
"endOfDaySummary" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"overdueAlert" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"summaryHour" INTEGER NOT NULL DEFAULT 16,
|
||||||
|
"lastSummaryOn" DATE,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "RoutineUserSettings_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RoutineCategory_organizationId_sortOrder_idx" ON "RoutineCategory"("organizationId", "sortOrder");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "RoutineCategory_organizationId_name_key" ON "RoutineCategory"("organizationId", "name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Routine_organizationId_isActive_deletedAt_idx" ON "Routine"("organizationId", "isActive", "deletedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Routine_assigneeId_idx" ON "Routine"("assigneeId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Routine_teamId_idx" ON "Routine"("teamId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RoutineTask_organizationId_assigneeId_scheduledFor_idx" ON "RoutineTask"("organizationId", "assigneeId", "scheduledFor");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RoutineTask_organizationId_status_dueAt_idx" ON "RoutineTask"("organizationId", "status", "dueAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RoutineTask_reminderAt_reminderSentAt_idx" ON "RoutineTask"("reminderAt", "reminderSentAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RoutineTask_leadId_idx" ON "RoutineTask"("leadId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RoutineTask_personId_idx" ON "RoutineTask"("personId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RoutineTask_companyId_idx" ON "RoutineTask"("companyId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RoutineTask_dealId_idx" ON "RoutineTask"("dealId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RoutineTask_activityId_idx" ON "RoutineTask"("activityId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "RoutineTask_routineId_assigneeId_scheduledFor_key" ON "RoutineTask"("routineId", "assigneeId", "scheduledFor");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RoutineTaskEvent_taskId_createdAt_idx" ON "RoutineTaskEvent"("taskId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RoutineTaskEvent_organizationId_createdAt_idx" ON "RoutineTaskEvent"("organizationId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "RoutineUserSettings_organizationId_userId_key" ON "RoutineUserSettings"("organizationId", "userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineCategory" ADD CONSTRAINT "RoutineCategory_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Routine" ADD CONSTRAINT "Routine_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Routine" ADD CONSTRAINT "Routine_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Routine" ADD CONSTRAINT "Routine_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Routine" ADD CONSTRAINT "Routine_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "RoutineCategory"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Routine" ADD CONSTRAINT "Routine_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTask" ADD CONSTRAINT "RoutineTask_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTask" ADD CONSTRAINT "RoutineTask_routineId_fkey" FOREIGN KEY ("routineId") REFERENCES "Routine"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTask" ADD CONSTRAINT "RoutineTask_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTask" ADD CONSTRAINT "RoutineTask_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "RoutineCategory"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTask" ADD CONSTRAINT "RoutineTask_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "Lead"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTask" ADD CONSTRAINT "RoutineTask_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTask" ADD CONSTRAINT "RoutineTask_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTask" ADD CONSTRAINT "RoutineTask_dealId_fkey" FOREIGN KEY ("dealId") REFERENCES "Deal"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTask" ADD CONSTRAINT "RoutineTask_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "Invoice"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTask" ADD CONSTRAINT "RoutineTask_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "Activity"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTask" ADD CONSTRAINT "RoutineTask_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTaskEvent" ADD CONSTRAINT "RoutineTaskEvent_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTaskEvent" ADD CONSTRAINT "RoutineTaskEvent_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "RoutineTask"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineTaskEvent" ADD CONSTRAINT "RoutineTaskEvent_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineUserSettings" ADD CONSTRAINT "RoutineUserSettings_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RoutineUserSettings" ADD CONSTRAINT "RoutineUserSettings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
|
||||||
|
-- Enable the new routines module for organizations with an explicit module list
|
||||||
|
UPDATE "Organization"
|
||||||
|
SET "enabledModules" = array_append("enabledModules", 'routines')
|
||||||
|
WHERE cardinality("enabledModules") > 0 AND NOT ('routines' = ANY("enabledModules"));
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Rename the default sales pipeline: «کاریز فروش» → «مسیر فروش»
|
||||||
|
UPDATE "Pipeline" SET "name" = 'مسیر فروش' WHERE "name" = 'کاریز فروش';
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ActivityStage" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"position" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"color" TEXT NOT NULL DEFAULT 'green',
|
||||||
|
"kind" "ActivityStatus" NOT NULL DEFAULT 'open',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ActivityStage_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Activity" ADD COLUMN "stageId" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ActivityStage_organizationId_name_key" ON "ActivityStage"("organizationId", "name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ActivityStage_organizationId_position_idx" ON "ActivityStage"("organizationId", "position");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Activity_stageId_idx" ON "Activity"("stageId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ActivityStage" ADD CONSTRAINT "ActivityStage_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Activity" ADD CONSTRAINT "Activity_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "ActivityStage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Activity" ADD COLUMN "fields" JSONB,
|
||||||
|
ADD COLUMN "participantIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||||
|
ADD COLUMN "typeDefId" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ActivityTypeDef" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"baseType" "ActivityType" NOT NULL DEFAULT 'task',
|
||||||
|
"icon" TEXT NOT NULL DEFAULT 'check',
|
||||||
|
"color" TEXT NOT NULL DEFAULT 'green',
|
||||||
|
"fields" JSONB NOT NULL DEFAULT '[]',
|
||||||
|
"position" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"isSystem" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ActivityTypeDef_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ActivityTypeDef_organizationId_position_idx" ON "ActivityTypeDef"("organizationId", "position");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ActivityTypeDef_organizationId_name_key" ON "ActivityTypeDef"("organizationId", "name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Activity_typeDefId_idx" ON "Activity"("typeDefId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ActivityTypeDef" ADD CONSTRAINT "ActivityTypeDef_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Activity" ADD CONSTRAINT "Activity_typeDefId_fkey" FOREIGN KEY ("typeDefId") REFERENCES "ActivityTypeDef"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "GamificationSettings" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"levels" JSONB NOT NULL DEFAULT '[]',
|
||||||
|
"monthlyGoal" INTEGER NOT NULL DEFAULT 1500,
|
||||||
|
"showFullLeaderboard" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"enabledAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "GamificationSettings_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "GamificationRule" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"event" TEXT NOT NULL,
|
||||||
|
"score" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"coins" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"conditions" JSONB NOT NULL DEFAULT '[]',
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"maxPerDay" INTEGER,
|
||||||
|
"maxCoinsPerDay" INTEGER,
|
||||||
|
"maxPerUser" INTEGER,
|
||||||
|
"cooldownMinutes" INTEGER,
|
||||||
|
"oncePerEntity" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"firstTimeOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"requiresApproval" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"roles" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||||
|
"teamIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||||
|
"position" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "GamificationRule_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "GamificationTransaction" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"kind" TEXT NOT NULL,
|
||||||
|
"event" TEXT,
|
||||||
|
"ruleId" TEXT,
|
||||||
|
"bonusId" TEXT,
|
||||||
|
"badgeId" TEXT,
|
||||||
|
"score" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"coins" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'approved',
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"entityType" TEXT,
|
||||||
|
"entityId" TEXT,
|
||||||
|
"entityTitle" TEXT,
|
||||||
|
"idempotencyKey" TEXT,
|
||||||
|
"reason" TEXT,
|
||||||
|
"createdById" TEXT,
|
||||||
|
"decidedById" TEXT,
|
||||||
|
"decidedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "GamificationTransaction_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "GamificationWallet" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"score" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"coins" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"coinsEarned" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"bestMonthScore" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "GamificationWallet_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "GamificationReward" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"cost" INTEGER NOT NULL,
|
||||||
|
"icon" TEXT NOT NULL DEFAULT 'gift',
|
||||||
|
"color" TEXT NOT NULL DEFAULT '#14b8a6',
|
||||||
|
"stock" INTEGER,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"requiresApproval" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"position" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "GamificationReward_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "GamificationRedemption" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"rewardId" TEXT NOT NULL,
|
||||||
|
"cost" INTEGER NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
"note" TEXT,
|
||||||
|
"decisionNote" TEXT,
|
||||||
|
"decidedById" TEXT,
|
||||||
|
"decidedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "GamificationRedemption_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "GamificationBadge" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"icon" TEXT NOT NULL DEFAULT 'medal',
|
||||||
|
"color" TEXT NOT NULL DEFAULT '#f59e0b',
|
||||||
|
"metric" TEXT NOT NULL,
|
||||||
|
"event" TEXT,
|
||||||
|
"threshold" INTEGER NOT NULL,
|
||||||
|
"coins" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"position" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "GamificationBadge_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "GamificationUserBadge" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"badgeId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"earnedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "GamificationUserBadge_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "GamificationBonus" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"kind" TEXT NOT NULL,
|
||||||
|
"event" TEXT,
|
||||||
|
"threshold" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"period" TEXT NOT NULL DEFAULT 'month',
|
||||||
|
"score" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"coins" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "GamificationBonus_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "GamificationSettings_organizationId_key" ON "GamificationSettings"("organizationId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationRule_organizationId_event_isActive_idx" ON "GamificationRule"("organizationId", "event", "isActive");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationTransaction_organizationId_createdAt_idx" ON "GamificationTransaction"("organizationId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationTransaction_organizationId_userId_createdAt_idx" ON "GamificationTransaction"("organizationId", "userId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationTransaction_organizationId_ruleId_userId_create_idx" ON "GamificationTransaction"("organizationId", "ruleId", "userId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationTransaction_organizationId_status_idx" ON "GamificationTransaction"("organizationId", "status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "GamificationTransaction_organizationId_idempotencyKey_key" ON "GamificationTransaction"("organizationId", "idempotencyKey");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationWallet_organizationId_score_idx" ON "GamificationWallet"("organizationId", "score");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "GamificationWallet_organizationId_userId_key" ON "GamificationWallet"("organizationId", "userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationReward_organizationId_isActive_idx" ON "GamificationReward"("organizationId", "isActive");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationRedemption_organizationId_status_idx" ON "GamificationRedemption"("organizationId", "status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationRedemption_organizationId_userId_idx" ON "GamificationRedemption"("organizationId", "userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationBadge_organizationId_isActive_idx" ON "GamificationBadge"("organizationId", "isActive");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationUserBadge_organizationId_userId_idx" ON "GamificationUserBadge"("organizationId", "userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "GamificationUserBadge_badgeId_userId_key" ON "GamificationUserBadge"("badgeId", "userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GamificationBonus_organizationId_isActive_idx" ON "GamificationBonus"("organizationId", "isActive");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GamificationSettings" ADD CONSTRAINT "GamificationSettings_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GamificationRule" ADD CONSTRAINT "GamificationRule_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GamificationTransaction" ADD CONSTRAINT "GamificationTransaction_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GamificationWallet" ADD CONSTRAINT "GamificationWallet_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GamificationReward" ADD CONSTRAINT "GamificationReward_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GamificationRedemption" ADD CONSTRAINT "GamificationRedemption_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GamificationRedemption" ADD CONSTRAINT "GamificationRedemption_rewardId_fkey" FOREIGN KEY ("rewardId") REFERENCES "GamificationReward"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GamificationBadge" ADD CONSTRAINT "GamificationBadge_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GamificationUserBadge" ADD CONSTRAINT "GamificationUserBadge_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GamificationUserBadge" ADD CONSTRAINT "GamificationUserBadge_badgeId_fkey" FOREIGN KEY ("badgeId") REFERENCES "GamificationBadge"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "GamificationBonus" ADD CONSTRAINT "GamificationBonus_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Enable the new module for orgs with an explicit module list
|
||||||
|
UPDATE "Organization"
|
||||||
|
SET "enabledModules" = array_append("enabledModules", 'gamification')
|
||||||
|
WHERE cardinality("enabledModules") > 0 AND NOT ('gamification' = ANY("enabledModules"));
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Activity" ADD COLUMN "leadId" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "AutomationCursor" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"ruleId" TEXT NOT NULL,
|
||||||
|
"stepId" TEXT NOT NULL,
|
||||||
|
"counter" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "AutomationCursor_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Activity_leadId_idx" ON "Activity"("leadId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "AutomationCursor_ruleId_stepId_key" ON "AutomationCursor"("ruleId", "stepId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Activity" ADD CONSTRAINT "Activity_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "Lead"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AutomationCursor" ADD CONSTRAINT "AutomationCursor_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "AutomationRule"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Person" ADD COLUMN "birthDate" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Lead" ADD COLUMN "birthDate" TIMESTAMP(3);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
ALTER TABLE "Organization" ADD COLUMN "deletionRetention" TEXT NOT NULL DEFAULT 'never';
|
||||||
|
|
||||||
|
CREATE TABLE "DeletionArchive" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"model" TEXT NOT NULL,
|
||||||
|
"entityId" TEXT NOT NULL,
|
||||||
|
"label" TEXT NOT NULL,
|
||||||
|
"snapshot" JSONB NOT NULL,
|
||||||
|
"deletedById" TEXT,
|
||||||
|
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "DeletionArchive_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "DeletionArchive_organizationId_deletedAt_idx" ON "DeletionArchive"("organizationId", "deletedAt");
|
||||||
|
CREATE INDEX "DeletionArchive_organizationId_model_idx" ON "DeletionArchive"("organizationId", "model");
|
||||||
|
|
||||||
|
ALTER TABLE "DeletionArchive" ADD CONSTRAINT "DeletionArchive_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "DeletionArchive" ADD CONSTRAINT "DeletionArchive_deletedById_fkey" FOREIGN KEY ("deletedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- بایگانی اطلاعات قبل: مرز محیط، مجوز عضو، و منبع فعالیت
|
||||||
|
ALTER TABLE "Organization" ADD COLUMN "priorArchiveEnabledAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
ALTER TABLE "Membership" ADD COLUMN "priorArchiveEnabled" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
ALTER TABLE "Membership" ADD COLUMN "priorArchiveLens" TEXT NOT NULL DEFAULT 'current';
|
||||||
|
|
||||||
|
ALTER TABLE "Activity" ADD COLUMN "importSource" TEXT NOT NULL DEFAULT 'manual';
|
||||||
|
|
||||||
|
CREATE INDEX "Activity_organizationId_importSource_idx" ON "Activity"("organizationId", "importSource");
|
||||||
|
|
||||||
|
-- فعالیت وصل به معاملهٔ دیدار از همین حالا قبلی است. بقیه بعد از روشن شدن ماژول و با تاریخ مرز پر میشوند.
|
||||||
|
UPDATE "Activity" AS a
|
||||||
|
SET "importSource" = 'didar'
|
||||||
|
WHERE a."importSource" = 'manual'
|
||||||
|
AND a."dealId" IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM "Deal" AS d
|
||||||
|
WHERE d."id" = a."dealId" AND d."importSource" = 'didar'
|
||||||
|
);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Prior archive: promote tag + cutoff query indexes
|
||||||
|
ALTER TABLE "Deal" ADD COLUMN "promotedFromPriorAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
ALTER TABLE "Activity" ADD COLUMN "promotedFromPriorAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
CREATE INDEX "Deal_organizationId_createdAt_idx" ON "Deal"("organizationId", "createdAt");
|
||||||
|
|
||||||
|
CREATE INDEX "Activity_organizationId_createdAt_idx" ON "Activity"("organizationId", "createdAt");
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "OutboundSmsLog" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"personId" TEXT,
|
||||||
|
"leadId" TEXT,
|
||||||
|
"mobile" TEXT NOT NULL,
|
||||||
|
"body" TEXT NOT NULL,
|
||||||
|
"source" TEXT NOT NULL,
|
||||||
|
"sourceId" TEXT,
|
||||||
|
"status" TEXT NOT NULL,
|
||||||
|
"error" TEXT,
|
||||||
|
"actorUserId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "OutboundSmsLog_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "OutboundSmsLog_organizationId_personId_createdAt_idx" ON "OutboundSmsLog"("organizationId", "personId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "OutboundSmsLog_organizationId_leadId_createdAt_idx" ON "OutboundSmsLog"("organizationId", "leadId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "OutboundSmsLog_organizationId_createdAt_idx" ON "OutboundSmsLog"("organizationId", "createdAt");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "OutboundSmsLog" ADD CONSTRAINT "OutboundSmsLog_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "OutboundSmsLog" ADD CONSTRAINT "OutboundSmsLog_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "OutboundSmsLog" ADD CONSTRAINT "OutboundSmsLog_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "Lead"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "FinancialAccount" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"bankName" TEXT,
|
||||||
|
"accountRef" TEXT,
|
||||||
|
"responsibleName" TEXT,
|
||||||
|
"openingBalance" DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||||
|
"openingDate" TIMESTAMP(3) NOT NULL,
|
||||||
|
"notes" TEXT,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"currency" TEXT NOT NULL DEFAULT 'IRR',
|
||||||
|
"createdById" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "FinancialAccount_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "FinancialCategory" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"kind" TEXT NOT NULL,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "FinancialCategory_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "FinancialDocument" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"kind" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"amount" DECIMAL(18,2) NOT NULL,
|
||||||
|
"bookedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"dueAt" TIMESTAMP(3),
|
||||||
|
"categoryId" TEXT,
|
||||||
|
"personId" TEXT,
|
||||||
|
"partyName" TEXT,
|
||||||
|
"accountId" TEXT,
|
||||||
|
"method" TEXT,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'open',
|
||||||
|
"reference" TEXT,
|
||||||
|
"notes" TEXT,
|
||||||
|
"createdById" TEXT,
|
||||||
|
"voidedAt" TIMESTAMP(3),
|
||||||
|
"voidReason" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "FinancialDocument_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "FinancialMovement" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"accountId" TEXT NOT NULL,
|
||||||
|
"documentId" TEXT,
|
||||||
|
"direction" TEXT NOT NULL,
|
||||||
|
"amount" DECIMAL(18,2) NOT NULL,
|
||||||
|
"kind" TEXT NOT NULL,
|
||||||
|
"transferGroupId" TEXT,
|
||||||
|
"occurredAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"memo" TEXT,
|
||||||
|
"reference" TEXT,
|
||||||
|
"createdById" TEXT,
|
||||||
|
"voidedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "FinancialMovement_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "FinancialAccount_organizationId_isActive_idx" ON "FinancialAccount"("organizationId", "isActive");
|
||||||
|
CREATE UNIQUE INDEX "FinancialCategory_organizationId_kind_name_key" ON "FinancialCategory"("organizationId", "kind", "name");
|
||||||
|
CREATE INDEX "FinancialCategory_organizationId_kind_idx" ON "FinancialCategory"("organizationId", "kind");
|
||||||
|
CREATE INDEX "FinancialDocument_organizationId_kind_bookedAt_idx" ON "FinancialDocument"("organizationId", "kind", "bookedAt");
|
||||||
|
CREATE INDEX "FinancialDocument_organizationId_status_idx" ON "FinancialDocument"("organizationId", "status");
|
||||||
|
CREATE INDEX "FinancialDocument_personId_idx" ON "FinancialDocument"("personId");
|
||||||
|
CREATE INDEX "FinancialMovement_organizationId_accountId_occurredAt_idx" ON "FinancialMovement"("organizationId", "accountId", "occurredAt");
|
||||||
|
CREATE INDEX "FinancialMovement_transferGroupId_idx" ON "FinancialMovement"("transferGroupId");
|
||||||
|
CREATE INDEX "FinancialMovement_documentId_idx" ON "FinancialMovement"("documentId");
|
||||||
|
|
||||||
|
ALTER TABLE "FinancialAccount" ADD CONSTRAINT "FinancialAccount_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialCategory" ADD CONSTRAINT "FinancialCategory_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialDocument" ADD CONSTRAINT "FinancialDocument_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialDocument" ADD CONSTRAINT "FinancialDocument_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "FinancialCategory"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialDocument" ADD CONSTRAINT "FinancialDocument_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialDocument" ADD CONSTRAINT "FinancialDocument_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "FinancialAccount"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialMovement" ADD CONSTRAINT "FinancialMovement_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialMovement" ADD CONSTRAINT "FinancialMovement_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "FinancialAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialMovement" ADD CONSTRAINT "FinancialMovement_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "FinancialDocument"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Person" ADD COLUMN "discLabel" TEXT,
|
||||||
|
ADD COLUMN "discReliable" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "discAssessedAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Person_organizationId_discLabel_idx" ON "Person"("organizationId", "discLabel");
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "DiscAssessment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"personId" TEXT NOT NULL,
|
||||||
|
"assessorId" TEXT NOT NULL,
|
||||||
|
"answers" JSONB NOT NULL,
|
||||||
|
"scoreD" INTEGER NOT NULL,
|
||||||
|
"scoreI" INTEGER NOT NULL,
|
||||||
|
"scoreS" INTEGER NOT NULL,
|
||||||
|
"scoreC" INTEGER NOT NULL,
|
||||||
|
"percentD" INTEGER NOT NULL,
|
||||||
|
"percentI" INTEGER NOT NULL,
|
||||||
|
"percentS" INTEGER NOT NULL,
|
||||||
|
"percentC" INTEGER NOT NULL,
|
||||||
|
"validCount" INTEGER NOT NULL,
|
||||||
|
"primaryTypes" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||||
|
"secondaryTypes" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||||
|
"label" TEXT NOT NULL,
|
||||||
|
"secondaryLabel" TEXT,
|
||||||
|
"reliable" BOOLEAN NOT NULL,
|
||||||
|
"confidence" TEXT NOT NULL,
|
||||||
|
"note" TEXT,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'confirmed',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "DiscAssessment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SalesCopilotSession" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"personId" TEXT,
|
||||||
|
"leadId" TEXT,
|
||||||
|
"dealId" TEXT,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"topic" TEXT NOT NULL,
|
||||||
|
"goal" TEXT NOT NULL,
|
||||||
|
"customerStatement" TEXT NOT NULL,
|
||||||
|
"behaviorNotes" TEXT,
|
||||||
|
"analysis" JSONB NOT NULL,
|
||||||
|
"confirmedResponse" TEXT,
|
||||||
|
"nextAction" TEXT,
|
||||||
|
"followUpAt" TIMESTAMP(3),
|
||||||
|
"callOutcome" TEXT,
|
||||||
|
"agentNote" TEXT,
|
||||||
|
"status" TEXT NOT NULL,
|
||||||
|
"messageDraft" TEXT,
|
||||||
|
"activityId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "SalesCopilotSession_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "DiscAssessment_organizationId_personId_createdAt_idx" ON "DiscAssessment"("organizationId", "personId", "createdAt");
|
||||||
|
CREATE INDEX "DiscAssessment_personId_idx" ON "DiscAssessment"("personId");
|
||||||
|
CREATE INDEX "SalesCopilotSession_organizationId_personId_createdAt_idx" ON "SalesCopilotSession"("organizationId", "personId", "createdAt");
|
||||||
|
CREATE INDEX "SalesCopilotSession_organizationId_leadId_idx" ON "SalesCopilotSession"("organizationId", "leadId");
|
||||||
|
CREATE INDEX "SalesCopilotSession_dealId_idx" ON "SalesCopilotSession"("dealId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DiscAssessment" ADD CONSTRAINT "DiscAssessment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "DiscAssessment" ADD CONSTRAINT "DiscAssessment_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "DiscAssessment" ADD CONSTRAINT "DiscAssessment_assessorId_fkey" FOREIGN KEY ("assessorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "SalesCopilotSession" ADD CONSTRAINT "SalesCopilotSession_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "SalesCopilotSession" ADD CONSTRAINT "SalesCopilotSession_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "SalesCopilotSession" ADD CONSTRAINT "SalesCopilotSession_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "Lead"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "SalesCopilotSession" ADD CONSTRAINT "SalesCopilotSession_dealId_fkey" FOREIGN KEY ("dealId") REFERENCES "Deal"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "SalesCopilotSession" ADD CONSTRAINT "SalesCopilotSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- ماژول برای محیطهایی که از قبل ماژول دارند روشن میشود و در سوپرادمین قابل خاموش کردن است
|
||||||
|
UPDATE "Organization"
|
||||||
|
SET "enabledModules" = array_append("enabledModules", 'behavior')
|
||||||
|
WHERE cardinality("enabledModules") > 0
|
||||||
|
AND NOT ('behavior' = ANY("enabledModules"));
|
||||||
|
|
||||||
|
-- مجوز رفتار برای نقشهایی که فروش دارند؛ پشتیبانی بدون این کلید میماند
|
||||||
|
UPDATE "OrgRole"
|
||||||
|
SET "permissions" = array_append("permissions", 'behavior')
|
||||||
|
WHERE NOT ('behavior' = ANY("permissions"))
|
||||||
|
AND (
|
||||||
|
"systemKey" IN ('owner', 'admin', 'sales')
|
||||||
|
OR 'sales' = ANY("permissions")
|
||||||
|
);
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
ALTER TABLE "FinancialMovement" ADD COLUMN "receivableId" TEXT;
|
||||||
|
ALTER TABLE "FinancialMovement" ADD COLUMN "installmentId" TEXT;
|
||||||
|
|
||||||
|
CREATE TABLE "FinancialReceivable" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"personId" TEXT,
|
||||||
|
"partyName" TEXT,
|
||||||
|
"totalAmount" DECIMAL(18,2) NOT NULL,
|
||||||
|
"notes" TEXT,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'open',
|
||||||
|
"createdById" TEXT,
|
||||||
|
"voidedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
CONSTRAINT "FinancialReceivable_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "FinancialInstallment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"receivableId" TEXT NOT NULL,
|
||||||
|
"position" INTEGER NOT NULL,
|
||||||
|
"dueDate" TIMESTAMP(3) NOT NULL,
|
||||||
|
"amount" DECIMAL(18,2) NOT NULL,
|
||||||
|
"reminderNotifiedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "FinancialInstallment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "FinancialMovement_receivableId_idx" ON "FinancialMovement"("receivableId");
|
||||||
|
CREATE INDEX "FinancialMovement_installmentId_idx" ON "FinancialMovement"("installmentId");
|
||||||
|
CREATE INDEX "FinancialReceivable_organizationId_status_idx" ON "FinancialReceivable"("organizationId", "status");
|
||||||
|
CREATE INDEX "FinancialReceivable_personId_idx" ON "FinancialReceivable"("personId");
|
||||||
|
CREATE UNIQUE INDEX "FinancialInstallment_receivableId_position_key" ON "FinancialInstallment"("receivableId", "position");
|
||||||
|
CREATE INDEX "FinancialInstallment_dueDate_idx" ON "FinancialInstallment"("dueDate");
|
||||||
|
|
||||||
|
ALTER TABLE "FinancialReceivable" ADD CONSTRAINT "FinancialReceivable_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialReceivable" ADD CONSTRAINT "FinancialReceivable_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialInstallment" ADD CONSTRAINT "FinancialInstallment_receivableId_fkey" FOREIGN KEY ("receivableId") REFERENCES "FinancialReceivable"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialMovement" ADD CONSTRAINT "FinancialMovement_receivableId_fkey" FOREIGN KEY ("receivableId") REFERENCES "FinancialReceivable"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialMovement" ADD CONSTRAINT "FinancialMovement_installmentId_fkey" FOREIGN KEY ("installmentId") REFERENCES "FinancialInstallment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
ALTER TABLE "FinancialMovement" ADD COLUMN "payrollId" TEXT;
|
||||||
|
CREATE INDEX "FinancialMovement_payrollId_idx" ON "FinancialMovement"("payrollId");
|
||||||
|
|
||||||
|
CREATE TABLE "FinancialPayroll" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"period" TEXT NOT NULL,
|
||||||
|
"baseAmount" DECIMAL(18,2) NOT NULL,
|
||||||
|
"benefits" DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||||
|
"bonus" DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||||
|
"deductions" DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||||
|
"payable" DECIMAL(18,2) NOT NULL,
|
||||||
|
"dueDate" TIMESTAMP(3) NOT NULL,
|
||||||
|
"notes" TEXT,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'open',
|
||||||
|
"createdById" TEXT,
|
||||||
|
"voidedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
CONSTRAINT "FinancialPayroll_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "FinancialRecurringItem" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"kind" TEXT NOT NULL,
|
||||||
|
"amount" DECIMAL(18,2) NOT NULL,
|
||||||
|
"interval" TEXT NOT NULL,
|
||||||
|
"startDate" TIMESTAMP(3) NOT NULL,
|
||||||
|
"endDate" TIMESTAMP(3),
|
||||||
|
"nextDue" TIMESTAMP(3) NOT NULL,
|
||||||
|
"accountId" TEXT,
|
||||||
|
"categoryId" TEXT,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdById" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
CONSTRAINT "FinancialRecurringItem_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "FinancialRecurringOccurrence" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"itemId" TEXT NOT NULL,
|
||||||
|
"periodKey" TEXT NOT NULL,
|
||||||
|
"dueDate" TIMESTAMP(3) NOT NULL,
|
||||||
|
"documentId" TEXT,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "FinancialRecurringOccurrence_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "FinancialReconciliation" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"accountId" TEXT NOT NULL,
|
||||||
|
"reconciledAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"statedBalance" DECIMAL(18,2) NOT NULL,
|
||||||
|
"systemBalance" DECIMAL(18,2) NOT NULL,
|
||||||
|
"difference" DECIMAL(18,2) NOT NULL,
|
||||||
|
"notes" TEXT,
|
||||||
|
"createdById" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "FinancialReconciliation_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "FinancialPayroll_organizationId_userId_period_key" ON "FinancialPayroll"("organizationId", "userId", "period");
|
||||||
|
CREATE INDEX "FinancialPayroll_organizationId_status_idx" ON "FinancialPayroll"("organizationId", "status");
|
||||||
|
CREATE INDEX "FinancialRecurringItem_organizationId_isActive_nextDue_idx" ON "FinancialRecurringItem"("organizationId", "isActive", "nextDue");
|
||||||
|
CREATE UNIQUE INDEX "FinancialRecurringOccurrence_itemId_periodKey_key" ON "FinancialRecurringOccurrence"("itemId", "periodKey");
|
||||||
|
CREATE INDEX "FinancialReconciliation_organizationId_accountId_reconciledAt_idx" ON "FinancialReconciliation"("organizationId", "accountId", "reconciledAt");
|
||||||
|
|
||||||
|
ALTER TABLE "FinancialPayroll" ADD CONSTRAINT "FinancialPayroll_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialMovement" ADD CONSTRAINT "FinancialMovement_payrollId_fkey" FOREIGN KEY ("payrollId") REFERENCES "FinancialPayroll"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialRecurringItem" ADD CONSTRAINT "FinancialRecurringItem_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialRecurringOccurrence" ADD CONSTRAINT "FinancialRecurringOccurrence_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "FinancialRecurringItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialReconciliation" ADD CONSTRAINT "FinancialReconciliation_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialReconciliation" ADD CONSTRAINT "FinancialReconciliation_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "FinancialAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
ALTER TABLE "Payment" ADD COLUMN "financialAccountId" TEXT;
|
||||||
|
ALTER TABLE "Cheque" ADD COLUMN "financialAccountId" TEXT;
|
||||||
|
ALTER TABLE "FinancialMovement" ADD COLUMN "paymentId" TEXT;
|
||||||
|
ALTER TABLE "FinancialMovement" ADD COLUMN "chequeId" TEXT;
|
||||||
|
CREATE INDEX "FinancialMovement_paymentId_idx" ON "FinancialMovement"("paymentId");
|
||||||
|
CREATE INDEX "FinancialMovement_chequeId_idx" ON "FinancialMovement"("chequeId");
|
||||||
|
|
||||||
|
CREATE TABLE "FinancialAuditLog" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"entityType" TEXT NOT NULL,
|
||||||
|
"entityId" TEXT NOT NULL,
|
||||||
|
"action" TEXT NOT NULL,
|
||||||
|
"before" JSONB,
|
||||||
|
"after" JSONB,
|
||||||
|
"actorUserId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "FinancialAuditLog_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX "FinancialAuditLog_organizationId_entityType_entityId_createdAt_idx" ON "FinancialAuditLog"("organizationId", "entityType", "entityId", "createdAt");
|
||||||
|
|
||||||
|
CREATE TABLE "FinancialAttachment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"documentId" TEXT NOT NULL,
|
||||||
|
"fileName" TEXT NOT NULL,
|
||||||
|
"mimeType" TEXT NOT NULL,
|
||||||
|
"storageKey" TEXT NOT NULL,
|
||||||
|
"createdById" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "FinancialAttachment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
CREATE INDEX "FinancialAttachment_documentId_idx" ON "FinancialAttachment"("documentId");
|
||||||
|
|
||||||
|
ALTER TABLE "FinancialAuditLog" ADD CONSTRAINT "FinancialAuditLog_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialAttachment" ADD CONSTRAINT "FinancialAttachment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialAttachment" ADD CONSTRAINT "FinancialAttachment_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "FinancialDocument"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Membership" ADD COLUMN "logoutTime" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "AttendanceEvent" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"kind" TEXT NOT NULL,
|
||||||
|
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"afterHours" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"sessionStartedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "AttendanceEvent_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AttendanceEvent_organizationId_at_idx" ON "AttendanceEvent"("organizationId", "at");
|
||||||
|
CREATE INDEX "AttendanceEvent_organizationId_userId_at_idx" ON "AttendanceEvent"("organizationId", "userId", "at");
|
||||||
|
CREATE UNIQUE INDEX "AttendanceEvent_organizationId_userId_kind_sessionStartedAt_key" ON "AttendanceEvent"("organizationId", "userId", "kind", "sessionStartedAt");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AttendanceEvent" ADD CONSTRAINT "AttendanceEvent_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "AttendanceEvent" ADD CONSTRAINT "AttendanceEvent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SmsAccount" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"balanceRials" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"pricePerPartRials" INTEGER NOT NULL DEFAULT 2500,
|
||||||
|
"isBlocked" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"autoBlockBelowRials" INTEGER,
|
||||||
|
"alertCriticalRials" INTEGER NOT NULL DEFAULT 10000,
|
||||||
|
"alertWarningRials" INTEGER NOT NULL DEFAULT 100000,
|
||||||
|
"panelMode" TEXT NOT NULL DEFAULT 'platform',
|
||||||
|
"ownPanelType" TEXT NOT NULL DEFAULT '0098',
|
||||||
|
"ownPanelUsername" TEXT,
|
||||||
|
"ownPanelPassword" TEXT,
|
||||||
|
"ownPanelFrom" TEXT,
|
||||||
|
"chargeCard" TEXT,
|
||||||
|
"chargeIban" TEXT,
|
||||||
|
"chargeHolder" TEXT,
|
||||||
|
"chargeInstruction" TEXT,
|
||||||
|
"alertDaysSent" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"lastAlertDate" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "SmsAccount_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SmsLedger" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"smsAccountId" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"amountRials" INTEGER NOT NULL,
|
||||||
|
"balanceAfterRials" INTEGER NOT NULL,
|
||||||
|
"parts" INTEGER,
|
||||||
|
"pricePerPartRials" INTEGER,
|
||||||
|
"note" TEXT,
|
||||||
|
"smsSendLogId" TEXT,
|
||||||
|
"createdById" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "SmsLedger_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SmsBillingSendLog" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"smsAccountId" TEXT NOT NULL,
|
||||||
|
"receiver" TEXT NOT NULL,
|
||||||
|
"body" TEXT NOT NULL,
|
||||||
|
"parts" INTEGER NOT NULL,
|
||||||
|
"pricePerPartRials" INTEGER NOT NULL,
|
||||||
|
"totalAmountRials" INTEGER NOT NULL,
|
||||||
|
"provider" TEXT,
|
||||||
|
"source" TEXT,
|
||||||
|
"isAlert" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "SmsBillingSendLog_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "SmsAccount_organizationId_key" ON "SmsAccount"("organizationId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "SmsAccount_isBlocked_idx" ON "SmsAccount"("isBlocked");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "SmsAccount_balanceRials_idx" ON "SmsAccount"("balanceRials");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "SmsLedger_smsAccountId_createdAt_idx" ON "SmsLedger"("smsAccountId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "SmsLedger_type_idx" ON "SmsLedger"("type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "SmsBillingSendLog_smsAccountId_createdAt_idx" ON "SmsBillingSendLog"("smsAccountId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "SmsBillingSendLog_source_idx" ON "SmsBillingSendLog"("source");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "SmsAccount" ADD CONSTRAINT "SmsAccount_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "SmsLedger" ADD CONSTRAINT "SmsLedger_smsAccountId_fkey" FOREIGN KEY ("smsAccountId") REFERENCES "SmsAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "SmsBillingSendLog" ADD CONSTRAINT "SmsBillingSendLog_smsAccountId_fkey" FOREIGN KEY ("smsAccountId") REFERENCES "SmsAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ActivityBoard" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"position" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ActivityBoard_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ActivityBoard_organizationId_name_key" ON "ActivityBoard"("organizationId", "name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ActivityBoard_organizationId_position_idx" ON "ActivityBoard"("organizationId", "position");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ActivityBoard" ADD CONSTRAINT "ActivityBoard_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Backfill: one default board per organization that already has activity stages
|
||||||
|
INSERT INTO "ActivityBoard" ("id", "organizationId", "name", "isDefault", "position", "createdAt", "updatedAt")
|
||||||
|
SELECT 'actb_' || md5(s."organizationId" || clock_timestamp()::text), s."organizationId", 'کارهای اصلی', true, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||||
|
FROM (SELECT DISTINCT "organizationId" FROM "ActivityStage") AS s;
|
||||||
|
|
||||||
|
-- AlterTable: stages belong to a board
|
||||||
|
ALTER TABLE "ActivityStage" ADD COLUMN "boardId" TEXT;
|
||||||
|
|
||||||
|
UPDATE "ActivityStage" AS st
|
||||||
|
SET "boardId" = b."id"
|
||||||
|
FROM "ActivityBoard" AS b
|
||||||
|
WHERE b."organizationId" = st."organizationId" AND b."isDefault" = true;
|
||||||
|
|
||||||
|
ALTER TABLE "ActivityStage" ALTER COLUMN "boardId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- Stage names are unique per board (not per organization)
|
||||||
|
DROP INDEX "ActivityStage_organizationId_name_key";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ActivityStage_boardId_name_key" ON "ActivityStage"("boardId", "name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ActivityStage_boardId_position_idx" ON "ActivityStage"("boardId", "position");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ActivityStage" ADD CONSTRAINT "ActivityStage_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "ActivityBoard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AlterTable: activities optionally point at a board (null = default board)
|
||||||
|
ALTER TABLE "Activity" ADD COLUMN "boardId" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Activity_boardId_idx" ON "Activity"("boardId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Activity" ADD CONSTRAINT "Activity_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "ActivityBoard"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- تیکهای مجوز نقش از این نسخه واقعاً اعمال میشوند.
|
||||||
|
-- تا امروز همهٔ نقشها عملاً به این بخشها دسترسی داشتند؛ همان دسترسی را برای نقشهای موجود حفظ میکنیم.
|
||||||
|
UPDATE "OrgRole"
|
||||||
|
SET "permissions" = ARRAY(
|
||||||
|
SELECT DISTINCT p
|
||||||
|
FROM unnest(
|
||||||
|
"permissions" || ARRAY['customers', 'leads', 'sales', 'billing', 'products', 'activities', 'notes', 'reports']
|
||||||
|
) AS p
|
||||||
|
);
|
||||||
|
|
||||||
|
-- لاگ فعالیت کاربران تا امروز فقط برای مالک و مدیر باز بود (مستقل از تیک)؛ همان را حفظ میکنیم.
|
||||||
|
UPDATE "OrgRole"
|
||||||
|
SET "permissions" = array_remove("permissions", 'audit')
|
||||||
|
WHERE "systemKey" IS NULL OR "systemKey" NOT IN ('owner', 'admin');
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Membership" ADD COLUMN "blockLoginAfterLogout" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CustomerGroup" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"color" TEXT NOT NULL DEFAULT 'teal',
|
||||||
|
"position" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"tahesabConnectionId" TEXT,
|
||||||
|
"tahesabGid" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "CustomerGroup_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "PersonGroupLink" (
|
||||||
|
"personId" TEXT NOT NULL,
|
||||||
|
"groupId" TEXT NOT NULL,
|
||||||
|
"source" TEXT NOT NULL DEFAULT 'manual',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "PersonGroupLink_pkey" PRIMARY KEY ("personId","groupId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CompanyGroupLink" (
|
||||||
|
"companyId" TEXT NOT NULL,
|
||||||
|
"groupId" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "CompanyGroupLink_pkey" PRIMARY KEY ("companyId","groupId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CustomerGroup_organizationId_name_key" ON "CustomerGroup"("organizationId", "name");
|
||||||
|
CREATE INDEX "CustomerGroup_organizationId_position_idx" ON "CustomerGroup"("organizationId", "position");
|
||||||
|
CREATE INDEX "CustomerGroup_tahesabConnectionId_tahesabGid_idx" ON "CustomerGroup"("tahesabConnectionId", "tahesabGid");
|
||||||
|
CREATE INDEX "PersonGroupLink_groupId_idx" ON "PersonGroupLink"("groupId");
|
||||||
|
CREATE INDEX "CompanyGroupLink_groupId_idx" ON "CompanyGroupLink"("groupId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CustomerGroup" ADD CONSTRAINT "CustomerGroup_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "PersonGroupLink" ADD CONSTRAINT "PersonGroupLink_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "PersonGroupLink" ADD CONSTRAINT "PersonGroupLink_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "CustomerGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "CompanyGroupLink" ADD CONSTRAINT "CompanyGroupLink_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "CompanyGroupLink" ADD CONSTRAINT "CompanyGroupLink_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "CustomerGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- تبدیل دادههای قبلی: «گروه مشتری» اکسل (customFields.group) و «گروهبندی شرکت» (groupName).
|
||||||
|
-- مقدارهای چندتایی با «،» «,» «/» جدا میشوند. برچسبهای «وضعیت مشتری» که دیدار در همان ستون میگذارد گروه حساب نمیشوند.
|
||||||
|
CREATE TEMP TABLE "_cg_src" AS
|
||||||
|
SELECT p."organizationId" AS org, 'person' AS kind, p."id" AS ref, left(btrim(x), 80) AS name
|
||||||
|
FROM "Person" p
|
||||||
|
CROSS JOIN LATERAL regexp_split_to_table(p."customFields"->>'group', '\s*[،,/]\s*') AS x
|
||||||
|
WHERE COALESCE(p."customFields"->>'group', '') <> ''
|
||||||
|
UNION ALL
|
||||||
|
SELECT c."organizationId", 'company', c."id", left(btrim(x), 80)
|
||||||
|
FROM "Company" c
|
||||||
|
CROSS JOIN LATERAL regexp_split_to_table(c."groupName", '\s*[،,/]\s*') AS x
|
||||||
|
WHERE COALESCE(c."groupName", '') <> '';
|
||||||
|
|
||||||
|
DELETE FROM "_cg_src"
|
||||||
|
WHERE name = ''
|
||||||
|
OR name IN ('مشتری تازه وارد', 'مشتری خوشحال', 'مشتری ناراضی', 'مشتری خاص');
|
||||||
|
|
||||||
|
INSERT INTO "CustomerGroup" ("id", "organizationId", "name", "color", "position", "createdAt", "updatedAt")
|
||||||
|
SELECT 'cgrp_' || md5(d.org || '|' || d.name), d.org, d.name, 'teal', 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||||
|
FROM (SELECT DISTINCT org, name FROM "_cg_src") AS d
|
||||||
|
ON CONFLICT ("organizationId", "name") DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO "PersonGroupLink" ("personId", "groupId", "source", "createdAt")
|
||||||
|
SELECT DISTINCT s.ref, g."id", 'manual', CURRENT_TIMESTAMP
|
||||||
|
FROM "_cg_src" s
|
||||||
|
JOIN "CustomerGroup" g ON g."organizationId" = s.org AND g."name" = s.name
|
||||||
|
WHERE s.kind = 'person'
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO "CompanyGroupLink" ("companyId", "groupId", "createdAt")
|
||||||
|
SELECT DISTINCT s.ref, g."id", CURRENT_TIMESTAMP
|
||||||
|
FROM "_cg_src" s
|
||||||
|
JOIN "CustomerGroup" g ON g."organizationId" = s.org AND g."name" = s.name
|
||||||
|
WHERE s.kind = 'company'
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
DROP TABLE "_cg_src";
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- Deep data access: role default + membership override (phase 1: customers)
|
||||||
|
|
||||||
|
ALTER TABLE "OrgRole" ADD COLUMN IF NOT EXISTS "dataAccessPolicy" JSONB NOT NULL DEFAULT '{}';
|
||||||
|
|
||||||
|
ALTER TABLE "Membership" ADD COLUMN IF NOT EXISTS "dataAccessOverride" JSONB NOT NULL DEFAULT '{}';
|
||||||
|
|
||||||
|
-- Migrate legacy own/team/org (+ optional contacts scope) into customers override
|
||||||
|
UPDATE "Membership" m
|
||||||
|
SET "dataAccessOverride" = jsonb_build_object(
|
||||||
|
'customers',
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE(
|
||||||
|
NULLIF(m."dataVisibilityScopes"::jsonb ->> 'contacts', ''),
|
||||||
|
m."dataVisibility"
|
||||||
|
) = 'org' THEN '{"mode":"all","rules":[]}'::jsonb
|
||||||
|
WHEN COALESCE(
|
||||||
|
NULLIF(m."dataVisibilityScopes"::jsonb ->> 'contacts', ''),
|
||||||
|
m."dataVisibility"
|
||||||
|
) = 'team' THEN '{"mode":"rules","rules":[{"owners":["self","team"]}]}'::jsonb
|
||||||
|
ELSE '{"mode":"rules","rules":[{"owners":["self"]}]}'::jsonb
|
||||||
|
END
|
||||||
|
)
|
||||||
|
WHERE m."role" NOT IN ('owner', 'admin')
|
||||||
|
AND (
|
||||||
|
m."dataAccessOverride" IS NULL
|
||||||
|
OR m."dataAccessOverride"::text IN ('{}', 'null')
|
||||||
|
);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Membership" ADD COLUMN "loginOtpOnly" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "AccountingAsnadLine" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"connectionId" TEXT NOT NULL,
|
||||||
|
"personId" TEXT,
|
||||||
|
"linkId" TEXT,
|
||||||
|
"moshtariCode" INTEGER NOT NULL,
|
||||||
|
"lineKey" TEXT NOT NULL,
|
||||||
|
"tahesabId" INTEGER,
|
||||||
|
"factorCode" TEXT,
|
||||||
|
"shFactor" INTEGER,
|
||||||
|
"radif" INTEGER,
|
||||||
|
"tarikh" TEXT NOT NULL,
|
||||||
|
"noRaw" TEXT NOT NULL DEFAULT '',
|
||||||
|
"kind" TEXT NOT NULL DEFAULT 'other',
|
||||||
|
"jensFelez" INTEGER,
|
||||||
|
"noKar" TEXT,
|
||||||
|
"vazn" DOUBLE PRECISION,
|
||||||
|
"tabdilVazn" DOUBLE PRECISION,
|
||||||
|
"mali" DOUBLE PRECISION,
|
||||||
|
"count" DOUBLE PRECISION,
|
||||||
|
"ayar" DOUBLE PRECISION,
|
||||||
|
"mazaneh" DOUBLE PRECISION,
|
||||||
|
"fi" DOUBLE PRECISION,
|
||||||
|
"sharh1" TEXT,
|
||||||
|
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "AccountingAsnadLine_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "AccountingAsnadLine_connectionId_lineKey_key" ON "AccountingAsnadLine"("connectionId", "lineKey");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AccountingAsnadLine_organizationId_connectionId_tarikh_idx" ON "AccountingAsnadLine"("organizationId", "connectionId", "tarikh");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AccountingAsnadLine_organizationId_kind_idx" ON "AccountingAsnadLine"("organizationId", "kind");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AccountingAsnadLine_personId_idx" ON "AccountingAsnadLine"("personId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AccountingAsnadLine_linkId_idx" ON "AccountingAsnadLine"("linkId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AccountingAsnadLine_moshtariCode_idx" ON "AccountingAsnadLine"("moshtariCode");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AccountingAsnadLine" ADD CONSTRAINT "AccountingAsnadLine_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AccountingAsnadLine" ADD CONSTRAINT "AccountingAsnadLine_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "AccountingConnection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AccountingAsnadLine" ADD CONSTRAINT "AccountingAsnadLine_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AccountingAsnadLine" ADD CONSTRAINT "AccountingAsnadLine_linkId_fkey" FOREIGN KEY ("linkId") REFERENCES "PersonAccountingLink"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "TahesabInvoiceSettings" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"logoDataUrl" TEXT,
|
||||||
|
"companyName" TEXT NOT NULL DEFAULT '',
|
||||||
|
"companySubtitle" TEXT NOT NULL DEFAULT '',
|
||||||
|
"address" TEXT NOT NULL DEFAULT '',
|
||||||
|
"phone" TEXT NOT NULL DEFAULT '',
|
||||||
|
"headerText" TEXT NOT NULL DEFAULT '',
|
||||||
|
"footerText" TEXT NOT NULL DEFAULT '',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "TahesabInvoiceSettings_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "TahesabInvoiceSettings_organizationId_key" ON "TahesabInvoiceSettings"("organizationId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TahesabInvoiceSettings" ADD CONSTRAINT "TahesabInvoiceSettings_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
ALTER TABLE "CustomerGroup" ADD COLUMN IF NOT EXISTS "parentId" TEXT;
|
||||||
|
CREATE INDEX IF NOT EXISTS "CustomerGroup_parentId_idx" ON "CustomerGroup"("parentId");
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "CustomerGroup"
|
||||||
|
ADD CONSTRAINT "CustomerGroup_parentId_fkey"
|
||||||
|
FOREIGN KEY ("parentId") REFERENCES "CustomerGroup"("id")
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE "AccountingAsnadLine" ADD COLUMN IF NOT EXISTS "darsad" DOUBLE PRECISION;
|
||||||
|
ALTER TABLE "AccountingAsnadLine" ADD COLUMN IF NOT EXISTS "darsadTalayee" DOUBLE PRECISION;
|
||||||
|
ALTER TABLE "AccountingAsnadLine" ADD COLUMN IF NOT EXISTS "maliatDarsad" DOUBLE PRECISION;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ActivityImage" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"activityId" TEXT NOT NULL,
|
||||||
|
"fileName" TEXT NOT NULL,
|
||||||
|
"mimeType" TEXT NOT NULL,
|
||||||
|
"sizeBytes" INTEGER NOT NULL,
|
||||||
|
"storageKey" TEXT NOT NULL,
|
||||||
|
"uploadedById" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "ActivityImage_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ActivityImage_activityId_createdAt_idx" ON "ActivityImage"("activityId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ActivityImage_organizationId_idx" ON "ActivityImage"("organizationId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ActivityImage_uploadedById_idx" ON "ActivityImage"("uploadedById");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ActivityImage" ADD CONSTRAINT "ActivityImage_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ActivityImage" ADD CONSTRAINT "ActivityImage_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "Activity"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ActivityImage" ADD CONSTRAINT "ActivityImage_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- پیامک پایان اشتراک فقط برای انقضاهای بعدی؛ حسابهای قبلاً تمامشده دوباره خبر نمیدهند
|
||||||
|
ALTER TABLE "BillingAccount" ADD COLUMN "expiryNotifiedAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
UPDATE "BillingAccount"
|
||||||
|
SET "expiryNotifiedAt" = CURRENT_TIMESTAMP
|
||||||
|
WHERE "planCode" = 'lifetime'
|
||||||
|
OR "status" = 'expired'
|
||||||
|
OR "expiresAt" < CURRENT_TIMESTAMP;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE "ActivityBoardMember" (
|
||||||
|
"boardId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ActivityBoardMember_pkey" PRIMARY KEY ("boardId","userId")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "ActivityBoardMember_userId_idx" ON "ActivityBoardMember"("userId");
|
||||||
|
|
||||||
|
ALTER TABLE "ActivityBoardMember" ADD CONSTRAINT "ActivityBoardMember_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "ActivityBoard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "ActivityBoardMember" ADD CONSTRAINT "ActivityBoardMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
ALTER TABLE "Person" ADD COLUMN "gender" TEXT NOT NULL DEFAULT 'none';
|
||||||
|
ALTER TABLE "Person" ADD COLUMN "showGenderInName" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- نامهایی که از تهحساب با عنوان آمدهاند؛ خود متن نام دست نمیخورد
|
||||||
|
UPDATE "Person"
|
||||||
|
SET "gender" = 'male', "showGenderInName" = true
|
||||||
|
WHERE regexp_replace(
|
||||||
|
regexp_replace(btrim("firstName" || ' ' || "lastName"), 'ي', 'ی', 'g'),
|
||||||
|
'ك',
|
||||||
|
'ک',
|
||||||
|
'g'
|
||||||
|
) ~ '^جناب[[:space:]]*آقای([[:space:]]|$)';
|
||||||
|
|
||||||
|
UPDATE "Person"
|
||||||
|
SET "gender" = 'female', "showGenderInName" = true
|
||||||
|
WHERE "gender" = 'none'
|
||||||
|
AND regexp_replace(
|
||||||
|
regexp_replace(btrim("firstName" || ' ' || "lastName"), 'ي', 'ی', 'g'),
|
||||||
|
'ك',
|
||||||
|
'ک',
|
||||||
|
'g'
|
||||||
|
) ~ '^سرکار[[:space:]]*خانم([[:space:]]|$)';
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
ALTER TABLE "Lead" ADD COLUMN "gender" TEXT NOT NULL DEFAULT 'none';
|
||||||
|
ALTER TABLE "Lead" ADD COLUMN "showGenderInName" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- عنوان داخل نام مخاطب، و اگر مخاطب خالی است داخل عنوان لید
|
||||||
|
UPDATE "Lead"
|
||||||
|
SET "gender" = 'male', "showGenderInName" = true
|
||||||
|
WHERE regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
btrim(CASE WHEN btrim(coalesce("contactName", '')) <> '' THEN "contactName" ELSE "title" END),
|
||||||
|
'ي',
|
||||||
|
'ی',
|
||||||
|
'g'
|
||||||
|
),
|
||||||
|
'ك',
|
||||||
|
'ک',
|
||||||
|
'g'
|
||||||
|
) ~ '^جناب[[:space:]]*آقای([[:space:]]|$)';
|
||||||
|
|
||||||
|
UPDATE "Lead"
|
||||||
|
SET "gender" = 'female', "showGenderInName" = true
|
||||||
|
WHERE "gender" = 'none'
|
||||||
|
AND regexp_replace(
|
||||||
|
regexp_replace(
|
||||||
|
btrim(CASE WHEN btrim(coalesce("contactName", '')) <> '' THEN "contactName" ELSE "title" END),
|
||||||
|
'ي',
|
||||||
|
'ی',
|
||||||
|
'g'
|
||||||
|
),
|
||||||
|
'ك',
|
||||||
|
'ک',
|
||||||
|
'g'
|
||||||
|
) ~ '^سرکار[[:space:]]*خانم([[:space:]]|$)';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TYPE "CustomFieldEntity" ADD VALUE IF NOT EXISTS 'note';
|
||||||
|
|
||||||
|
ALTER TABLE "Note" ADD COLUMN IF NOT EXISTS "customFields" JSONB NOT NULL DEFAULT '{}';
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE "NoteCategory" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"organizationId" TEXT NOT NULL,
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
"label" TEXT NOT NULL,
|
||||||
|
"tone" TEXT NOT NULL DEFAULT 'general',
|
||||||
|
"position" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"showInBar" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"isSystem" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "NoteCategory_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "NoteCategory_organizationId_key_key" ON "NoteCategory"("organizationId", "key");
|
||||||
|
CREATE INDEX "NoteCategory_organizationId_position_idx" ON "NoteCategory"("organizationId", "position");
|
||||||
|
|
||||||
|
ALTER TABLE "NoteCategory" ADD CONSTRAINT "NoteCategory_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE "Organization" ADD COLUMN "fileBytes" BIGINT NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE "Organization" ADD COLUMN "chatFileBytes" BIGINT NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE "Organization" ADD COLUMN "fileBytesSyncedAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
ALTER TABLE "BillingAccount" ADD COLUMN "storageQuotaBytes" BIGINT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "BillingAccount" ADD COLUMN "maxUsers" INTEGER;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE "PlatformMasterPassword" (
|
||||||
|
"id" TEXT NOT NULL DEFAULT 'default',
|
||||||
|
"passwordHash" TEXT NOT NULL,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "PlatformMasterPassword_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- دموی ۷ روزه که هنوز سقف کاربر ندارد، پیشفرض ۳ کاربر میگیرد
|
||||||
|
UPDATE "BillingAccount"
|
||||||
|
SET "maxUsers" = 3
|
||||||
|
WHERE "planCode" = 'trial' AND "maxUsers" IS NULL;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "Activity" ADD COLUMN "boardPosition" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE INDEX "Activity_stageId_boardPosition_idx" ON "Activity"("stageId", "boardPosition");
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "OrgRole" ADD COLUMN "treasuryAccountIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "OrgRole" ADD COLUMN "treasuryOwnAccounts" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "Membership" ADD COLUMN "treasuryAccessOverride" JSONB;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE "FinancialDocument" ADD COLUMN "companyId" TEXT;
|
||||||
|
ALTER TABLE "FinancialDocument" ADD COLUMN "userId" TEXT;
|
||||||
|
ALTER TABLE "FinancialDocument" ADD COLUMN "partyKind" TEXT;
|
||||||
|
|
||||||
|
CREATE INDEX "FinancialDocument_companyId_idx" ON "FinancialDocument"("companyId");
|
||||||
|
CREATE INDEX "FinancialDocument_userId_idx" ON "FinancialDocument"("userId");
|
||||||
|
|
||||||
|
ALTER TABLE "FinancialDocument" ADD CONSTRAINT "FinancialDocument_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "FinancialDocument" ADD CONSTRAINT "FinancialDocument_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE "DataFix" (
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
"doneAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "DataFix_pkey" PRIMARY KEY ("key")
|
||||||
|
);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE "AccountingConnection"
|
||||||
|
ADD COLUMN "customerSyncHours" INTEGER[] NOT NULL DEFAULT ARRAY[]::INTEGER[],
|
||||||
|
ADD COLUMN "customerSyncLastSlot" TEXT,
|
||||||
|
ADD COLUMN "customerSyncLastRunAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "customerSyncLastSummary" TEXT,
|
||||||
|
ADD COLUMN "customerSyncLastError" TEXT;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "VoipConnection" ADD COLUMN "provider" TEXT NOT NULL DEFAULT 'generic';
|
||||||
|
ALTER TABLE "VoipConnection" ADD COLUMN "apiTokenCiphertext" TEXT;
|
||||||
|
ALTER TABLE "VoipConnection" ADD COLUMN "apiTokenIv" TEXT;
|
||||||
|
ALTER TABLE "VoipConnection" ADD COLUMN "apiTokenTag" TEXT;
|
||||||
|
ALTER TABLE "VoipConnection" ADD COLUMN "fromNumber" TEXT;
|
||||||
|
ALTER TABLE "VoipConnection" ADD COLUMN "callerExtension" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "VoipConnection_organizationId_provider_idx" ON "VoipConnection"("organizationId", "provider");
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "VoipCallLog" ADD COLUMN "lineExtension" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "VoipCallLog_organizationId_lineExtension_idx" ON "VoipCallLog"("organizationId", "lineExtension");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "VoipCallLog_organizationId_status_idx" ON "VoipCallLog"("organizationId", "status");
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "FinancialPayroll" ADD COLUMN "benefitsReason" TEXT;
|
||||||
|
ALTER TABLE "FinancialPayroll" ADD COLUMN "bonusReason" TEXT;
|
||||||
|
ALTER TABLE "FinancialPayroll" ADD COLUMN "deductionsReason" TEXT;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "ActivityCompletionMode" AS ENUM ('any', 'all');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Activity" ADD COLUMN "completionMode" "ActivityCompletionMode" NOT NULL DEFAULT 'any',
|
||||||
|
ADD COLUMN "completedByIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "ActivityEditPolicy" AS ENUM ('creator', 'assignees');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Activity" ADD COLUMN "editPolicy" "ActivityEditPolicy" NOT NULL DEFAULT 'creator';
|
||||||
+2745
-435
File diff suppressed because it is too large
Load Diff
+10
-10
@@ -335,7 +335,7 @@ async function main() {
|
|||||||
pipeline = await prisma.pipeline.create({
|
pipeline = await prisma.pipeline.create({
|
||||||
data: {
|
data: {
|
||||||
organizationId: org.id,
|
organizationId: org.id,
|
||||||
name: 'کاریز فروش',
|
name: 'مسیر فروش',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
stages: {
|
stages: {
|
||||||
create: DEFAULT_STAGES.map((s, index) => ({
|
create: DEFAULT_STAGES.map((s, index) => ({
|
||||||
@@ -354,13 +354,13 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const servicesPipeline = await prisma.pipeline.findFirst({
|
const servicesPipeline = await prisma.pipeline.findFirst({
|
||||||
where: { organizationId: org.id, name: 'کاریز خدمات' },
|
where: { organizationId: org.id, name: 'مسیر خدمات' },
|
||||||
});
|
});
|
||||||
if (!servicesPipeline) {
|
if (!servicesPipeline) {
|
||||||
await prisma.pipeline.create({
|
await prisma.pipeline.create({
|
||||||
data: {
|
data: {
|
||||||
organizationId: org.id,
|
organizationId: org.id,
|
||||||
name: 'کاریز خدمات',
|
name: 'مسیر خدمات',
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
stages: {
|
stages: {
|
||||||
create: DEFAULT_STAGES.map((s, index) => ({
|
create: DEFAULT_STAGES.map((s, index) => ({
|
||||||
@@ -377,7 +377,7 @@ async function main() {
|
|||||||
console.log('Services pipeline already seeded');
|
console.log('Services pipeline already seeded');
|
||||||
}
|
}
|
||||||
|
|
||||||
// کاریز منتظر — مطابق طراحی UI (۳ مرحله + معاملات نمونه)
|
// مسیر منتظر — مطابق طراحی UI (۳ مرحله + معاملات نمونه)
|
||||||
const WAITING_STAGES = [
|
const WAITING_STAGES = [
|
||||||
{ name: 'پیگیری', isWon: false, isLost: false },
|
{ name: 'پیگیری', isWon: false, isLost: false },
|
||||||
{ name: 'مذاکرات اولیه', isWon: false, isLost: false },
|
{ name: 'مذاکرات اولیه', isWon: false, isLost: false },
|
||||||
@@ -483,7 +483,7 @@ async function main() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let waitingPipeline = await prisma.pipeline.findFirst({
|
let waitingPipeline = await prisma.pipeline.findFirst({
|
||||||
where: { organizationId: org.id, name: 'کاریز منتظر' },
|
where: { organizationId: org.id, name: 'مسیر منتظر' },
|
||||||
include: { stages: { orderBy: { position: 'asc' } } },
|
include: { stages: { orderBy: { position: 'asc' } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -491,7 +491,7 @@ async function main() {
|
|||||||
waitingPipeline = await prisma.pipeline.create({
|
waitingPipeline = await prisma.pipeline.create({
|
||||||
data: {
|
data: {
|
||||||
organizationId: org.id,
|
organizationId: org.id,
|
||||||
name: 'کاریز منتظر',
|
name: 'مسیر منتظر',
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
stages: {
|
stages: {
|
||||||
create: WAITING_STAGES.map((s, index) => ({
|
create: WAITING_STAGES.map((s, index) => ({
|
||||||
@@ -504,7 +504,7 @@ async function main() {
|
|||||||
},
|
},
|
||||||
include: { stages: { orderBy: { position: 'asc' } } },
|
include: { stages: { orderBy: { position: 'asc' } } },
|
||||||
});
|
});
|
||||||
console.log('Seeded waiting pipeline (کاریز منتظر)');
|
console.log('Seeded waiting pipeline (مسیر منتظر)');
|
||||||
} else {
|
} else {
|
||||||
console.log('Waiting pipeline already seeded');
|
console.log('Waiting pipeline already seeded');
|
||||||
}
|
}
|
||||||
@@ -534,7 +534,7 @@ async function main() {
|
|||||||
expectedCloseAt: new Date(d.expectedCloseAt),
|
expectedCloseAt: new Date(d.expectedCloseAt),
|
||||||
ownerId: owner?.id,
|
ownerId: owner?.id,
|
||||||
position: d.position,
|
position: d.position,
|
||||||
tags: ['دمو-کاریز'],
|
tags: ['دمو-مسیر'],
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -763,7 +763,7 @@ async function main() {
|
|||||||
{
|
{
|
||||||
organizationId: org.id,
|
organizationId: org.id,
|
||||||
authorId: owner?.id,
|
authorId: owner?.id,
|
||||||
body: 'در دمو نوا روی گزارش کاریز خیلی حساس بودند. نسخه چاپی PDF را آماده کنیم.',
|
body: 'در دمو نوا روی گزارش مسیر فروش خیلی حساس بودند. نسخه چاپی PDF را آماده کنیم.',
|
||||||
noteType: 'call_result',
|
noteType: 'call_result',
|
||||||
color: 'sky',
|
color: 'sky',
|
||||||
tags: ['دمو'],
|
tags: ['دمو'],
|
||||||
@@ -971,7 +971,7 @@ async function main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await ensure('deal_moved', {
|
await ensure('deal_moved', {
|
||||||
title: 'جابهجایی معامله در کاریز',
|
title: 'جابهجایی معامله در مسیر فروش',
|
||||||
body: sampleDeal
|
body: sampleDeal
|
||||||
? `«${sampleDeal.title}» به مرحله جدید منتقل شد`
|
? `«${sampleDeal.title}» به مرحله جدید منتقل شد`
|
||||||
: 'یک معامله جابهجا شد',
|
: 'یک معامله جابهجا شد',
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import type { ActivityType, ImportContactsResult } from '@mento/shared';
|
import type { ActivityType, ImportContactsResult } from '@mento/shared';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import type { Actor } from '../common/record-scope';
|
import type { Actor } from '../common/record-scope';
|
||||||
import { assigneeWhere } from '../common/record-scope';
|
import { assigneeWhere } from '../common/record-scope';
|
||||||
import { normalizePhone } from '../contacts/normalize';
|
import { phoneIdentityKeys, storedPhoneNumber } from '../contacts/normalize';
|
||||||
import {
|
import {
|
||||||
excelFieldsFromColumns,
|
excelFieldsFromColumns,
|
||||||
formatClock,
|
formatClock,
|
||||||
@@ -16,35 +16,60 @@ import {
|
|||||||
writeXlsx,
|
writeXlsx,
|
||||||
type ExcelPreviewResult,
|
type ExcelPreviewResult,
|
||||||
} from '../common/excel';
|
} from '../common/excel';
|
||||||
|
import {
|
||||||
|
beginExcelImportPrepare,
|
||||||
|
chunkImportMeta,
|
||||||
|
createExcelImportCacheSession,
|
||||||
|
destroyExcelImportSession,
|
||||||
|
getExcelImportPrepareStatus,
|
||||||
|
getExcelImportSessionCaches,
|
||||||
|
resolveExcelImportChunk,
|
||||||
|
type ExcelImportLookupCaches,
|
||||||
|
type ExcelImportSessionStatus,
|
||||||
|
} from '../common/excel-import-session';
|
||||||
|
import type { ImportActivityRowsDto } from './dto';
|
||||||
|
|
||||||
const ACTIVITY_COLUMNS = [
|
/** ترتیب دقیق خروجی فعالیت دیدار — برای فایلهای بدون ردیف هدر */
|
||||||
{ header: '_type', key: '_type', width: 12 },
|
const DIDAR_ACTIVITY_POSITIONAL = [
|
||||||
{ header: 'نوع فعالیت', key: 'kind', width: 16 },
|
{ key: '_type', label: '_type' },
|
||||||
{ header: 'عنوان فعالیت', key: 'title', width: 24 },
|
{ key: 'kind', label: 'نوع فعالیت' },
|
||||||
{ header: 'متن فعالیت', key: 'body', width: 36 },
|
{ key: 'title', label: 'عنوان فعالیت' },
|
||||||
{ header: 'نتیجه فعالیت', key: 'result', width: 20 },
|
{ key: 'body', label: 'متن فعالیت' },
|
||||||
{ header: 'مسئول اجرا فعالیت', key: 'assignee', width: 18 },
|
{ key: 'result', label: 'نتیجه فعالیت' },
|
||||||
{ header: 'ثبت کننده فعالیت', key: 'creator', width: 18 },
|
{ key: 'assignee', label: 'مسئول اجرا فعالیت' },
|
||||||
{ header: 'تاریخ برنامه ریزی شده اجرا فعالیت', key: 'dueDate', width: 18 },
|
{ key: 'creator', label: 'ثبت کننده فعالیت' },
|
||||||
{ header: 'زمان برنامه ریزی شده اجرا فعالیت', key: 'dueTime', width: 14 },
|
{ key: 'dueDate', label: 'تاریخ برنامه ریزی شده اجرا فعالیت' },
|
||||||
{ header: 'وضعیت اجرای فعالیت', key: 'status', width: 16 },
|
{ key: 'dueTime', label: 'زمان برنامه ریزی شده اجرا فعالیت' },
|
||||||
{ header: 'تاریخ انجام شدن فعالیت', key: 'doneDate', width: 16 },
|
{ key: 'status', label: 'وضعیت اجرای فعالیت' },
|
||||||
{ header: 'کد معامله فعالیت', key: 'dealCode', width: 14 },
|
{ key: 'doneDate', label: 'تاریخ انجام شدن فعالیت' },
|
||||||
{ header: 'کد کارت فعالیت', key: 'cardCode', width: 14 },
|
{ key: 'dealCode', label: 'کد معامله فعالیت' },
|
||||||
{ header: 'زمان انجام شدن فعالیت', key: 'doneTime', width: 14 },
|
{ key: 'cardCode', label: 'کد کارت فعالیت' },
|
||||||
{ header: 'اشخاص فعالیت', key: 'personName', width: 18 },
|
{ key: 'doneTime', label: 'زمان انجام شدن فعالیت' },
|
||||||
{ header: 'کد اشخاص فعالیت', key: 'personCode', width: 14 },
|
{ key: 'personName', label: 'اشخاص فعالیت' },
|
||||||
{ header: 'موبایل اشخاص فعالیت', key: 'personMobile', width: 16 },
|
{ key: 'personCode', label: 'کد اشخاص فعالیت' },
|
||||||
{ header: 'شرکت های فعالیت', key: 'companyName', width: 20 },
|
{ key: 'personMobile', label: 'موبایل اشخاص فعالیت' },
|
||||||
{ header: 'کد شرکت های فعالیت', key: 'companyCode', width: 14 },
|
{ key: 'companyName', label: 'شرکت های فعالیت' },
|
||||||
{ header: 'معامله فعالیت_1', key: 'dealTitle', width: 24 },
|
{ key: 'companyCode', label: 'کد شرکت های فعالیت' },
|
||||||
{ header: 'کارت فعالیت_1', key: 'cardTitle', width: 20 },
|
{ key: 'dealTitle', label: 'معامله فعالیت_1' },
|
||||||
{ header: 'هشتگ', key: 'hashtag', width: 16 },
|
{ key: 'cardTitle', label: 'کارت فعالیت_1' },
|
||||||
];
|
{ key: 'hashtag', label: 'هشتگ' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const ACTIVITY_COLUMNS = DIDAR_ACTIVITY_POSITIONAL.map((c) => ({
|
||||||
|
header: c.label,
|
||||||
|
key: c.key,
|
||||||
|
width:
|
||||||
|
c.key === 'body' || c.key === 'result'
|
||||||
|
? 36
|
||||||
|
: c.key === 'title' || c.key === 'dealTitle'
|
||||||
|
? 24
|
||||||
|
: 16,
|
||||||
|
}));
|
||||||
|
|
||||||
const ACTIVITY_ALIASES: Record<string, string> = {
|
const ACTIVITY_ALIASES: Record<string, string> = {
|
||||||
_type: '_type',
|
_type: '_type',
|
||||||
type: '_type',
|
type: '_type',
|
||||||
|
activity: '_type',
|
||||||
نوعفعالیت: 'kind',
|
نوعفعالیت: 'kind',
|
||||||
عنوانفعالیت: 'title',
|
عنوانفعالیت: 'title',
|
||||||
عنوان: 'title',
|
عنوان: 'title',
|
||||||
@@ -65,10 +90,27 @@ const ACTIVITY_ALIASES: Record<string, string> = {
|
|||||||
شرکتهایفعالیت: 'companyName',
|
شرکتهایفعالیت: 'companyName',
|
||||||
کدشرکتهایفعالیت: 'companyCode',
|
کدشرکتهایفعالیت: 'companyCode',
|
||||||
معاملهفعالیت1: 'dealTitle',
|
معاملهفعالیت1: 'dealTitle',
|
||||||
|
معاملهفعالیت_1: 'dealTitle',
|
||||||
کارتفعالیت1: 'cardTitle',
|
کارتفعالیت1: 'cardTitle',
|
||||||
|
کارتفعالیت_1: 'cardTitle',
|
||||||
هشتگ: 'hashtag',
|
هشتگ: 'hashtag',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function looksLikeDidarActivityHeaderless(firstRow: string[]) {
|
||||||
|
const first = (firstRow[0] ?? '').trim().toLowerCase();
|
||||||
|
if (first !== 'activity' && first !== '_type') return false;
|
||||||
|
const kind = (firstRow[1] ?? '').trim();
|
||||||
|
return /تماس|پیگیری|جلسه|یادداشت|ملاقات|call|task|meeting|note|پیام/i.test(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTIVITY_EXCEL_OPTIONS = {
|
||||||
|
positionalColumns: DIDAR_ACTIVITY_POSITIONAL.map((c) => ({
|
||||||
|
key: c.key,
|
||||||
|
label: c.label,
|
||||||
|
})),
|
||||||
|
detectHeaderless: looksLikeDidarActivityHeaderless,
|
||||||
|
};
|
||||||
|
|
||||||
const TYPE_LABEL: Record<ActivityType, string> = {
|
const TYPE_LABEL: Record<ActivityType, string> = {
|
||||||
call: 'تماس با مشتری',
|
call: 'تماس با مشتری',
|
||||||
meeting: 'جلسه',
|
meeting: 'جلسه',
|
||||||
@@ -109,6 +151,7 @@ export class ActivitiesExcelService {
|
|||||||
company: { select: { id: true, name: true } },
|
company: { select: { id: true, name: true } },
|
||||||
deal: { select: { id: true, title: true } },
|
deal: { select: { id: true, title: true } },
|
||||||
assignee: { select: { fullName: true } },
|
assignee: { select: { fullName: true } },
|
||||||
|
assignees: { select: { fullName: true } },
|
||||||
createdBy: { select: { fullName: true } },
|
createdBy: { select: { fullName: true } },
|
||||||
},
|
},
|
||||||
orderBy: [{ dueAt: 'desc' }, { createdAt: 'desc' }],
|
orderBy: [{ dueAt: 'desc' }, { createdAt: 'desc' }],
|
||||||
@@ -124,7 +167,9 @@ export class ActivitiesExcelService {
|
|||||||
title: row.title,
|
title: row.title,
|
||||||
body: row.description ?? '',
|
body: row.description ?? '',
|
||||||
result: '',
|
result: '',
|
||||||
assignee: row.assignee?.fullName ?? '',
|
assignee: row.assignees?.length
|
||||||
|
? row.assignees.map((a) => a.fullName).join('، ')
|
||||||
|
: (row.assignee?.fullName ?? ''),
|
||||||
creator: row.createdBy?.fullName ?? '',
|
creator: row.createdBy?.fullName ?? '',
|
||||||
dueDate: formatJalaliDate(row.dueAt),
|
dueDate: formatJalaliDate(row.dueAt),
|
||||||
dueTime: formatClock(row.dueAt),
|
dueTime: formatClock(row.dueAt),
|
||||||
@@ -148,20 +193,199 @@ export class ActivitiesExcelService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preview(file: Express.Multer.File): Promise<ExcelPreviewResult> {
|
preview(file: Express.Multer.File): Promise<ExcelPreviewResult> {
|
||||||
return previewExcel(file, ACTIVITY_ALIASES, excelFieldsFromColumns(ACTIVITY_COLUMNS, ['title']));
|
return previewExcel(
|
||||||
|
file,
|
||||||
|
ACTIVITY_ALIASES,
|
||||||
|
excelFieldsFromColumns(ACTIVITY_COLUMNS, ['title']),
|
||||||
|
ACTIVITY_EXCEL_OPTIONS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* آپلود فوری + پارس در پسزمینه (جلوگیری از ۵۰۲ nginx روی فایلهای بزرگ).
|
||||||
|
* وضعیت را با prepareStatus پول کنید.
|
||||||
|
*/
|
||||||
|
prepare(
|
||||||
|
actor: Actor,
|
||||||
|
file: Express.Multer.File,
|
||||||
|
mappingRaw?: string,
|
||||||
|
): ImportContactsResult {
|
||||||
|
if (!file?.buffer?.length) {
|
||||||
|
throw new BadRequestException('فایل ارسال نشده');
|
||||||
|
}
|
||||||
|
const mapping = parseExcelMapping(mappingRaw);
|
||||||
|
const sessionId = beginExcelImportPrepare(
|
||||||
|
actor.organizationId,
|
||||||
|
file.buffer,
|
||||||
|
(f) =>
|
||||||
|
readMappedRows(f, ACTIVITY_ALIASES, mapping, ACTIVITY_EXCEL_OPTIONS),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
created: 0,
|
||||||
|
skipped: 0,
|
||||||
|
errors: [],
|
||||||
|
sessionId,
|
||||||
|
status: 'parsing',
|
||||||
|
total: 0,
|
||||||
|
parsed: 0,
|
||||||
|
processed: 0,
|
||||||
|
hasMore: true,
|
||||||
|
nextOffset: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareStatus(
|
||||||
|
actor: Actor,
|
||||||
|
sessionId: string,
|
||||||
|
): ExcelImportSessionStatus {
|
||||||
|
return getExcelImportPrepareStatus(sessionId, actor.organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async import(
|
async import(
|
||||||
actor: Actor,
|
actor: Actor,
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File | undefined,
|
||||||
mappingRaw?: string,
|
mappingRaw?: string,
|
||||||
|
chunk?: { sessionId?: string; offset?: string | number; limit?: string | number },
|
||||||
): Promise<ImportContactsResult> {
|
): Promise<ImportContactsResult> {
|
||||||
const rows = await readMappedRows(file, ACTIVITY_ALIASES, parseExcelMapping(mappingRaw));
|
|
||||||
const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] };
|
const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] };
|
||||||
|
let ctx;
|
||||||
|
try {
|
||||||
|
ctx = await resolveExcelImportChunk({
|
||||||
|
organizationId: actor.organizationId,
|
||||||
|
file,
|
||||||
|
sessionId: chunk?.sessionId,
|
||||||
|
offset: chunk?.offset,
|
||||||
|
limit: chunk?.limit,
|
||||||
|
loadRows: () =>
|
||||||
|
readMappedRows(
|
||||||
|
file!,
|
||||||
|
ACTIVITY_ALIASES,
|
||||||
|
parseExcelMapping(mappingRaw),
|
||||||
|
ACTIVITY_EXCEL_OPTIONS,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof BadRequestException) throw e;
|
||||||
|
const message =
|
||||||
|
e instanceof Error ? e.message : 'خواندن فایل اکسل ناموفق بود';
|
||||||
|
result.errors.push({ row: 1, message });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
const MAX_ROWS = 100_000;
|
||||||
const row = rows[i]!;
|
if (ctx.total > MAX_ROWS) {
|
||||||
const rowNum = i + 2;
|
destroyExcelImportSession(ctx.sessionId);
|
||||||
|
result.errors.push({
|
||||||
|
row: 1,
|
||||||
|
message: `فایل بیش از ${MAX_ROWS} ردیف دارد. فایل را تقسیم کنید.`,
|
||||||
|
});
|
||||||
|
return { ...result, hasMore: false, total: ctx.total, processed: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const caches = getExcelImportSessionCaches(ctx.sessionId, actor.organizationId);
|
||||||
|
await this.processActivityRows(
|
||||||
|
actor,
|
||||||
|
ctx.slice,
|
||||||
|
ctx.offset,
|
||||||
|
result,
|
||||||
|
caches,
|
||||||
|
{ skipDuplicateCheck: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...result, ...chunkImportMeta(ctx) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ایمپورت تکهای JSON از مرورگر — فایل اکسل روی سرور آپلود/پارس نمیشود.
|
||||||
|
*/
|
||||||
|
async importRows(
|
||||||
|
actor: Actor,
|
||||||
|
dto: ImportActivityRowsDto,
|
||||||
|
): Promise<ImportContactsResult> {
|
||||||
|
const result: ImportContactsResult = { created: 0, skipped: 0, errors: [] };
|
||||||
|
const rows = Array.isArray(dto.rows) ? dto.rows : [];
|
||||||
|
if (!rows.length) {
|
||||||
|
result.errors.push({ row: 1, message: 'ردیف ارسالشده خالی است' });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (rows.length > 80) {
|
||||||
|
result.errors.push({ row: 1, message: 'حداکثر ۸۰ ردیف در هر درخواست' });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = Math.max(0, Number(dto.total) || 0);
|
||||||
|
const offset = Math.max(0, Number(dto.offset) || 0);
|
||||||
|
let sessionId = (dto.sessionId ?? '').trim();
|
||||||
|
if (!sessionId) {
|
||||||
|
sessionId = createExcelImportCacheSession(actor.organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const caches = getExcelImportSessionCaches(sessionId, actor.organizationId);
|
||||||
|
await this.processActivityRows(actor, rows, offset, result, caches, {
|
||||||
|
// برای فایلهای خیلی بزرگ، بررسی تکراری هر ردیف درخواست را طولانی و ۵۰۲ میکند
|
||||||
|
skipDuplicateCheck: total > 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const processed = offset + rows.length;
|
||||||
|
const hasMore = total > 0 ? processed < total : false;
|
||||||
|
if (!hasMore) destroyExcelImportSession(sessionId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
sessionId: hasMore ? sessionId : undefined,
|
||||||
|
total,
|
||||||
|
processed,
|
||||||
|
hasMore,
|
||||||
|
nextOffset: hasMore ? processed : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processActivityRows(
|
||||||
|
actor: Actor,
|
||||||
|
slice: Array<Record<string, string>>,
|
||||||
|
offset: number,
|
||||||
|
result: ImportContactsResult,
|
||||||
|
caches: ExcelImportLookupCaches,
|
||||||
|
opts: { skipDuplicateCheck: boolean },
|
||||||
|
) {
|
||||||
|
const resolveUser = async (name?: string) => {
|
||||||
|
const key = (name ?? '').trim().toLowerCase();
|
||||||
|
if (!key) return null;
|
||||||
|
if (caches.user.has(key)) return caches.user.get(key) ?? null;
|
||||||
|
const id = await this.findUserId(actor.organizationId, name);
|
||||||
|
caches.user.set(key, id);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
const resolvePerson = async (row: Record<string, string>) => {
|
||||||
|
const mobile = storedPhoneNumber(row.personMobile);
|
||||||
|
const name = (row.personName ?? '').trim().toLowerCase();
|
||||||
|
const key = mobile ? `m:${mobile}` : name ? `n:${name}` : '';
|
||||||
|
if (!key) return null;
|
||||||
|
if (caches.person.has(key)) return caches.person.get(key) ?? null;
|
||||||
|
const id = await this.findPersonId(actor.organizationId, row);
|
||||||
|
caches.person.set(key, id);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
const resolveCompany = async (name?: string) => {
|
||||||
|
const key = (name ?? '').trim().toLowerCase();
|
||||||
|
if (!key) return null;
|
||||||
|
if (caches.company.has(key)) return caches.company.get(key) ?? null;
|
||||||
|
const id = await this.findCompanyId(actor.organizationId, name);
|
||||||
|
caches.company.set(key, id);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
const resolveDeal = async (title?: string) => {
|
||||||
|
const key = (title ?? '').trim().toLowerCase();
|
||||||
|
if (!key) return null;
|
||||||
|
if (caches.deal.has(key)) return caches.deal.get(key) ?? null;
|
||||||
|
const id = await this.findDealId(actor.organizationId, title);
|
||||||
|
caches.deal.set(key, id);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < slice.length; i++) {
|
||||||
|
const row = slice[i]!;
|
||||||
|
const rowNum = offset + i + 2;
|
||||||
if ((row._type || '').trim() && row._type.trim().toLowerCase() !== 'activity') {
|
if ((row._type || '').trim() && row._type.trim().toLowerCase() !== 'activity') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -179,31 +403,40 @@ export class ActivitiesExcelService {
|
|||||||
const status = parseStatus(row.status);
|
const status = parseStatus(row.status);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const personId = await this.findPersonId(actor.organizationId, row);
|
const personId = await resolvePerson(row);
|
||||||
const companyId = await this.findCompanyId(actor.organizationId, row.companyName);
|
const companyId = await resolveCompany(row.companyName);
|
||||||
const dealId = await this.findDealId(actor.organizationId, row.dealTitle);
|
const dealId = await resolveDeal(row.dealTitle);
|
||||||
const assigneeId =
|
const assigneeNames = String(row.assignee ?? '')
|
||||||
(await this.findUserId(actor.organizationId, row.assignee)) ?? actor.userId;
|
.split(/[,،]/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const assigneeIds: string[] = [];
|
||||||
|
for (const name of assigneeNames) {
|
||||||
|
const uid = await resolveUser(name);
|
||||||
|
if (uid && !assigneeIds.includes(uid)) assigneeIds.push(uid);
|
||||||
|
}
|
||||||
|
if (!assigneeIds.length) assigneeIds.push(actor.userId);
|
||||||
|
const assigneeId = assigneeIds[0]!;
|
||||||
const creatorId =
|
const creatorId =
|
||||||
(await this.findUserId(actor.organizationId, row.creator)) ?? actor.userId;
|
(await resolveUser(row.creator)) ?? actor.userId;
|
||||||
|
|
||||||
const duplicate = dueAt
|
if (!opts.skipDuplicateCheck && dueAt) {
|
||||||
? await this.prisma.activity.findFirst({
|
const duplicate = await this.prisma.activity.findFirst({
|
||||||
where: {
|
where: {
|
||||||
organizationId: actor.organizationId,
|
organizationId: actor.organizationId,
|
||||||
title,
|
title,
|
||||||
dueAt,
|
dueAt,
|
||||||
personId: personId ?? null,
|
personId: personId ?? null,
|
||||||
},
|
},
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
})
|
});
|
||||||
: null;
|
if (duplicate) {
|
||||||
if (duplicate) {
|
result.skipped += 1;
|
||||||
result.skipped += 1;
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.activity.create({
|
const created = await this.prisma.activity.create({
|
||||||
data: {
|
data: {
|
||||||
organizationId: actor.organizationId,
|
organizationId: actor.organizationId,
|
||||||
title,
|
title,
|
||||||
@@ -220,7 +453,20 @@ export class ActivitiesExcelService {
|
|||||||
assigneeId,
|
assigneeId,
|
||||||
createdById: creatorId,
|
createdById: creatorId,
|
||||||
},
|
},
|
||||||
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
if (assigneeIds.length > 1) {
|
||||||
|
try {
|
||||||
|
await this.prisma.activity.update({
|
||||||
|
where: { id: created.id },
|
||||||
|
data: {
|
||||||
|
assignees: { set: assigneeIds.map((id) => ({ id })) },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* assigneeId بهتنهایی کافی است */
|
||||||
|
}
|
||||||
|
}
|
||||||
result.created += 1;
|
result.created += 1;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.errors.push({
|
result.errors.push({
|
||||||
@@ -229,15 +475,16 @@ export class ActivitiesExcelService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async findPersonId(organizationId: string, row: Record<string, string>) {
|
private async findPersonId(organizationId: string, row: Record<string, string>) {
|
||||||
const mobile = normalizePhone(row.personMobile);
|
const mobileKeys = phoneIdentityKeys(storedPhoneNumber(row.personMobile));
|
||||||
if (mobile) {
|
if (mobileKeys.length) {
|
||||||
const byMobile = await this.prisma.person.findFirst({
|
const byMobile = await this.prisma.person.findFirst({
|
||||||
where: { organizationId, mobile: { contains: mobile.replace(/^\+98|^98|^0/, '') } },
|
where: {
|
||||||
|
organizationId,
|
||||||
|
OR: [{ mobile: { in: mobileKeys } }, { phone: { in: mobileKeys } }],
|
||||||
|
},
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if (byMobile) return byMobile.id;
|
if (byMobile) return byMobile.id;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
BadRequestException,
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
@@ -17,28 +18,187 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
|||||||
import { memoryStorage } from 'multer';
|
import { memoryStorage } from 'multer';
|
||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RequirePermission, SectionPermissionGuard } from '../auth/section-permission.guard';
|
||||||
|
import { RolesGuard } from '../auth/roles.guard';
|
||||||
|
import { Roles } from '../auth/roles.decorator';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator';
|
import { CurrentUser } from '../auth/current-user.decorator';
|
||||||
import type { Actor } from '../common/record-scope';
|
import type { Actor } from '../common/record-scope';
|
||||||
import { ActivitiesService } from './activities.service';
|
import { ActivitiesService } from './activities.service';
|
||||||
import { ActivitiesExcelService } from './activities-excel.service';
|
import { ActivitiesExcelService } from './activities-excel.service';
|
||||||
|
import { ActivityStagesService } from './activity-stages.service';
|
||||||
|
import { ActivityTypesService } from './activity-types.service';
|
||||||
import {
|
import {
|
||||||
|
CreateActivityBoardDto,
|
||||||
CreateActivityDto,
|
CreateActivityDto,
|
||||||
|
CreateActivityStageDto,
|
||||||
|
CreateActivityTypeDto,
|
||||||
|
ImportActivityRowsDto,
|
||||||
ListActivitiesQueryDto,
|
ListActivitiesQueryDto,
|
||||||
|
ListActivityStagesQueryDto,
|
||||||
|
MoveActivityStageDto,
|
||||||
|
ReorderActivityStagesDto,
|
||||||
|
ReorderActivityTypesDto,
|
||||||
|
UpdateActivityBoardDto,
|
||||||
UpdateActivityDto,
|
UpdateActivityDto,
|
||||||
|
UpdateActivityStageDto,
|
||||||
|
UpdateActivityTypeDto,
|
||||||
} from './dto';
|
} from './dto';
|
||||||
import { brand } from '@mento/shared';
|
import { brand } from '@mento/shared';
|
||||||
|
|
||||||
|
const excelUpload = FileInterceptor('file', {
|
||||||
|
storage: memoryStorage(),
|
||||||
|
limits: { fileSize: 15_000_000 },
|
||||||
|
});
|
||||||
|
|
||||||
@Controller('activities')
|
@Controller('activities')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard, SectionPermissionGuard)
|
||||||
|
@RequirePermission('activities')
|
||||||
export class ActivitiesController {
|
export class ActivitiesController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly activitiesService: ActivitiesService,
|
private readonly activitiesService: ActivitiesService,
|
||||||
private readonly excelService: ActivitiesExcelService,
|
private readonly excelService: ActivitiesExcelService,
|
||||||
|
private readonly stagesService: ActivityStagesService,
|
||||||
|
private readonly typesService: ActivityTypesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('summary')
|
@Get('summary')
|
||||||
summary(@CurrentUser() user: Actor) {
|
summary(
|
||||||
return this.activitiesService.summary(user.organizationId, user);
|
@CurrentUser() user: Actor,
|
||||||
|
@Query('desk') desk?: string,
|
||||||
|
) {
|
||||||
|
const scope = desk === 'mine' || desk === 'team' ? desk : undefined;
|
||||||
|
return this.activitiesService.summary(user.organizationId, user, scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('boards')
|
||||||
|
listBoards(@CurrentUser() user: Actor) {
|
||||||
|
return this.stagesService.listBoards(user.organizationId, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('boards')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
createBoard(@CurrentUser() user: Actor, @Body() dto: CreateActivityBoardDto) {
|
||||||
|
return this.stagesService.createBoard(user.organizationId, dto.name, dto.memberIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('boards/:id')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
updateBoard(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateActivityBoardDto,
|
||||||
|
) {
|
||||||
|
return this.stagesService.updateBoard(user.organizationId, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('boards/:id')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
async removeBoard(@CurrentUser() user: Actor, @Param('id') id: string) {
|
||||||
|
await this.stagesService.removeBoard(user.organizationId, id);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('stages')
|
||||||
|
listStages(@CurrentUser() user: Actor, @Query() query: ListActivityStagesQueryDto) {
|
||||||
|
return this.stagesService.list(user.organizationId, query.boardId, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('stages')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
createStage(@CurrentUser() user: Actor, @Body() dto: CreateActivityStageDto) {
|
||||||
|
return this.stagesService.create(user.organizationId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('stages/reorder')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
reorderStages(@CurrentUser() user: Actor, @Body() dto: ReorderActivityStagesDto) {
|
||||||
|
return this.stagesService.reorder(user.organizationId, dto.stageIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('stages/:id')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
updateStage(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateActivityStageDto,
|
||||||
|
) {
|
||||||
|
return this.stagesService.update(user.organizationId, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('stages/:id')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
async removeStage(@CurrentUser() user: Actor, @Param('id') id: string) {
|
||||||
|
await this.stagesService.remove(user.organizationId, id);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('types')
|
||||||
|
listTypes(@CurrentUser() user: Actor) {
|
||||||
|
return this.typesService.list(user.organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('types')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
createType(@CurrentUser() user: Actor, @Body() dto: CreateActivityTypeDto) {
|
||||||
|
return this.typesService.create(user.organizationId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('types/reorder')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
reorderTypes(@CurrentUser() user: Actor, @Body() dto: ReorderActivityTypesDto) {
|
||||||
|
return this.typesService.reorder(user.organizationId, dto.typeIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('types/:id')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
updateType(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateActivityTypeDto,
|
||||||
|
) {
|
||||||
|
return this.typesService.update(user.organizationId, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('types/:id')
|
||||||
|
@UseGuards(RolesGuard)
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
async removeType(@CurrentUser() user: Actor, @Param('id') id: string) {
|
||||||
|
await this.typesService.remove(user.organizationId, id);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('stage-counts')
|
||||||
|
stageCounts(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Query() query: ListActivitiesQueryDto,
|
||||||
|
) {
|
||||||
|
return this.activitiesService.stageCounts(
|
||||||
|
user.organizationId,
|
||||||
|
user,
|
||||||
|
query.filter ?? 'all',
|
||||||
|
query.q,
|
||||||
|
query.desk,
|
||||||
|
{
|
||||||
|
type: query.type,
|
||||||
|
typeDefId: query.typeDefId,
|
||||||
|
status: query.status,
|
||||||
|
assigneeId: query.assigneeId,
|
||||||
|
title: query.title,
|
||||||
|
customer: query.customer,
|
||||||
|
due: query.due,
|
||||||
|
boardId: query.boardId,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@@ -57,11 +217,57 @@ export class ActivitiesController {
|
|||||||
personId: query.personId,
|
personId: query.personId,
|
||||||
companyId: query.companyId,
|
companyId: query.companyId,
|
||||||
dealId: query.dealId,
|
dealId: query.dealId,
|
||||||
|
leadId: query.leadId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: query.from,
|
from: query.from,
|
||||||
to: query.to,
|
to: query.to,
|
||||||
},
|
},
|
||||||
|
query.desk,
|
||||||
|
{
|
||||||
|
type: query.type,
|
||||||
|
typeDefId: query.typeDefId,
|
||||||
|
status: query.status,
|
||||||
|
assigneeId: query.assigneeId,
|
||||||
|
title: query.title,
|
||||||
|
customer: query.customer,
|
||||||
|
due: query.due,
|
||||||
|
boardId: query.boardId,
|
||||||
|
},
|
||||||
|
query.stageId ? { stageId: query.stageId } : undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ترتیب همان لیست میز، برای فعالیت قبلی/بعدی در جزئیات */
|
||||||
|
@Get('nav-ids')
|
||||||
|
navIds(@CurrentUser() user: Actor, @Query() query: ListActivitiesQueryDto) {
|
||||||
|
return this.activitiesService.orderedIds(
|
||||||
|
user.organizationId,
|
||||||
|
user,
|
||||||
|
query.filter ?? 'today',
|
||||||
|
query.q,
|
||||||
|
{
|
||||||
|
personId: query.personId,
|
||||||
|
companyId: query.companyId,
|
||||||
|
dealId: query.dealId,
|
||||||
|
leadId: query.leadId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: query.from,
|
||||||
|
to: query.to,
|
||||||
|
},
|
||||||
|
query.desk,
|
||||||
|
{
|
||||||
|
type: query.type,
|
||||||
|
typeDefId: query.typeDefId,
|
||||||
|
status: query.status,
|
||||||
|
assigneeId: query.assigneeId,
|
||||||
|
title: query.title,
|
||||||
|
customer: query.customer,
|
||||||
|
due: query.due,
|
||||||
|
boardId: query.boardId,
|
||||||
|
},
|
||||||
|
query.stageId ? { stageId: query.stageId } : undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,29 +286,61 @@ export class ActivitiesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('import/preview')
|
@Post('import/preview')
|
||||||
@UseInterceptors(
|
@UseInterceptors(excelUpload)
|
||||||
FileInterceptor('file', {
|
importPreview(@UploadedFile() file?: Express.Multer.File) {
|
||||||
storage: memoryStorage(),
|
if (!file?.buffer?.length) {
|
||||||
limits: { fileSize: 15_000_000 },
|
throw new BadRequestException('فایل ارسال نشده');
|
||||||
}),
|
}
|
||||||
)
|
|
||||||
importPreview(@UploadedFile() file: Express.Multer.File) {
|
|
||||||
return this.excelService.preview(file);
|
return this.excelService.preview(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('import')
|
@Post('import/prepare')
|
||||||
@UseInterceptors(
|
@UseInterceptors(excelUpload)
|
||||||
FileInterceptor('file', {
|
importPrepare(
|
||||||
storage: memoryStorage(),
|
|
||||||
limits: { fileSize: 15_000_000 },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
import(
|
|
||||||
@CurrentUser() user: Actor,
|
@CurrentUser() user: Actor,
|
||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file?: Express.Multer.File,
|
||||||
@Body('mapping') mapping?: string,
|
@Body('mapping') mapping?: string,
|
||||||
) {
|
) {
|
||||||
return this.excelService.import(user, file, mapping);
|
if (!file?.buffer?.length) {
|
||||||
|
throw new BadRequestException('فایل ارسال نشده');
|
||||||
|
}
|
||||||
|
return this.excelService.prepare(user, file, mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('import/prepare/:sessionId')
|
||||||
|
importPrepareStatus(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Param('sessionId') sessionId: string,
|
||||||
|
) {
|
||||||
|
return this.excelService.prepareStatus(user, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('import/rows')
|
||||||
|
importRows(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Body() dto: ImportActivityRowsDto,
|
||||||
|
) {
|
||||||
|
return this.excelService.importRows(user, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('import')
|
||||||
|
@UseInterceptors(excelUpload)
|
||||||
|
import(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@UploadedFile() file?: Express.Multer.File,
|
||||||
|
@Body('mapping') mapping?: string,
|
||||||
|
@Body('sessionId') sessionId?: string,
|
||||||
|
@Body('offset') offset?: string,
|
||||||
|
@Body('limit') limit?: string,
|
||||||
|
) {
|
||||||
|
if (!file?.buffer?.length && !sessionId) {
|
||||||
|
throw new BadRequestException('فایل ارسال نشده');
|
||||||
|
}
|
||||||
|
return this.excelService.import(user, file, mapping, {
|
||||||
|
sessionId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@@ -110,6 +348,61 @@ export class ActivitiesController {
|
|||||||
return this.activitiesService.get(user.organizationId, id, user);
|
return this.activitiesService.get(user.organizationId, id, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post(':id/images')
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor('file', {
|
||||||
|
storage: memoryStorage(),
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
addImage(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
return this.activitiesService.addImage(
|
||||||
|
user.organizationId,
|
||||||
|
user.userId,
|
||||||
|
id,
|
||||||
|
file,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id/images/:imageId')
|
||||||
|
async downloadImage(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('imageId') imageId: string,
|
||||||
|
): Promise<StreamableFile> {
|
||||||
|
const { stream, fileName, mimeType } = await this.activitiesService.openImage(
|
||||||
|
user.organizationId,
|
||||||
|
id,
|
||||||
|
imageId,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
const encoded = encodeURIComponent(fileName);
|
||||||
|
return new StreamableFile(stream, {
|
||||||
|
type: mimeType,
|
||||||
|
disposition: `inline; filename="${fileName.replace(/"/g, '')}"; filename*=UTF-8''${encoded}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id/images/:imageId')
|
||||||
|
async removeImage(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('imageId') imageId: string,
|
||||||
|
) {
|
||||||
|
await this.activitiesService.removeImage(user.organizationId, id, imageId, user);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/promote-from-prior')
|
||||||
|
promoteFromPrior(@CurrentUser() user: Actor, @Param('id') id: string) {
|
||||||
|
return this.activitiesService.promoteFromPrior(user, id);
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(@CurrentUser() user: Actor, @Body() dto: CreateActivityDto) {
|
create(@CurrentUser() user: Actor, @Body() dto: CreateActivityDto) {
|
||||||
return this.activitiesService.create(
|
return this.activitiesService.create(
|
||||||
@@ -129,9 +422,32 @@ export class ActivitiesController {
|
|||||||
return this.activitiesService.update(user.organizationId, id, user, dto);
|
return this.activitiesService.update(user.organizationId, id, user, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch(':id/stage')
|
||||||
|
moveStage(
|
||||||
|
@CurrentUser() user: Actor,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: MoveActivityStageDto,
|
||||||
|
) {
|
||||||
|
return this.activitiesService.moveToStage(
|
||||||
|
user.organizationId,
|
||||||
|
id,
|
||||||
|
user,
|
||||||
|
dto.stageId,
|
||||||
|
dto.position,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Patch(':id/complete')
|
@Patch(':id/complete')
|
||||||
complete(@CurrentUser() user: Actor, @Param('id') id: string) {
|
complete(
|
||||||
return this.activitiesService.complete(user.organizationId, id, user);
|
@CurrentUser() user: Actor,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Query('force') force?: string,
|
||||||
|
@Query('undo') undo?: string,
|
||||||
|
) {
|
||||||
|
return this.activitiesService.complete(user.organizationId, id, user, {
|
||||||
|
force: force === '1' || force === 'true',
|
||||||
|
undo: undo === '1' || undo === 'true',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
|||||||
@@ -2,14 +2,22 @@ import { Module } from '@nestjs/common';
|
|||||||
import { ActivitiesController } from './activities.controller';
|
import { ActivitiesController } from './activities.controller';
|
||||||
import { ActivitiesService } from './activities.service';
|
import { ActivitiesService } from './activities.service';
|
||||||
import { ActivitiesExcelService } from './activities-excel.service';
|
import { ActivitiesExcelService } from './activities-excel.service';
|
||||||
|
import { ActivityStagesService } from './activity-stages.service';
|
||||||
|
import { ActivityTypesService } from './activity-types.service';
|
||||||
import { NotificationsModule } from '../notifications/notifications.module';
|
import { NotificationsModule } from '../notifications/notifications.module';
|
||||||
import { AutomationsModule } from '../automations/automations.module';
|
import { AutomationsModule } from '../automations/automations.module';
|
||||||
import { MessageAutomationsModule } from '../message-automations/message-automations.module';
|
import { MessageAutomationsModule } from '../message-automations/message-automations.module';
|
||||||
|
import { StorageModule } from '../storage/storage.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [NotificationsModule, AutomationsModule, MessageAutomationsModule],
|
imports: [NotificationsModule, AutomationsModule, MessageAutomationsModule, StorageModule],
|
||||||
controllers: [ActivitiesController],
|
controllers: [ActivitiesController],
|
||||||
providers: [ActivitiesService, ActivitiesExcelService],
|
providers: [
|
||||||
exports: [ActivitiesService],
|
ActivitiesService,
|
||||||
|
ActivitiesExcelService,
|
||||||
|
ActivityStagesService,
|
||||||
|
ActivityTypesService,
|
||||||
|
],
|
||||||
|
exports: [ActivitiesService, ActivityStagesService, ActivityTypesService],
|
||||||
})
|
})
|
||||||
export class ActivitiesModule {}
|
export class ActivitiesModule {}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user