e31ea5354a
Заменяет static-снапшот LEAD_STATUSES в коде на live-данные из БД. Custom slug'и (добавленные после deployment'а) теперь видны UI без rebuild'а. Backend: - LeadStatus model (PK=slug string, incrementing=false, timestamps=null). - LeadStatusController::index — GET /api/lead-statuses, ORDER BY sort_order, slug. Таблица глобальная (не tenant-aware), auth не требуется на MVP. Pest +5 (LeadStatusesIndexTest): - 200 + не пустой / все 14 системных slug'ов из seed / все нужные поля / sort_order ASC / кастомный slug после INSERT появляется в endpoint'е. Frontend: - api/leadStatuses.ts::listLeadStatuses — GET helper. - stores/leadStatuses.ts::useLeadStatusesStore — Pinia setup-store: statuses default = LEAD_STATUSES snapshot (UI работает без fetch'а), load(force=false) идемпотентен, bySlug computed Map, findBySlug helper. На fail — snapshot остаётся, fetchError=true. - DealsView/KanbanView/DealDetailDrawer переехали со static-импорта LEAD_STATUSES на store. KanbanView использует safe-access dealsByStatus[slug] || [] (защита от custom slug'а из API без seeded column). load() в onMounted у обоих view'ов. Vitest +7 (leadStatusesStore.spec.ts): - initial snapshot / findBySlug existing & null / load success replace + loaded / load reject — fetchError + snapshot fallback / load идемпотентен / load(force=true) refetch. - 2 spec'а DealDetailDrawer получили setActivePinia(createPinia()) в beforeEach (без этого Pinia store-injection в jsdom падает). PHPStan baseline регенерирован. Регресс: - Lint+type-check+format passed. - Vitest 280/280 за 19.44 сек (+7 от 273). - Vite build 1.17 сек. - Pint + PHPStan passed. - Pest 210/210 за 24.59 сек (+5 от 205, 840 assertions). Реестр v1.63→v1.64 / CLAUDE.md v1.54→v1.55. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
58 lines
2.0 KiB
TypeScript
58 lines
2.0 KiB
TypeScript
import { computed, ref } from 'vue';
|
||
import { defineStore } from 'pinia';
|
||
import { LEAD_STATUSES, type LeadStatus } from '../composables/leadStatuses';
|
||
import { type ApiLeadStatus, listLeadStatuses } from '../api/leadStatuses';
|
||
|
||
/**
|
||
* Pinia store для статусов воронки. Заменяет static-снапшот `LEAD_STATUSES`
|
||
* (composables/leadStatuses.ts) на live-данные из API при наличии auth.
|
||
*
|
||
* Initial state — snapshot (UI работает сразу без fetch'а). `load()` вызывается
|
||
* из DealsView/KanbanView на mount и replace'ит snapshot реальными данными.
|
||
* На fail — снапшот остаётся.
|
||
*/
|
||
export const useLeadStatusesStore = defineStore('leadStatuses', () => {
|
||
const statuses = ref<LeadStatus[]>([...LEAD_STATUSES]);
|
||
const loading = ref(false);
|
||
const fetchError = ref(false);
|
||
const loaded = ref(false);
|
||
|
||
const bySlug = computed(() => {
|
||
const map = new Map<string, LeadStatus>();
|
||
statuses.value.forEach((s) => map.set(s.slug, s));
|
||
return map;
|
||
});
|
||
|
||
function findBySlug(slug: string): LeadStatus | null {
|
||
return bySlug.value.get(slug) ?? null;
|
||
}
|
||
|
||
async function load(force = false): Promise<void> {
|
||
if (loaded.value && !force) return;
|
||
loading.value = true;
|
||
fetchError.value = false;
|
||
try {
|
||
const apiList = await listLeadStatuses();
|
||
statuses.value = apiList.map(mapApi);
|
||
loaded.value = true;
|
||
} catch {
|
||
fetchError.value = true;
|
||
// Snapshot остаётся как fallback.
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
return { statuses, loading, fetchError, loaded, bySlug, findBySlug, load };
|
||
});
|
||
|
||
function mapApi(api: ApiLeadStatus): LeadStatus {
|
||
return {
|
||
slug: api.slug,
|
||
nameRu: api.name_ru,
|
||
isSystem: api.is_system,
|
||
sortOrder: api.sort_order,
|
||
colorHex: api.color_hex,
|
||
};
|
||
}
|