feat: scaffold offline-first mobile app (RN+Expo, expo-sqlite)
Capa API tipada de los 8 endpoints, BD local espejo del bundle + outbox (operaciones y media) + cursor de sync, motor runSync (PUSH /sync -> PUSH /media -> PULL bundle?since) con idempotencia por uuid y last-write-wins, mutaciones de alto nivel (write local + encolar), sesion con token en SecureStore, conectividad NetInfo y UI minima (Login -> Proyectos -> Detalle). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3f454b59a5
commit
4e9c7d059f
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Mutaciones de alto nivel. Cada una:
|
||||
* 1. (updates) escribe optimistamente en la BD local para reflejar el cambio ya,
|
||||
* 2. encola la operación en el outbox para enviarla cuando haya red.
|
||||
*
|
||||
* El servidor SIEMPRE fija user_id/reported_by/project_id y valida permisos:
|
||||
* el cliente nunca los envía (ver brief §5.1).
|
||||
*/
|
||||
import { getDb } from '../db/database';
|
||||
import { enqueueOperation } from '../db/outbox';
|
||||
import {
|
||||
IssuePriority,
|
||||
IssueStatus,
|
||||
IssueType,
|
||||
} from '../api/types';
|
||||
|
||||
// ----- progress (append-only en servidor) -----
|
||||
|
||||
export function recordProgressUpdate(input: {
|
||||
phase_id: number;
|
||||
progress: number;
|
||||
comment?: string;
|
||||
location?: { lat: number; lng: number };
|
||||
}): Promise<string> {
|
||||
return enqueueOperation('progress_update', 'create', { ...input });
|
||||
}
|
||||
|
||||
// ----- feature.update (editable, last-write-wins) -----
|
||||
|
||||
export async function updateFeature(input: {
|
||||
id: number;
|
||||
status?: string;
|
||||
progress?: number;
|
||||
responsible?: string;
|
||||
}): Promise<string> {
|
||||
const db = await getDb();
|
||||
const sets: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
if (input.status !== undefined) {
|
||||
sets.push('status = ?');
|
||||
params.push(input.status);
|
||||
}
|
||||
if (input.progress !== undefined) {
|
||||
sets.push('progress = ?');
|
||||
params.push(input.progress);
|
||||
}
|
||||
if (input.responsible !== undefined) {
|
||||
sets.push('responsible = ?');
|
||||
params.push(input.responsible);
|
||||
}
|
||||
if (sets.length) {
|
||||
params.push(input.id);
|
||||
await db.runAsync(
|
||||
`UPDATE features SET ${sets.join(', ')}, dirty = 1 WHERE id = ?`,
|
||||
...(params as never[]),
|
||||
);
|
||||
}
|
||||
return enqueueOperation('feature', 'update', { ...input });
|
||||
}
|
||||
|
||||
// ----- inspection.create (append-only) -----
|
||||
|
||||
export function createInspection(input: {
|
||||
feature_id: number;
|
||||
template_id?: number;
|
||||
data?: Record<string, unknown>;
|
||||
status?: string;
|
||||
result?: string;
|
||||
notes?: string;
|
||||
}): Promise<string> {
|
||||
return enqueueOperation('inspection', 'create', { ...input });
|
||||
}
|
||||
|
||||
// ----- issues -----
|
||||
|
||||
export function createIssue(input: {
|
||||
project_id: number;
|
||||
feature_id?: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
priority?: IssuePriority;
|
||||
status?: IssueStatus;
|
||||
type?: IssueType;
|
||||
}): Promise<string> {
|
||||
return enqueueOperation('issue', 'create', { ...input });
|
||||
}
|
||||
|
||||
export async function updateIssue(input: {
|
||||
id: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
priority?: IssuePriority;
|
||||
status?: IssueStatus;
|
||||
type?: IssueType;
|
||||
assigned_to?: number;
|
||||
resolution_notes?: string;
|
||||
}): Promise<string> {
|
||||
const db = await getDb();
|
||||
const cols: Record<string, unknown> = {
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
priority: input.priority,
|
||||
status: input.status,
|
||||
type: input.type,
|
||||
assigned_to: input.assigned_to,
|
||||
resolution_notes: input.resolution_notes,
|
||||
};
|
||||
const sets: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
for (const [k, v] of Object.entries(cols)) {
|
||||
if (v !== undefined) {
|
||||
sets.push(`${k} = ?`);
|
||||
params.push(v);
|
||||
}
|
||||
}
|
||||
if (sets.length) {
|
||||
params.push(input.id);
|
||||
await db.runAsync(
|
||||
`UPDATE issues SET ${sets.join(', ')}, dirty = 1 WHERE id = ?`,
|
||||
...(params as never[]),
|
||||
);
|
||||
}
|
||||
return enqueueOperation('issue', 'update', { ...input });
|
||||
}
|
||||
|
||||
// ----- issue tasks -----
|
||||
|
||||
export function createIssueTask(input: {
|
||||
issue_id: number;
|
||||
title: string;
|
||||
assigned_to?: number;
|
||||
due_date?: string;
|
||||
is_done?: boolean;
|
||||
}): Promise<string> {
|
||||
return enqueueOperation('issue_task', 'create', { ...input });
|
||||
}
|
||||
|
||||
export async function updateIssueTask(input: {
|
||||
id: number;
|
||||
title?: string;
|
||||
assigned_to?: number;
|
||||
due_date?: string;
|
||||
is_done?: boolean;
|
||||
}): Promise<string> {
|
||||
const db = await getDb();
|
||||
if (input.is_done !== undefined) {
|
||||
await db.runAsync(
|
||||
'UPDATE issue_tasks SET is_done = ?, dirty = 1 WHERE id = ?',
|
||||
input.is_done ? 1 : 0,
|
||||
input.id,
|
||||
);
|
||||
}
|
||||
return enqueueOperation('issue_task', 'update', { ...input });
|
||||
}
|
||||
|
||||
// ----- issue comments (append-only) -----
|
||||
|
||||
export function createIssueComment(input: {
|
||||
issue_id: number;
|
||||
body: string;
|
||||
}): Promise<string> {
|
||||
return enqueueOperation('issue_comment', 'create', { ...input });
|
||||
}
|
||||
Reference in New Issue
Block a user