システム管理ストア
:LiTarget: 用途
システムの実行状態・ステータスを localStorage / KV にキャッシュして管理。
:LiSparkle: 特徴
- localStorage連携
- KV連携
- 型安全
- リアルタイム更新
:LiCode: 実コード(SCALE Base より自動抽出)
:LiInfo:
lib/system-mgmt-store.tsの中身そのもの。コピペ即可。
// システム管理: PJT・タスクの localStorage CRUD フック。
// 1アカウント=1デバイス前提のため KV / D1 同期はしない。
// データ構造: System > Project (PJT) > Task
'use client';
import { useEffect, useState, useCallback } from 'react';
export type TaskStatus = 'todo' | 'requested' | 'fixing' | 'done';
export type TaskPriority = 'P0' | 'P1' | 'P2' | 'P3';
export const STATUS_LABEL: Record<TaskStatus, string> = {
todo: '未依頼',
requested: '依頼中',
fixing: '修正中',
done: '完了',
};
export const STATUS_COLOR: Record<TaskStatus, string> = {
todo: '#94a3b8',
requested: '#3b82f6',
fixing: '#f59e0b',
done: '#10b981',
};
export const STATUS_ORDER: TaskStatus[] = ['todo', 'requested', 'fixing', 'done'];
export const PRIORITY_COLOR: Record<TaskPriority, string> = {
P0: '#ef4444',
P1: '#f59e0b',
P2: '#3b82f6',
P3: '#94a3b8',
};
export interface SystemTask {
id: string;
systemId: string;
projectId: string;
title: string;
rawNote: string;
refinedPrompt: string;
status: TaskStatus;
priority: TaskPriority;
createdAt: string;
updatedAt: string;
requestedAt?: string;
completedAt?: string;
}
export interface SystemProject {
id: string;
systemId: string;
name: string;
order: number;
createdAt: string;
}
const TASK_KEY = 'scale-base-system-mgmt-tasks-v1';
const PROJECT_KEY = 'scale-base-system-mgmt-projects-v1';
const DEFAULT_PROJECT_NAME = '全般';
function uid(): string {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
}
function nowIso(): string {
return new Date().toISOString();
}
// ----- Migration: 旧ステータス → 新ステータス & projectId 補完 -----
function migrateTask(t: any): SystemTask {
const oldStatus = t.status;
let status: TaskStatus = 'todo';
if (oldStatus === 'doing' || oldStatus === 'requested') status = 'requested';
else if (oldStatus === 'fixing') status = 'fixing';
else if (oldStatus === 'done') status = 'done';
else status = 'todo';
return {
id: t.id || uid(),
systemId: t.systemId,
projectId: t.projectId || `__default__${t.systemId}`,
title: t.title || '',
rawNote: t.rawNote || '',
refinedPrompt: t.refinedPrompt || '',
status,
priority: t.priority || 'P2',
createdAt: t.createdAt || nowIso(),
updatedAt: t.updatedAt || nowIso(),
requestedAt: t.requestedAt,
completedAt: t.completedAt,
};
}
function readTasks(): SystemTask[] {
if (typeof window === 'undefined') return [];
try {
const raw = localStorage.getItem(TASK_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return [];
return parsed.map(migrateTask);
} catch {
return [];
}
}
function writeTasks(tasks: SystemTask[]): void {
if (typeof window === 'undefined') return;
localStorage.setItem(TASK_KEY, JSON.stringify(tasks));
}
function readProjects(): SystemProject[] {
if (typeof window === 'undefined') return [];
try {
const raw = localStorage.getItem(PROJECT_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
function writeProjects(projects: SystemProject[]): void {
if (typeof window === 'undefined') return;
localStorage.setItem(PROJECT_KEY, JSON.stringify(projects));
}
// systemId に対するデフォルトPJTがなければ作成
function ensureDefaultProject(systemId: string): SystemProject {
const all = readProjects();
const existing = all.find((p) => p.systemId === systemId);
if (existing) return existing;
const def: SystemProject = {
id: `__default__${systemId}`,
systemId,
name: DEFAULT_PROJECT_NAME,
order: 0,
createdAt: nowIso(),
};
writeProjects([...all, def]);
return def;
}
export function useSystemMgmt(systemId: string) {
const [tasks, setTasks] = useState<SystemTask[]>([]);
const [projects, setProjects] = useState<SystemProject[]>([]);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
ensureDefaultProject(systemId);
setTasks(readTasks().filter((t) => t.systemId === systemId));
setProjects(readProjects().filter((p) => p.systemId === systemId).sort((a, b) => a.order - b.order));
setLoaded(true);
}, [systemId]);
const refresh = useCallback(() => {
setTasks(readTasks().filter((t) => t.systemId === systemId));
setProjects(readProjects().filter((p) => p.systemId === systemId).sort((a, b) => a.order - b.order));
}, [systemId]);
// ---- Project ops ----
const addProject = useCallback(
(name: string) => {
const trimmed = name.trim();
if (!trimmed) return null;
const all = readProjects();
const existing = all.find((p) => p.systemId === systemId && p.name === trimmed);
if (existing) return existing;
const sysProjects = all.filter((p) => p.systemId === systemId);
const next: SystemProject = {
id: uid(),
systemId,
name: trimmed,
order: sysProjects.length,
createdAt: nowIso(),
};
writeProjects([...all, next]);
refresh();
return next;
},
[systemId, refresh]
);
const renameProject = useCallback(
(projectId: string, name: string) => {
const trimmed = name.trim();
if (!trimmed) return;
const all = readProjects();
writeProjects(all.map((p) => (p.id === projectId ? { ...p, name: trimmed } : p)));
refresh();
},
[refresh]
);
const deleteProject = useCallback(
(projectId: string) => {
const allProjects = readProjects();
const target = allProjects.find((p) => p.id === projectId);
if (!target) return;
// タスクをデフォルトに移動
const def = ensureDefaultProject(target.systemId);
const allTasks = readTasks().map((t) =>
t.projectId === projectId ? { ...t, projectId: def.id, updatedAt: nowIso() } : t
);
writeTasks(allTasks);
writeProjects(allProjects.filter((p) => p.id !== projectId));
refresh();
},
[refresh]
);
// ---- Task ops ----
const addTask = useCallback(
(input: { projectId: string; title: string; rawNote: string; refinedPrompt: string; priority?: TaskPriority }) => {
const now = nowIso();
const next: SystemTask = {
id: uid(),
systemId,
projectId: input.projectId,
title: input.title.trim(),
rawNote: input.rawNote || '',
refinedPrompt: input.refinedPrompt || '',
status: 'todo',
priority: input.priority || 'P2',
createdAt: now,
updatedAt: now,
};
writeTasks([next, ...readTasks()]);
refresh();
return next;
},
[systemId, refresh]
);
const addManyTasks = useCallback(
(
projectId: string,
items: Array<{ title: string; rawNote: string; refinedPrompt: string; priority?: TaskPriority }>
) => {
const now = nowIso();
const created: SystemTask[] = items.map((it) => ({
id: uid(),
systemId,
projectId,
title: it.title.trim(),
rawNote: it.rawNote || '',
refinedPrompt: it.refinedPrompt || '',
status: 'todo',
priority: it.priority || 'P2',
createdAt: now,
updatedAt: now,
}));
writeTasks([...created, ...readTasks()]);
refresh();
return created;
},
[systemId, refresh]
);
const updateTask = useCallback(
(id: string, patch: Partial<SystemTask>) => {
const now = nowIso();
const next = readTasks().map((t) => {
if (t.id !== id) return t;
const merged: SystemTask = { ...t, ...patch, updatedAt: now };
// ステータス連動タイムスタンプ
if (patch.status === 'done') merged.completedAt = now;
else if (patch.status) merged.completedAt = undefined;
if (patch.status === 'requested' && !merged.requestedAt) merged.requestedAt = now;
return merged;
});
writeTasks(next);
refresh();
},
[refresh]
);
const deleteTask = useCallback(
(id: string) => {
writeTasks(readTasks().filter((t) => t.id !== id));
refresh();
},
[refresh]
);
// 一括ステータス更新(todo→requested など)
const bulkUpdateStatus = useCallback(
(ids: string[], status: TaskStatus) => {
const idSet = new Set(ids);
const now = nowIso();
const next = readTasks().map((t) => {
if (!idSet.has(t.id)) return t;
const merged: SystemTask = { ...t, status, updatedAt: now };
if (status === 'done') merged.completedAt = now;
else merged.completedAt = undefined;
if (status === 'requested' && !merged.requestedAt) merged.requestedAt = now;
return merged;
});
writeTasks(next);
refresh();
},
[refresh]
);
return {
tasks,
projects,
loaded,
addProject,
renameProject,
deleteProject,
addTask,
addManyTasks,
updateTask,
deleteTask,
bulkUpdateStatus,
refresh,
};
}
// 一覧画面用: システムごとのタスク件数集計
export function countTasksBySystem(): Record<
string,
{ total: number; todo: number; requested: number; fixing: number; done: number; lastUpdated?: string }
> {
if (typeof window === 'undefined') return {};
const all = readTasks();
const counts: Record<
string,
{ total: number; todo: number; requested: number; fixing: number; done: number; lastUpdated?: string }
> = {};
for (const t of all) {
if (!counts[t.systemId])
counts[t.systemId] = { total: 0, todo: 0, requested: 0, fixing: 0, done: 0 };
counts[t.systemId].total++;
counts[t.systemId][t.status]++;
const last = counts[t.systemId].lastUpdated;
if (!last || t.updatedAt > last) counts[t.systemId].lastUpdated = t.updatedAt;
}
return counts;
}
:LiFolder: ソースファイルのパス
/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/lib/system-mgmt-store.ts
:LiHandPointer: 使い方
対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。
:LiAlertCircle: 注意事項
- 依存パッケージを忘れず追加