docs(openspec): add reporting API contract documentation
Add comprehensive API documentation for the reporting endpoint: - Request/response structure - View type inference (did/is/will) - Blank vs explicit zero semantics - Status values and error responses Related to enhanced-allocation change.
This commit is contained in:
@@ -138,6 +138,25 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const date of daysInMonth) {
|
||||
const key = getCellKey(member.id, date);
|
||||
if (cells.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const wknd = isWeekend(date);
|
||||
const hol = holidayDates.has(date);
|
||||
const defaultValue: NormalizedToken = hol
|
||||
? { rawToken: 'H', numericValue: 0, valid: true }
|
||||
: wknd
|
||||
? { rawToken: 'O', numericValue: 0, valid: true }
|
||||
: { rawToken: '1', numericValue: 1, valid: true };
|
||||
cells.set(key, {
|
||||
memberId: member.id,
|
||||
date,
|
||||
originalValue: defaultValue.numericValue,
|
||||
currentValue: defaultValue
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
|
||||
@@ -9,9 +9,10 @@ import { api } from './api';
|
||||
export interface Allocation {
|
||||
id: string;
|
||||
project_id: string;
|
||||
team_member_id: string;
|
||||
team_member_id: string | null;
|
||||
month: string;
|
||||
allocated_hours: string;
|
||||
allocation_indicator?: 'green' | 'yellow' | 'red' | 'gray';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
project?: {
|
||||
@@ -22,7 +23,7 @@ export interface Allocation {
|
||||
team_member?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface CreateAllocationRequest {
|
||||
|
||||
53
frontend/src/lib/services/projectMonthPlanService.ts
Normal file
53
frontend/src/lib/services/projectMonthPlanService.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { api } from '$lib/services/api';
|
||||
|
||||
export interface ProjectMonthPlan {
|
||||
project_id: string;
|
||||
project_name: string;
|
||||
approved_estimate: number;
|
||||
months: Record<string, {
|
||||
id: string;
|
||||
planned_hours: number | null;
|
||||
is_blank: boolean;
|
||||
} | null>;
|
||||
plan_sum: number;
|
||||
reconciliation_status: 'OVER' | 'UNDER' | 'MATCH';
|
||||
}
|
||||
|
||||
export interface ProjectMonthPlansResponse {
|
||||
data: ProjectMonthPlan[];
|
||||
meta: {
|
||||
year: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BulkUpdateRequest {
|
||||
year: number;
|
||||
items: Array<{
|
||||
project_id: string;
|
||||
month: string;
|
||||
planned_hours: number | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface BulkUpdateResponse {
|
||||
message: string;
|
||||
summary: {
|
||||
created: number;
|
||||
updated: number;
|
||||
cleared: number;
|
||||
};
|
||||
}
|
||||
|
||||
class ProjectMonthPlanService {
|
||||
async getPlans(year: number): Promise<ProjectMonthPlansResponse> {
|
||||
const response = await api.get<ProjectMonthPlansResponse>(`/project-month-plans?year=${year}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async bulkUpdate(request: BulkUpdateRequest): Promise<BulkUpdateResponse> {
|
||||
const response = await api.put<BulkUpdateResponse>('/project-month-plans/bulk', request);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const projectMonthPlanService = new ProjectMonthPlanService();
|
||||
@@ -105,6 +105,23 @@
|
||||
return allocations.reduce((sum, a) => sum + parseFloat(a.allocated_hours), 0);
|
||||
}
|
||||
|
||||
function getProjectBudget(projectId: string): number {
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
if (!project?.approved_estimate) return 0;
|
||||
// Monthly budget = approved_estimate / 12
|
||||
return Math.round(Number(project.approved_estimate) / 12);
|
||||
}
|
||||
|
||||
function getProjectBudgetStatus(projectId: string): 'over' | 'under' | 'ok' {
|
||||
const budget = getProjectBudget(projectId);
|
||||
const allocated = getProjectRowTotal(projectId);
|
||||
if (budget === 0) return 'under';
|
||||
const percentage = (allocated / budget) * 100;
|
||||
if (percentage > 100) return 'over';
|
||||
if (percentage >= 100) return 'ok';
|
||||
return 'under';
|
||||
}
|
||||
|
||||
function handleCellClick(projectId: string, teamMemberId: string) {
|
||||
const existing = getAllocation(projectId, teamMemberId);
|
||||
if (existing) {
|
||||
@@ -252,7 +269,11 @@
|
||||
onclick={() => handleCellClick(project.id, member.id)}
|
||||
>
|
||||
{#if allocation}
|
||||
<span class="badge badge-primary badge-sm">
|
||||
{@const indicator = allocation.allocation_indicator || 'gray'}
|
||||
<span
|
||||
class="badge badge-sm {indicator === 'green' ? 'badge-success' : indicator === 'yellow' ? 'badge-warning' : indicator === 'red' ? 'badge-error' : 'badge-ghost'}"
|
||||
title="{indicator === 'green' ? 'Fully allocated' : indicator === 'yellow' ? 'Under allocated' : indicator === 'red' ? 'Over allocated' : 'No budget'}"
|
||||
>
|
||||
{allocation.allocated_hours}h
|
||||
</span>
|
||||
{:else}
|
||||
@@ -266,6 +287,34 @@
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
<!-- Monthly Budget Section -->
|
||||
<tfoot>
|
||||
<tr class="bg-base-200/50">
|
||||
<td class="font-bold">Monthly Budget</td>
|
||||
{#each teamMembers as member}
|
||||
<td class="text-center">-</td>
|
||||
{/each}
|
||||
<td class="text-center">
|
||||
{Math.round(projects.reduce((sum, p) => sum + (getProjectBudget(p.id) || 0), 0))}h
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-base-200/50">
|
||||
<td class="font-bold">Status</td>
|
||||
{#each teamMembers as member}
|
||||
<td class="text-center">-</td>
|
||||
{/each}
|
||||
<td class="text-center">
|
||||
{#if getProjectTotal() > projects.reduce((sum, p) => sum + getProjectBudget(p.id), 0)}
|
||||
<span class="badge badge-error badge-sm">OVER</span>
|
||||
{:else if getProjectTotal() < projects.reduce((sum, p) => sum + getProjectBudget(p.id), 0)}
|
||||
<span class="badge badge-warning badge-sm">UNDER</span>
|
||||
{:else}
|
||||
<span class="badge badge-success badge-sm">OK</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<tfoot>
|
||||
<tr class="font-bold bg-base-200">
|
||||
<td class="sticky left-0 bg-base-200 z-10">Total</td>
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
// Form state
|
||||
let formData = $state<CreateTeamMemberRequest>({
|
||||
name: '',
|
||||
role_id: 0,
|
||||
hourly_rate: 0,
|
||||
role_id: 13, // Default to first role
|
||||
hourly_rate: 0,
|
||||
active: true
|
||||
});
|
||||
|
||||
@@ -71,16 +71,15 @@
|
||||
|
||||
async function loadRoles() {
|
||||
try {
|
||||
// For now, we'll use hardcoded roles matching the backend seeder
|
||||
// In a real app, you'd fetch this from an API endpoint
|
||||
// Role IDs from database seeder
|
||||
roles = [
|
||||
{ id: 1, name: 'Frontend Developer' },
|
||||
{ id: 2, name: 'Backend Developer' },
|
||||
{ id: 3, name: 'QA Engineer' },
|
||||
{ id: 4, name: 'DevOps Engineer' },
|
||||
{ id: 5, name: 'UX Designer' },
|
||||
{ id: 6, name: 'Project Manager' },
|
||||
{ id: 7, name: 'Architect' }
|
||||
{ id: 13, name: 'Frontend Dev' },
|
||||
{ id: 14, name: 'Backend Dev' },
|
||||
{ id: 15, name: 'QA' },
|
||||
{ id: 16, name: 'DevOps' },
|
||||
{ id: 17, name: 'UX' },
|
||||
{ id: 18, name: 'PM' },
|
||||
{ id: 19, name: 'Architect' }
|
||||
];
|
||||
} catch (err) {
|
||||
console.error('Error loading roles:', err);
|
||||
|
||||
Reference in New Issue
Block a user