Compare commits
1 Commits
b13fbfe8c7
...
fix/viewer
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d137a40d3 |
@@ -587,7 +587,7 @@ export function AccountsPage() {
|
||||
{investments.filter(i => i.is_active).length > 0 && (
|
||||
<>
|
||||
<Divider label="Investment Accounts" labelPosition="center" my="xs" />
|
||||
<InvestmentMiniTable investments={investments.filter(i => i.is_active)} onEdit={handleEditInvestment} />
|
||||
<InvestmentMiniTable investments={investments.filter(i => i.is_active)} onEdit={handleEditInvestment} isReadOnly={isReadOnly} />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -605,7 +605,7 @@ export function AccountsPage() {
|
||||
{operatingInvestments.length > 0 && (
|
||||
<>
|
||||
<Divider label="Operating Investment Accounts" labelPosition="center" my="xs" />
|
||||
<InvestmentMiniTable investments={operatingInvestments} onEdit={handleEditInvestment} />
|
||||
<InvestmentMiniTable investments={operatingInvestments} onEdit={handleEditInvestment} isReadOnly={isReadOnly} />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -623,7 +623,7 @@ export function AccountsPage() {
|
||||
{reserveInvestments.length > 0 && (
|
||||
<>
|
||||
<Divider label="Reserve Investment Accounts" labelPosition="center" my="xs" />
|
||||
<InvestmentMiniTable investments={reserveInvestments} onEdit={handleEditInvestment} />
|
||||
<InvestmentMiniTable investments={reserveInvestments} onEdit={handleEditInvestment} isReadOnly={isReadOnly} />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -1087,9 +1087,11 @@ function AccountTable({
|
||||
function InvestmentMiniTable({
|
||||
investments,
|
||||
onEdit,
|
||||
isReadOnly = false,
|
||||
}: {
|
||||
investments: Investment[];
|
||||
onEdit: (inv: Investment) => void;
|
||||
isReadOnly?: boolean;
|
||||
}) {
|
||||
const totalPrincipal = investments.reduce((s, i) => s + parseFloat(i.principal || '0'), 0);
|
||||
const totalValue = investments.reduce(
|
||||
@@ -1132,7 +1134,7 @@ function InvestmentMiniTable({
|
||||
<Table.Th ta="right">Maturity Value</Table.Th>
|
||||
<Table.Th>Maturity Date</Table.Th>
|
||||
<Table.Th ta="right">Days Remaining</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
{!isReadOnly && <Table.Th></Table.Th>}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
@@ -1182,6 +1184,7 @@ function InvestmentMiniTable({
|
||||
'-'
|
||||
)}
|
||||
</Table.Td>
|
||||
{!isReadOnly && (
|
||||
<Table.Td>
|
||||
<Tooltip label="Edit investment">
|
||||
<ActionIcon variant="subtle" onClick={() => onEdit(inv)}>
|
||||
@@ -1189,6 +1192,7 @@ function InvestmentMiniTable({
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Table.Td>
|
||||
)}
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
|
||||
@@ -72,9 +72,10 @@ interface KanbanCardProps {
|
||||
project: Project;
|
||||
onEdit: (p: Project) => void;
|
||||
onDragStart: (e: DragEvent<HTMLDivElement>, project: Project) => void;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
function KanbanCard({ project, onEdit, onDragStart }: KanbanCardProps) {
|
||||
function KanbanCard({ project, onEdit, onDragStart, isReadOnly }: KanbanCardProps) {
|
||||
const plannedLabel = formatPlannedDate(project.planned_date);
|
||||
// For projects in the Future bucket with a specific year, show the year
|
||||
const currentYear = new Date().getFullYear();
|
||||
@@ -86,21 +87,23 @@ function KanbanCard({ project, onEdit, onDragStart }: KanbanCardProps) {
|
||||
padding="sm"
|
||||
radius="md"
|
||||
withBorder
|
||||
draggable
|
||||
onDragStart={(e) => onDragStart(e, project)}
|
||||
style={{ cursor: 'grab', userSelect: 'none' }}
|
||||
draggable={!isReadOnly}
|
||||
onDragStart={!isReadOnly ? (e) => onDragStart(e, project) : undefined}
|
||||
style={{ cursor: isReadOnly ? 'default' : 'grab', userSelect: 'none' }}
|
||||
mb="xs"
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap" mb={4}>
|
||||
<Group gap={6} wrap="nowrap" style={{ overflow: 'hidden' }}>
|
||||
<IconGripVertical size={14} style={{ flexShrink: 0, color: 'var(--mantine-color-dimmed)' }} />
|
||||
{!isReadOnly && <IconGripVertical size={14} style={{ flexShrink: 0, color: 'var(--mantine-color-dimmed)' }} />}
|
||||
<Text fw={600} size="sm" truncate>
|
||||
{project.name}
|
||||
</Text>
|
||||
</Group>
|
||||
{!isReadOnly && (
|
||||
<ActionIcon variant="subtle" size="sm" onClick={() => onEdit(project)}>
|
||||
<IconEdit size={14} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Group gap={6} mb={6}>
|
||||
@@ -148,11 +151,12 @@ interface KanbanColumnProps {
|
||||
isDragOver: boolean;
|
||||
onDragOverHandler: (e: DragEvent<HTMLDivElement>, year: number) => void;
|
||||
onDragLeave: () => void;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
function KanbanColumn({
|
||||
year, projects, onEdit, onDragStart, onDrop,
|
||||
isDragOver, onDragOverHandler, onDragLeave,
|
||||
isDragOver, onDragOverHandler, onDragLeave, isReadOnly,
|
||||
}: KanbanColumnProps) {
|
||||
const totalEst = projects.reduce((s, p) => s + parseFloat(p.estimated_cost || '0'), 0);
|
||||
const isFuture = year === FUTURE_YEAR;
|
||||
@@ -178,9 +182,9 @@ function KanbanColumn({
|
||||
border: isDragOver ? '2px dashed var(--mantine-color-blue-4)' : undefined,
|
||||
transition: 'background-color 150ms ease, border 150ms ease',
|
||||
}}
|
||||
onDragOver={(e) => onDragOverHandler(e, year)}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={(e) => onDrop(e, year)}
|
||||
onDragOver={!isReadOnly ? (e) => onDragOverHandler(e, year) : undefined}
|
||||
onDragLeave={!isReadOnly ? onDragLeave : undefined}
|
||||
onDrop={!isReadOnly ? (e) => onDrop(e, year) : undefined}
|
||||
>
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Title order={5}>{yearLabel(year)}</Title>
|
||||
@@ -199,7 +203,7 @@ function KanbanColumn({
|
||||
<Box style={{ flex: 1, minHeight: 60 }}>
|
||||
{projects.length === 0 ? (
|
||||
<Text size="xs" c="dimmed" ta="center" py="lg">
|
||||
Drop projects here
|
||||
{isReadOnly ? 'No projects' : 'Drop projects here'}
|
||||
</Text>
|
||||
) : useWideLayout ? (
|
||||
<div style={{
|
||||
@@ -208,12 +212,12 @@ function KanbanColumn({
|
||||
gap: 'var(--mantine-spacing-xs)',
|
||||
}}>
|
||||
{projects.map((p) => (
|
||||
<KanbanCard key={p.id} project={p} onEdit={onEdit} onDragStart={onDragStart} />
|
||||
<KanbanCard key={p.id} project={p} onEdit={onEdit} onDragStart={onDragStart} isReadOnly={isReadOnly} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
projects.map((p) => (
|
||||
<KanbanCard key={p.id} project={p} onEdit={onEdit} onDragStart={onDragStart} />
|
||||
<KanbanCard key={p.id} project={p} onEdit={onEdit} onDragStart={onDragStart} isReadOnly={isReadOnly} />
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
@@ -595,6 +599,7 @@ export function CapitalProjectsPage() {
|
||||
isDragOver={dragOverYear === year}
|
||||
onDragOverHandler={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { useAuthStore, useIsReadOnly } from '../../stores/authStore';
|
||||
import api from '../../services/api';
|
||||
|
||||
interface HealthScore {
|
||||
@@ -311,6 +311,7 @@ interface DashboardData {
|
||||
|
||||
export function DashboardPage() {
|
||||
const currentOrg = useAuthStore((s) => s.currentOrg);
|
||||
const isReadOnly = useIsReadOnly();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Track whether a refresh is in progress (per score type) for async polling
|
||||
@@ -424,7 +425,7 @@ export function DashboardPage() {
|
||||
</ThemeIcon>
|
||||
}
|
||||
isRefreshing={operatingRefreshing}
|
||||
onRefresh={handleRefreshOperating}
|
||||
onRefresh={!isReadOnly ? handleRefreshOperating : undefined}
|
||||
lastFailed={!!healthScores?.operating_last_failed}
|
||||
/>
|
||||
<HealthScoreCard
|
||||
@@ -436,7 +437,7 @@ export function DashboardPage() {
|
||||
</ThemeIcon>
|
||||
}
|
||||
isRefreshing={reserveRefreshing}
|
||||
onRefresh={handleRefreshReserve}
|
||||
onRefresh={!isReadOnly ? handleRefreshReserve : undefined}
|
||||
lastFailed={!!healthScores?.reserve_last_failed}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import api from '../../services/api';
|
||||
import { useIsReadOnly } from '../../stores/authStore';
|
||||
|
||||
// ── Types ──
|
||||
|
||||
@@ -347,6 +348,7 @@ function RecommendationsDisplay({
|
||||
export function InvestmentPlanningPage() {
|
||||
const [ratesExpanded, setRatesExpanded] = useState(true);
|
||||
const [isTriggering, setIsTriggering] = useState(false);
|
||||
const isReadOnly = useIsReadOnly();
|
||||
|
||||
// Load financial snapshot on mount
|
||||
const { data: snapshot, isLoading: snapshotLoading } = useQuery<FinancialSnapshot>({
|
||||
@@ -696,6 +698,7 @@ export function InvestmentPlanningPage() {
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
{!isReadOnly && (
|
||||
<Button
|
||||
leftSection={<IconSparkles size={16} />}
|
||||
onClick={handleTriggerAI}
|
||||
@@ -705,6 +708,7 @@ export function InvestmentPlanningPage() {
|
||||
>
|
||||
{aiResult ? 'Refresh Recommendations' : 'Get AI Recommendations'}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{/* Processing State */}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { notifications } from '@mantine/notifications';
|
||||
import { IconSend, IconInfoCircle, IconCheck, IconX } from '@tabler/icons-react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '../../services/api';
|
||||
import { useIsReadOnly } from '../../stores/authStore';
|
||||
|
||||
interface Invoice {
|
||||
id: string; invoice_number: string; unit_number: string; unit_id: string;
|
||||
@@ -64,6 +65,7 @@ export function InvoicesPage() {
|
||||
const [preview, setPreview] = useState<Preview | null>(null);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const isReadOnly = useIsReadOnly();
|
||||
|
||||
const { data: invoices = [], isLoading } = useQuery<Invoice[]>({
|
||||
queryKey: ['invoices'],
|
||||
@@ -124,10 +126,12 @@ export function InvoicesPage() {
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Title order={2}>Invoices</Title>
|
||||
{!isReadOnly && (
|
||||
<Group>
|
||||
<Button variant="outline" onClick={() => lateFeesMutation.mutate()} loading={lateFeesMutation.isPending}>Apply Late Fees</Button>
|
||||
<Button leftSection={<IconSend size={16} />} onClick={openBulk}>Generate Invoices</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
<Group>
|
||||
<Card withBorder p="sm"><Text size="xs" c="dimmed">Total Invoices</Text><Text fw={700}>{invoices.length}</Text></Card>
|
||||
|
||||
Reference in New Issue
Block a user