feat(rulebook): frontend API client + Pinia store

This commit is contained in:
2026-05-27 21:59:23 -04:00
parent eab5c5a026
commit 605dd0a13a
2 changed files with 263 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client";
export interface Rulebook {
id: number;
owner_user_id: number;
title: string;
description: string;
created_at: string | null;
updated_at: string | null;
}
export interface RulebookTopic {
id: number;
rulebook_id: number;
title: string;
description: string;
order_index: number;
created_at: string | null;
updated_at: string | null;
}
export interface Rule {
id: number;
topic_id: number;
title: string;
statement: string;
why: string;
how_to_apply: string;
order_index: number;
created_at: string | null;
updated_at: string | null;
}
export interface RuleHeader {
id: number;
title: string;
statement: string;
topic_id: number;
}
export interface ApplicableRules {
rules: {
id: number;
title: string;
statement: string;
topic_title: string;
rulebook_title: string;
}[];
truncated: boolean;
subscribed_rulebooks: { id: number; title: string }[];
}
// ── Rulebooks ───────────────────────────────────────────────────────
export async function listRulebooks(): Promise<Rulebook[]> {
const data = await apiGet<{ rulebooks: Rulebook[] }>("/api/rulebooks");
return data.rulebooks;
}
export async function getRulebook(id: number): Promise<Rulebook & { topics: RulebookTopic[] }> {
return apiGet(`/api/rulebooks/${id}`);
}
export async function createRulebook(data: { title: string; description?: string }): Promise<Rulebook> {
return apiPost("/api/rulebooks", data);
}
export async function updateRulebook(id: number, data: Partial<{ title: string; description: string }>): Promise<Rulebook> {
return apiPatch(`/api/rulebooks/${id}`, data);
}
export async function deleteRulebook(id: number): Promise<void> {
return apiDelete(`/api/rulebooks/${id}`);
}
// ── Topics ─────────────────────────────────────────────────────────
export async function listTopics(rulebookId: number): Promise<RulebookTopic[]> {
const data = await apiGet<{ topics: RulebookTopic[] }>(`/api/rulebooks/${rulebookId}/topics`);
return data.topics;
}
export async function createTopic(rulebookId: number, data: { title: string; description?: string; order_index?: number }): Promise<RulebookTopic> {
return apiPost(`/api/rulebooks/${rulebookId}/topics`, data);
}
export async function updateTopic(id: number, data: Partial<{ title: string; description: string; order_index: number }>): Promise<RulebookTopic> {
return apiPatch(`/api/rulebook-topics/${id}`, data);
}
export async function deleteTopic(id: number): Promise<void> {
return apiDelete(`/api/rulebook-topics/${id}`);
}
// ── Rules ──────────────────────────────────────────────────────────
export async function listRules(filters: { rulebook_id?: number; topic_id?: number; project_id?: number } = {}): Promise<Rule[]> {
const params = new URLSearchParams();
if (filters.rulebook_id) params.set("rulebook_id", String(filters.rulebook_id));
if (filters.topic_id) params.set("topic_id", String(filters.topic_id));
if (filters.project_id) params.set("project_id", String(filters.project_id));
const qs = params.toString();
const data = await apiGet<{ rules: Rule[] }>(`/api/rules${qs ? `?${qs}` : ""}`);
return data.rules;
}
export async function getRule(id: number): Promise<Rule> {
return apiGet(`/api/rules/${id}`);
}
export async function createRule(topicId: number, data: { title: string; statement: string; why?: string; how_to_apply?: string; order_index?: number }): Promise<Rule> {
return apiPost(`/api/rulebook-topics/${topicId}/rules`, data);
}
export async function updateRule(id: number, data: Partial<{ title: string; statement: string; why: string; how_to_apply: string; order_index: number }>): Promise<Rule> {
return apiPatch(`/api/rules/${id}`, data);
}
export async function deleteRule(id: number): Promise<void> {
return apiDelete(`/api/rules/${id}`);
}
// ── Subscriptions ──────────────────────────────────────────────────
export async function subscribeProject(projectId: number, rulebookId: number): Promise<void> {
await apiPost(`/api/projects/${projectId}/rulebook-subscriptions`, { rulebook_id: rulebookId });
}
export async function unsubscribeProject(projectId: number, rulebookId: number): Promise<void> {
return apiDelete(`/api/projects/${projectId}/rulebook-subscriptions/${rulebookId}`);
}
export async function getProjectApplicableRules(projectId: number): Promise<ApplicableRules> {
return apiGet(`/api/projects/${projectId}/rules`);
}
+128
View File
@@ -0,0 +1,128 @@
import { ref } from "vue";
import { defineStore } from "pinia";
import * as api from "@/api/rulebooks";
import type { Rulebook, RulebookTopic, Rule, RuleHeader } from "@/api/rulebooks";
import { useToastStore } from "@/stores/toast";
export const useRulebooksStore = defineStore("rulebooks", () => {
const rulebooks = ref<Rulebook[]>([]);
const topicsByRulebook = ref<Record<number, RulebookTopic[]>>({});
const rulesByTopic = ref<Record<number, RuleHeader[]>>({});
const currentRule = ref<Rule | null>(null);
const loading = ref(false);
async function fetchRulebooks() {
loading.value = true;
try {
rulebooks.value = await api.listRulebooks();
} catch (e) {
useToastStore().show("Failed to load rulebooks", "error");
throw e;
} finally {
loading.value = false;
}
}
async function fetchTopics(rulebookId: number) {
try {
topicsByRulebook.value[rulebookId] = await api.listTopics(rulebookId);
} catch (e) {
useToastStore().show("Failed to load topics", "error");
throw e;
}
}
async function fetchRules(topicId: number) {
try {
const rules = await api.listRules({ topic_id: topicId });
rulesByTopic.value[topicId] = rules.map((r) => ({
id: r.id, title: r.title, statement: r.statement, topic_id: r.topic_id,
}));
} catch (e) {
useToastStore().show("Failed to load rules", "error");
throw e;
}
}
async function fetchRule(id: number) {
currentRule.value = await api.getRule(id);
}
async function createRulebook(data: { title: string; description?: string }) {
const rb = await api.createRulebook(data);
rulebooks.value.push(rb);
return rb;
}
async function updateRulebook(id: number, data: Partial<Pick<Rulebook, "title" | "description">>) {
const rb = await api.updateRulebook(id, data);
const idx = rulebooks.value.findIndex((r) => r.id === id);
if (idx >= 0) rulebooks.value[idx] = rb;
return rb;
}
async function deleteRulebook(id: number) {
await api.deleteRulebook(id);
rulebooks.value = rulebooks.value.filter((r) => r.id !== id);
delete topicsByRulebook.value[id];
}
async function createTopic(rulebookId: number, data: { title: string; description?: string }) {
const topic = await api.createTopic(rulebookId, data);
if (!topicsByRulebook.value[rulebookId]) topicsByRulebook.value[rulebookId] = [];
topicsByRulebook.value[rulebookId].push(topic);
return topic;
}
async function updateTopic(id: number, data: Partial<Pick<RulebookTopic, "title" | "description" | "order_index">>) {
const topic = await api.updateTopic(id, data);
for (const rbId of Object.keys(topicsByRulebook.value)) {
const list = topicsByRulebook.value[Number(rbId)];
const idx = list.findIndex((t) => t.id === id);
if (idx >= 0) list[idx] = topic;
}
return topic;
}
async function deleteTopic(id: number) {
await api.deleteTopic(id);
for (const rbId of Object.keys(topicsByRulebook.value)) {
topicsByRulebook.value[Number(rbId)] = topicsByRulebook.value[Number(rbId)].filter((t) => t.id !== id);
}
delete rulesByTopic.value[id];
}
async function createRule(topicId: number, data: { title: string; statement: string; why?: string; how_to_apply?: string }) {
const rule = await api.createRule(topicId, data);
if (!rulesByTopic.value[topicId]) rulesByTopic.value[topicId] = [];
rulesByTopic.value[topicId].push({ id: rule.id, title: rule.title, statement: rule.statement, topic_id: rule.topic_id });
return rule;
}
async function updateRule(id: number, data: Partial<Pick<Rule, "title" | "statement" | "why" | "how_to_apply" | "order_index">>) {
const rule = await api.updateRule(id, data);
if (currentRule.value?.id === id) currentRule.value = rule;
for (const tid of Object.keys(rulesByTopic.value)) {
const list = rulesByTopic.value[Number(tid)];
const idx = list.findIndex((r) => r.id === id);
if (idx >= 0) list[idx] = { id: rule.id, title: rule.title, statement: rule.statement, topic_id: rule.topic_id };
}
return rule;
}
async function deleteRule(id: number) {
await api.deleteRule(id);
if (currentRule.value?.id === id) currentRule.value = null;
for (const tid of Object.keys(rulesByTopic.value)) {
rulesByTopic.value[Number(tid)] = rulesByTopic.value[Number(tid)].filter((r) => r.id !== id);
}
}
return {
rulebooks, topicsByRulebook, rulesByTopic, currentRule, loading,
fetchRulebooks, fetchTopics, fetchRules, fetchRule,
createRulebook, updateRulebook, deleteRulebook,
createTopic, updateTopic, deleteTopic,
createRule, updateRule, deleteRule,
};
});