Compare commits
2 Commits
7d4df25d16
...
fix/viewer
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d137a40d3 | |||
| 3bf6b8c6c9 |
@@ -153,6 +153,14 @@ export class OrganizationsService {
|
|||||||
existing.role = data.role;
|
existing.role = data.role;
|
||||||
return this.userOrgRepository.save(existing);
|
return this.userOrgRepository.save(existing);
|
||||||
}
|
}
|
||||||
|
// Update password for existing user being added to a new org
|
||||||
|
if (data.password) {
|
||||||
|
const passwordHash = await bcrypt.hash(data.password, 12);
|
||||||
|
await dataSource.query(
|
||||||
|
`UPDATE shared.users SET password_hash = $1 WHERE id = $2`,
|
||||||
|
[passwordHash, userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create new user
|
// Create new user
|
||||||
const passwordHash = await bcrypt.hash(data.password, 12);
|
const passwordHash = await bcrypt.hash(data.password, 12);
|
||||||
|
|||||||
@@ -9,20 +9,5 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
<script>
|
|
||||||
(function(d,t) {
|
|
||||||
var BASE_URL="https//chat.hoaledgeriq.com";
|
|
||||||
var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
|
|
||||||
g.src=BASE_URL+"/packs/js/sdk.js";
|
|
||||||
g.async = true;
|
|
||||||
s.parentNode.insertBefore(g,s);
|
|
||||||
g.onload=function(){
|
|
||||||
window.chatwootSDK.run({
|
|
||||||
websiteToken: 'K6VXvTtKXvaCMvre4yK85SPb',
|
|
||||||
baseUrl: BASE_URL
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})(document,"script");
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -587,7 +587,7 @@ export function AccountsPage() {
|
|||||||
{investments.filter(i => i.is_active).length > 0 && (
|
{investments.filter(i => i.is_active).length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider label="Investment Accounts" labelPosition="center" my="xs" />
|
<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>
|
</Stack>
|
||||||
@@ -605,7 +605,7 @@ export function AccountsPage() {
|
|||||||
{operatingInvestments.length > 0 && (
|
{operatingInvestments.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider label="Operating Investment Accounts" labelPosition="center" my="xs" />
|
<Divider label="Operating Investment Accounts" labelPosition="center" my="xs" />
|
||||||
<InvestmentMiniTable investments={operatingInvestments} onEdit={handleEditInvestment} />
|
<InvestmentMiniTable investments={operatingInvestments} onEdit={handleEditInvestment} isReadOnly={isReadOnly} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -623,7 +623,7 @@ export function AccountsPage() {
|
|||||||
{reserveInvestments.length > 0 && (
|
{reserveInvestments.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider label="Reserve Investment Accounts" labelPosition="center" my="xs" />
|
<Divider label="Reserve Investment Accounts" labelPosition="center" my="xs" />
|
||||||
<InvestmentMiniTable investments={reserveInvestments} onEdit={handleEditInvestment} />
|
<InvestmentMiniTable investments={reserveInvestments} onEdit={handleEditInvestment} isReadOnly={isReadOnly} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -1087,9 +1087,11 @@ function AccountTable({
|
|||||||
function InvestmentMiniTable({
|
function InvestmentMiniTable({
|
||||||
investments,
|
investments,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
isReadOnly = false,
|
||||||
}: {
|
}: {
|
||||||
investments: Investment[];
|
investments: Investment[];
|
||||||
onEdit: (inv: Investment) => void;
|
onEdit: (inv: Investment) => void;
|
||||||
|
isReadOnly?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const totalPrincipal = investments.reduce((s, i) => s + parseFloat(i.principal || '0'), 0);
|
const totalPrincipal = investments.reduce((s, i) => s + parseFloat(i.principal || '0'), 0);
|
||||||
const totalValue = investments.reduce(
|
const totalValue = investments.reduce(
|
||||||
@@ -1132,7 +1134,7 @@ function InvestmentMiniTable({
|
|||||||
<Table.Th ta="right">Maturity Value</Table.Th>
|
<Table.Th ta="right">Maturity Value</Table.Th>
|
||||||
<Table.Th>Maturity Date</Table.Th>
|
<Table.Th>Maturity Date</Table.Th>
|
||||||
<Table.Th ta="right">Days Remaining</Table.Th>
|
<Table.Th ta="right">Days Remaining</Table.Th>
|
||||||
<Table.Th></Table.Th>
|
{!isReadOnly && <Table.Th></Table.Th>}
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
@@ -1182,6 +1184,7 @@ function InvestmentMiniTable({
|
|||||||
'-'
|
'-'
|
||||||
)}
|
)}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
{!isReadOnly && (
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Tooltip label="Edit investment">
|
<Tooltip label="Edit investment">
|
||||||
<ActionIcon variant="subtle" onClick={() => onEdit(inv)}>
|
<ActionIcon variant="subtle" onClick={() => onEdit(inv)}>
|
||||||
@@ -1189,6 +1192,7 @@ function InvestmentMiniTable({
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
)}
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { IconDeviceFloppy, IconUpload, IconDownload, IconInfoCircle } from '@tab
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import api from '../../services/api';
|
import api from '../../services/api';
|
||||||
import { useIsReadOnly } from '../../stores/authStore';
|
import { useIsReadOnly } from '../../stores/authStore';
|
||||||
import { usePreferencesStore } from '../../stores/preferencesStore';
|
|
||||||
|
|
||||||
interface BudgetLine {
|
interface BudgetLine {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
@@ -99,11 +98,6 @@ export function BudgetsPage() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const isReadOnly = useIsReadOnly();
|
const isReadOnly = useIsReadOnly();
|
||||||
const isDark = usePreferencesStore((s) => s.colorScheme) === 'dark';
|
|
||||||
const stickyBg = isDark ? 'var(--mantine-color-dark-7)' : 'white';
|
|
||||||
const stickyBorder = isDark ? 'var(--mantine-color-dark-4)' : '#e9ecef';
|
|
||||||
const incomeSectionBg = isDark ? 'var(--mantine-color-green-9)' : '#e6f9e6';
|
|
||||||
const expenseSectionBg = isDark ? 'var(--mantine-color-red-9)' : '#fde8e8';
|
|
||||||
|
|
||||||
const { isLoading } = useQuery<BudgetLine[]>({
|
const { isLoading } = useQuery<BudgetLine[]>({
|
||||||
queryKey: ['budgets', year],
|
queryKey: ['budgets', year],
|
||||||
@@ -323,8 +317,8 @@ export function BudgetsPage() {
|
|||||||
<Table striped highlightOnHover style={{ minWidth: 1600 }}>
|
<Table striped highlightOnHover style={{ minWidth: 1600 }}>
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th style={{ position: 'sticky', left: 0, background: stickyBg, zIndex: 2, minWidth: 120 }}>Acct #</Table.Th>
|
<Table.Th style={{ position: 'sticky', left: 0, background: 'white', zIndex: 2, minWidth: 120 }}>Acct #</Table.Th>
|
||||||
<Table.Th style={{ position: 'sticky', left: 120, background: stickyBg, zIndex: 2, minWidth: 220 }}>Account Name</Table.Th>
|
<Table.Th style={{ position: 'sticky', left: 120, background: 'white', zIndex: 2, minWidth: 220 }}>Account Name</Table.Th>
|
||||||
{monthLabels.map((m) => (
|
{monthLabels.map((m) => (
|
||||||
<Table.Th key={m} ta="right" style={{ minWidth: 90 }}>{m}</Table.Th>
|
<Table.Th key={m} ta="right" style={{ minWidth: 90 }}>{m}</Table.Th>
|
||||||
))}
|
))}
|
||||||
@@ -343,7 +337,7 @@ export function BudgetsPage() {
|
|||||||
const lines = budgetData.filter((b) => b.account_type === type);
|
const lines = budgetData.filter((b) => b.account_type === type);
|
||||||
if (lines.length === 0) return null;
|
if (lines.length === 0) return null;
|
||||||
|
|
||||||
const sectionBg = type === 'income' ? incomeSectionBg : expenseSectionBg;
|
const sectionBg = type === 'income' ? '#e6f9e6' : '#fde8e8';
|
||||||
const sectionTotal = lines.reduce((sum, line) => sum + (line.annual_total || 0), 0);
|
const sectionTotal = lines.reduce((sum, line) => sum + (line.annual_total || 0), 0);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -374,9 +368,9 @@ export function BudgetsPage() {
|
|||||||
style={{
|
style={{
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
left: 0,
|
left: 0,
|
||||||
background: stickyBg,
|
background: 'white',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
borderRight: `1px solid ${stickyBorder}`,
|
borderRight: '1px solid #e9ecef',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="sm" c="dimmed" ff="monospace">{line.account_number}</Text>
|
<Text size="sm" c="dimmed" ff="monospace">{line.account_number}</Text>
|
||||||
@@ -385,9 +379,9 @@ export function BudgetsPage() {
|
|||||||
style={{
|
style={{
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
left: 120,
|
left: 120,
|
||||||
background: stickyBg,
|
background: 'white',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
borderRight: `1px solid ${stickyBorder}`,
|
borderRight: '1px solid #e9ecef',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group gap={6} wrap="nowrap">
|
<Group gap={6} wrap="nowrap">
|
||||||
|
|||||||
@@ -72,9 +72,10 @@ interface KanbanCardProps {
|
|||||||
project: Project;
|
project: Project;
|
||||||
onEdit: (p: Project) => void;
|
onEdit: (p: Project) => void;
|
||||||
onDragStart: (e: DragEvent<HTMLDivElement>, project: 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);
|
const plannedLabel = formatPlannedDate(project.planned_date);
|
||||||
// For projects in the Future bucket with a specific year, show the year
|
// For projects in the Future bucket with a specific year, show the year
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
@@ -86,21 +87,23 @@ function KanbanCard({ project, onEdit, onDragStart }: KanbanCardProps) {
|
|||||||
padding="sm"
|
padding="sm"
|
||||||
radius="md"
|
radius="md"
|
||||||
withBorder
|
withBorder
|
||||||
draggable
|
draggable={!isReadOnly}
|
||||||
onDragStart={(e) => onDragStart(e, project)}
|
onDragStart={!isReadOnly ? (e) => onDragStart(e, project) : undefined}
|
||||||
style={{ cursor: 'grab', userSelect: 'none' }}
|
style={{ cursor: isReadOnly ? 'default' : 'grab', userSelect: 'none' }}
|
||||||
mb="xs"
|
mb="xs"
|
||||||
>
|
>
|
||||||
<Group justify="space-between" wrap="nowrap" mb={4}>
|
<Group justify="space-between" wrap="nowrap" mb={4}>
|
||||||
<Group gap={6} wrap="nowrap" style={{ overflow: 'hidden' }}>
|
<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>
|
<Text fw={600} size="sm" truncate>
|
||||||
{project.name}
|
{project.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
{!isReadOnly && (
|
||||||
<ActionIcon variant="subtle" size="sm" onClick={() => onEdit(project)}>
|
<ActionIcon variant="subtle" size="sm" onClick={() => onEdit(project)}>
|
||||||
<IconEdit size={14} />
|
<IconEdit size={14} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap={6} mb={6}>
|
<Group gap={6} mb={6}>
|
||||||
@@ -148,11 +151,12 @@ interface KanbanColumnProps {
|
|||||||
isDragOver: boolean;
|
isDragOver: boolean;
|
||||||
onDragOverHandler: (e: DragEvent<HTMLDivElement>, year: number) => void;
|
onDragOverHandler: (e: DragEvent<HTMLDivElement>, year: number) => void;
|
||||||
onDragLeave: () => void;
|
onDragLeave: () => void;
|
||||||
|
isReadOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function KanbanColumn({
|
function KanbanColumn({
|
||||||
year, projects, onEdit, onDragStart, onDrop,
|
year, projects, onEdit, onDragStart, onDrop,
|
||||||
isDragOver, onDragOverHandler, onDragLeave,
|
isDragOver, onDragOverHandler, onDragLeave, isReadOnly,
|
||||||
}: KanbanColumnProps) {
|
}: KanbanColumnProps) {
|
||||||
const totalEst = projects.reduce((s, p) => s + parseFloat(p.estimated_cost || '0'), 0);
|
const totalEst = projects.reduce((s, p) => s + parseFloat(p.estimated_cost || '0'), 0);
|
||||||
const isFuture = year === FUTURE_YEAR;
|
const isFuture = year === FUTURE_YEAR;
|
||||||
@@ -178,9 +182,9 @@ function KanbanColumn({
|
|||||||
border: isDragOver ? '2px dashed var(--mantine-color-blue-4)' : undefined,
|
border: isDragOver ? '2px dashed var(--mantine-color-blue-4)' : undefined,
|
||||||
transition: 'background-color 150ms ease, border 150ms ease',
|
transition: 'background-color 150ms ease, border 150ms ease',
|
||||||
}}
|
}}
|
||||||
onDragOver={(e) => onDragOverHandler(e, year)}
|
onDragOver={!isReadOnly ? (e) => onDragOverHandler(e, year) : undefined}
|
||||||
onDragLeave={onDragLeave}
|
onDragLeave={!isReadOnly ? onDragLeave : undefined}
|
||||||
onDrop={(e) => onDrop(e, year)}
|
onDrop={!isReadOnly ? (e) => onDrop(e, year) : undefined}
|
||||||
>
|
>
|
||||||
<Group justify="space-between" mb="sm">
|
<Group justify="space-between" mb="sm">
|
||||||
<Title order={5}>{yearLabel(year)}</Title>
|
<Title order={5}>{yearLabel(year)}</Title>
|
||||||
@@ -199,7 +203,7 @@ function KanbanColumn({
|
|||||||
<Box style={{ flex: 1, minHeight: 60 }}>
|
<Box style={{ flex: 1, minHeight: 60 }}>
|
||||||
{projects.length === 0 ? (
|
{projects.length === 0 ? (
|
||||||
<Text size="xs" c="dimmed" ta="center" py="lg">
|
<Text size="xs" c="dimmed" ta="center" py="lg">
|
||||||
Drop projects here
|
{isReadOnly ? 'No projects' : 'Drop projects here'}
|
||||||
</Text>
|
</Text>
|
||||||
) : useWideLayout ? (
|
) : useWideLayout ? (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -208,12 +212,12 @@ function KanbanColumn({
|
|||||||
gap: 'var(--mantine-spacing-xs)',
|
gap: 'var(--mantine-spacing-xs)',
|
||||||
}}>
|
}}>
|
||||||
{projects.map((p) => (
|
{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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
projects.map((p) => (
|
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>
|
</Box>
|
||||||
@@ -595,6 +599,7 @@ export function CapitalProjectsPage() {
|
|||||||
isDragOver={dragOverYear === year}
|
isDragOver={dragOverYear === year}
|
||||||
onDragOverHandler={handleDragOver}
|
onDragOverHandler={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
IconArrowLeft, IconArrowRight, IconCalendar,
|
IconArrowLeft, IconArrowRight, IconCalendar,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { usePreferencesStore } from '../../stores/preferencesStore';
|
|
||||||
import {
|
import {
|
||||||
AreaChart, Area, XAxis, YAxis, CartesianGrid,
|
AreaChart, Area, XAxis, YAxis, CartesianGrid,
|
||||||
Tooltip as RechartsTooltip, ResponsiveContainer, Legend,
|
Tooltip as RechartsTooltip, ResponsiveContainer, Legend,
|
||||||
@@ -80,7 +79,6 @@ export function CashFlowForecastPage() {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const currentYear = now.getFullYear();
|
const currentYear = now.getFullYear();
|
||||||
const currentMonth = now.getMonth() + 1;
|
const currentMonth = now.getMonth() + 1;
|
||||||
const isDark = usePreferencesStore((s) => s.colorScheme) === 'dark';
|
|
||||||
|
|
||||||
// Filter: All, Operating, Reserve
|
// Filter: All, Operating, Reserve
|
||||||
const [fundFilter, setFundFilter] = useState<string>('all');
|
const [fundFilter, setFundFilter] = useState<string>('all');
|
||||||
@@ -420,10 +418,10 @@ export function CashFlowForecastPage() {
|
|||||||
<tr
|
<tr
|
||||||
key={d.month}
|
key={d.month}
|
||||||
style={{
|
style={{
|
||||||
borderBottom: `1px solid ${isDark ? 'var(--mantine-color-dark-4)' : 'var(--mantine-color-gray-2)'}`,
|
borderBottom: '1px solid var(--mantine-color-gray-2)',
|
||||||
backgroundColor: d.is_forecast
|
backgroundColor: d.is_forecast
|
||||||
? (isDark ? 'var(--mantine-color-orange-9)' : 'var(--mantine-color-orange-0)')
|
? 'var(--mantine-color-orange-0)'
|
||||||
: i % 2 === 0 ? 'transparent' : (isDark ? 'var(--mantine-color-dark-5)' : 'var(--mantine-color-gray-0)'),
|
: i % 2 === 0 ? 'transparent' : 'var(--mantine-color-gray-0)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<td style={{ padding: '6px 12px', fontWeight: 500 }}>{d.month}</td>
|
<td style={{ padding: '6px 12px', fontWeight: 500 }}>{d.month}</td>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useAuthStore } from '../../stores/authStore';
|
import { useAuthStore, useIsReadOnly } from '../../stores/authStore';
|
||||||
import api from '../../services/api';
|
import api from '../../services/api';
|
||||||
|
|
||||||
interface HealthScore {
|
interface HealthScore {
|
||||||
@@ -311,6 +311,7 @@ interface DashboardData {
|
|||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const currentOrg = useAuthStore((s) => s.currentOrg);
|
const currentOrg = useAuthStore((s) => s.currentOrg);
|
||||||
|
const isReadOnly = useIsReadOnly();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Track whether a refresh is in progress (per score type) for async polling
|
// Track whether a refresh is in progress (per score type) for async polling
|
||||||
@@ -424,7 +425,7 @@ export function DashboardPage() {
|
|||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
}
|
}
|
||||||
isRefreshing={operatingRefreshing}
|
isRefreshing={operatingRefreshing}
|
||||||
onRefresh={handleRefreshOperating}
|
onRefresh={!isReadOnly ? handleRefreshOperating : undefined}
|
||||||
lastFailed={!!healthScores?.operating_last_failed}
|
lastFailed={!!healthScores?.operating_last_failed}
|
||||||
/>
|
/>
|
||||||
<HealthScoreCard
|
<HealthScoreCard
|
||||||
@@ -436,7 +437,7 @@ export function DashboardPage() {
|
|||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
}
|
}
|
||||||
isRefreshing={reserveRefreshing}
|
isRefreshing={reserveRefreshing}
|
||||||
onRefresh={handleRefreshReserve}
|
onRefresh={!isReadOnly ? handleRefreshReserve : undefined}
|
||||||
lastFailed={!!healthScores?.reserve_last_failed}
|
lastFailed={!!healthScores?.reserve_last_failed}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
import api from '../../services/api';
|
import api from '../../services/api';
|
||||||
|
import { useIsReadOnly } from '../../stores/authStore';
|
||||||
|
|
||||||
// ── Types ──
|
// ── Types ──
|
||||||
|
|
||||||
@@ -347,6 +348,7 @@ function RecommendationsDisplay({
|
|||||||
export function InvestmentPlanningPage() {
|
export function InvestmentPlanningPage() {
|
||||||
const [ratesExpanded, setRatesExpanded] = useState(true);
|
const [ratesExpanded, setRatesExpanded] = useState(true);
|
||||||
const [isTriggering, setIsTriggering] = useState(false);
|
const [isTriggering, setIsTriggering] = useState(false);
|
||||||
|
const isReadOnly = useIsReadOnly();
|
||||||
|
|
||||||
// Load financial snapshot on mount
|
// Load financial snapshot on mount
|
||||||
const { data: snapshot, isLoading: snapshotLoading } = useQuery<FinancialSnapshot>({
|
const { data: snapshot, isLoading: snapshotLoading } = useQuery<FinancialSnapshot>({
|
||||||
@@ -696,6 +698,7 @@ export function InvestmentPlanningPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
|
{!isReadOnly && (
|
||||||
<Button
|
<Button
|
||||||
leftSection={<IconSparkles size={16} />}
|
leftSection={<IconSparkles size={16} />}
|
||||||
onClick={handleTriggerAI}
|
onClick={handleTriggerAI}
|
||||||
@@ -705,6 +708,7 @@ export function InvestmentPlanningPage() {
|
|||||||
>
|
>
|
||||||
{aiResult ? 'Refresh Recommendations' : 'Get AI Recommendations'}
|
{aiResult ? 'Refresh Recommendations' : 'Get AI Recommendations'}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Processing State */}
|
{/* Processing State */}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { notifications } from '@mantine/notifications';
|
|||||||
import { IconSend, IconInfoCircle, IconCheck, IconX } from '@tabler/icons-react';
|
import { IconSend, IconInfoCircle, IconCheck, IconX } from '@tabler/icons-react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import api from '../../services/api';
|
import api from '../../services/api';
|
||||||
|
import { useIsReadOnly } from '../../stores/authStore';
|
||||||
|
|
||||||
interface Invoice {
|
interface Invoice {
|
||||||
id: string; invoice_number: string; unit_number: string; unit_id: string;
|
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 [preview, setPreview] = useState<Preview | null>(null);
|
||||||
const [previewLoading, setPreviewLoading] = useState(false);
|
const [previewLoading, setPreviewLoading] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const isReadOnly = useIsReadOnly();
|
||||||
|
|
||||||
const { data: invoices = [], isLoading } = useQuery<Invoice[]>({
|
const { data: invoices = [], isLoading } = useQuery<Invoice[]>({
|
||||||
queryKey: ['invoices'],
|
queryKey: ['invoices'],
|
||||||
@@ -124,10 +126,12 @@ export function InvoicesPage() {
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={2}>Invoices</Title>
|
<Title order={2}>Invoices</Title>
|
||||||
|
{!isReadOnly && (
|
||||||
<Group>
|
<Group>
|
||||||
<Button variant="outline" onClick={() => lateFeesMutation.mutate()} loading={lateFeesMutation.isPending}>Apply Late Fees</Button>
|
<Button variant="outline" onClick={() => lateFeesMutation.mutate()} loading={lateFeesMutation.isPending}>Apply Late Fees</Button>
|
||||||
<Button leftSection={<IconSend size={16} />} onClick={openBulk}>Generate Invoices</Button>
|
<Button leftSection={<IconSend size={16} />} onClick={openBulk}>Generate Invoices</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
<Group>
|
<Group>
|
||||||
<Card withBorder p="sm"><Text size="xs" c="dimmed">Total Invoices</Text><Text fw={700}>{invoices.length}</Text></Card>
|
<Card withBorder p="sm"><Text size="xs" c="dimmed">Total Invoices</Text><Text fw={700}>{invoices.length}</Text></Card>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import api from '../../services/api';
|
import api from '../../services/api';
|
||||||
import { useIsReadOnly } from '../../stores/authStore';
|
import { useIsReadOnly } from '../../stores/authStore';
|
||||||
import { usePreferencesStore } from '../../stores/preferencesStore';
|
|
||||||
import { AttachmentPanel } from '../../components/attachments/AttachmentPanel';
|
import { AttachmentPanel } from '../../components/attachments/AttachmentPanel';
|
||||||
|
|
||||||
interface ActualLine {
|
interface ActualLine {
|
||||||
@@ -67,11 +66,6 @@ export function MonthlyActualsPage() {
|
|||||||
const [savedJEId, setSavedJEId] = useState<string | null>(null);
|
const [savedJEId, setSavedJEId] = useState<string | null>(null);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isReadOnly = useIsReadOnly();
|
const isReadOnly = useIsReadOnly();
|
||||||
const isDark = usePreferencesStore((s) => s.colorScheme) === 'dark';
|
|
||||||
const stickyBg = isDark ? 'var(--mantine-color-dark-7)' : 'white';
|
|
||||||
const stickyBorder = isDark ? 'var(--mantine-color-dark-4)' : '#e9ecef';
|
|
||||||
const incomeBg = isDark ? 'var(--mantine-color-green-9)' : '#e6f9e6';
|
|
||||||
const expenseBg = isDark ? 'var(--mantine-color-red-9)' : '#fde8e8';
|
|
||||||
|
|
||||||
const yearOptions = Array.from({ length: 5 }, (_, i) => {
|
const yearOptions = Array.from({ length: 5 }, (_, i) => {
|
||||||
const y = new Date().getFullYear() - 2 + i;
|
const y = new Date().getFullYear() - 2 + i;
|
||||||
@@ -184,16 +178,16 @@ export function MonthlyActualsPage() {
|
|||||||
<Table.Tr key={line.account_id}>
|
<Table.Tr key={line.account_id}>
|
||||||
<Table.Td
|
<Table.Td
|
||||||
style={{
|
style={{
|
||||||
position: 'sticky', left: 0, background: stickyBg, zIndex: 1,
|
position: 'sticky', left: 0, background: 'white', zIndex: 1,
|
||||||
borderRight: `1px solid ${stickyBorder}`,
|
borderRight: '1px solid #e9ecef',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="sm" c="dimmed" ff="monospace">{line.account_number}</Text>
|
<Text size="sm" c="dimmed" ff="monospace">{line.account_number}</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td
|
<Table.Td
|
||||||
style={{
|
style={{
|
||||||
position: 'sticky', left: 120, background: stickyBg, zIndex: 1,
|
position: 'sticky', left: 120, background: 'white', zIndex: 1,
|
||||||
borderRight: `1px solid ${stickyBorder}`,
|
borderRight: '1px solid #e9ecef',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group gap={6} wrap="nowrap">
|
<Group gap={6} wrap="nowrap">
|
||||||
@@ -298,10 +292,10 @@ export function MonthlyActualsPage() {
|
|||||||
<Table striped highlightOnHover style={{ minWidth: 700 }}>
|
<Table striped highlightOnHover style={{ minWidth: 700 }}>
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th style={{ position: 'sticky', left: 0, background: stickyBg, zIndex: 2, minWidth: 120 }}>
|
<Table.Th style={{ position: 'sticky', left: 0, background: 'white', zIndex: 2, minWidth: 120 }}>
|
||||||
Acct #
|
Acct #
|
||||||
</Table.Th>
|
</Table.Th>
|
||||||
<Table.Th style={{ position: 'sticky', left: 120, background: stickyBg, zIndex: 2, minWidth: 220 }}>
|
<Table.Th style={{ position: 'sticky', left: 120, background: 'white', zIndex: 2, minWidth: 220 }}>
|
||||||
Account Name
|
Account Name
|
||||||
</Table.Th>
|
</Table.Th>
|
||||||
<Table.Th ta="right" style={{ minWidth: 110 }}>Budget</Table.Th>
|
<Table.Th ta="right" style={{ minWidth: 110 }}>Budget</Table.Th>
|
||||||
@@ -310,8 +304,8 @@ export function MonthlyActualsPage() {
|
|||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{renderSection('Income', incomeLines, incomeBg, totals.incomeBudget, totals.incomeActual)}
|
{renderSection('Income', incomeLines, '#e6f9e6', totals.incomeBudget, totals.incomeActual)}
|
||||||
{renderSection('Expenses', expenseLines, expenseBg, totals.expenseBudget, totals.expenseActual)}
|
{renderSection('Expenses', expenseLines, '#fde8e8', totals.expenseBudget, totals.expenseActual)}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import api from '../../services/api';
|
import api from '../../services/api';
|
||||||
import { usePreferencesStore } from '../../stores/preferencesStore';
|
|
||||||
|
|
||||||
interface BudgetVsActualLine {
|
interface BudgetVsActualLine {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
@@ -47,9 +46,6 @@ const monthFilterOptions = [
|
|||||||
export function BudgetVsActualPage() {
|
export function BudgetVsActualPage() {
|
||||||
const [year, setYear] = useState(new Date().getFullYear().toString());
|
const [year, setYear] = useState(new Date().getFullYear().toString());
|
||||||
const [month, setMonth] = useState('');
|
const [month, setMonth] = useState('');
|
||||||
const isDark = usePreferencesStore((s) => s.colorScheme) === 'dark';
|
|
||||||
const incomeBg = isDark ? 'var(--mantine-color-green-9)' : '#e6f9e6';
|
|
||||||
const expenseBg = isDark ? 'var(--mantine-color-red-9)' : '#fde8e8';
|
|
||||||
|
|
||||||
const yearOptions = Array.from({ length: 5 }, (_, i) => {
|
const yearOptions = Array.from({ length: 5 }, (_, i) => {
|
||||||
const y = new Date().getFullYear() - 2 + i;
|
const y = new Date().getFullYear() - 2 + i;
|
||||||
@@ -96,7 +92,7 @@ export function BudgetVsActualPage() {
|
|||||||
|
|
||||||
const renderSection = (title: string, sectionLines: BudgetVsActualLine[], isExpense: boolean, totalBudget: number, totalActual: number) => (
|
const renderSection = (title: string, sectionLines: BudgetVsActualLine[], isExpense: boolean, totalBudget: number, totalActual: number) => (
|
||||||
<>
|
<>
|
||||||
<Table.Tr style={{ background: isExpense ? expenseBg : incomeBg }}>
|
<Table.Tr style={{ background: isExpense ? '#fde8e8' : '#e6f9e6' }}>
|
||||||
<Table.Td colSpan={6} fw={700}>{title}</Table.Td>
|
<Table.Td colSpan={6} fw={700}>{title}</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
{sectionLines.map((line) => {
|
{sectionLines.map((line) => {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
IconTrendingUp, IconTrendingDown, IconAlertTriangle, IconChartBar,
|
IconTrendingUp, IconTrendingDown, IconAlertTriangle, IconChartBar,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import api from '../../services/api';
|
import api from '../../services/api';
|
||||||
import { usePreferencesStore } from '../../stores/preferencesStore';
|
|
||||||
|
|
||||||
interface BudgetVsActualItem {
|
interface BudgetVsActualItem {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
@@ -49,9 +48,6 @@ export function QuarterlyReportPage() {
|
|||||||
const currentQuarter = Math.ceil((now.getMonth() + 1) / 3);
|
const currentQuarter = Math.ceil((now.getMonth() + 1) / 3);
|
||||||
const defaultQuarter = currentQuarter;
|
const defaultQuarter = currentQuarter;
|
||||||
const defaultYear = now.getFullYear();
|
const defaultYear = now.getFullYear();
|
||||||
const isDark = usePreferencesStore((s) => s.colorScheme) === 'dark';
|
|
||||||
const incomeBg = isDark ? 'var(--mantine-color-green-9)' : '#e6f9e6';
|
|
||||||
const expenseBg = isDark ? 'var(--mantine-color-red-9)' : '#fde8e8';
|
|
||||||
|
|
||||||
const [year, setYear] = useState(String(defaultYear));
|
const [year, setYear] = useState(String(defaultYear));
|
||||||
const [quarter, setQuarter] = useState(String(defaultQuarter));
|
const [quarter, setQuarter] = useState(String(defaultQuarter));
|
||||||
@@ -211,7 +207,7 @@ export function QuarterlyReportPage() {
|
|||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{incomeItems.length > 0 && (
|
{incomeItems.length > 0 && (
|
||||||
<Table.Tr style={{ background: incomeBg }}>
|
<Table.Tr style={{ background: '#e6f9e6' }}>
|
||||||
<Table.Td colSpan={8} fw={700}>Income</Table.Td>
|
<Table.Td colSpan={8} fw={700}>Income</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
)}
|
)}
|
||||||
@@ -219,7 +215,7 @@ export function QuarterlyReportPage() {
|
|||||||
<BVARow key={item.account_id} item={item} isExpense={false} />
|
<BVARow key={item.account_id} item={item} isExpense={false} />
|
||||||
))}
|
))}
|
||||||
{incomeItems.length > 0 && (
|
{incomeItems.length > 0 && (
|
||||||
<Table.Tr style={{ background: incomeBg }}>
|
<Table.Tr style={{ background: '#e6f9e6' }}>
|
||||||
<Table.Td colSpan={2} fw={700}>Total Income</Table.Td>
|
<Table.Td colSpan={2} fw={700}>Total Income</Table.Td>
|
||||||
<Table.Td ta="right" fw={700} ff="monospace">{fmt(incomeItems.reduce((s, i) => s + i.quarter_budget, 0))}</Table.Td>
|
<Table.Td ta="right" fw={700} ff="monospace">{fmt(incomeItems.reduce((s, i) => s + i.quarter_budget, 0))}</Table.Td>
|
||||||
<Table.Td ta="right" fw={700} ff="monospace">{fmt(incomeItems.reduce((s, i) => s + i.quarter_actual, 0))}</Table.Td>
|
<Table.Td ta="right" fw={700} ff="monospace">{fmt(incomeItems.reduce((s, i) => s + i.quarter_actual, 0))}</Table.Td>
|
||||||
@@ -230,7 +226,7 @@ export function QuarterlyReportPage() {
|
|||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
)}
|
)}
|
||||||
{expenseItems.length > 0 && (
|
{expenseItems.length > 0 && (
|
||||||
<Table.Tr style={{ background: expenseBg }}>
|
<Table.Tr style={{ background: '#fde8e8' }}>
|
||||||
<Table.Td colSpan={8} fw={700}>Expenses</Table.Td>
|
<Table.Td colSpan={8} fw={700}>Expenses</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
)}
|
)}
|
||||||
@@ -238,7 +234,7 @@ export function QuarterlyReportPage() {
|
|||||||
<BVARow key={item.account_id} item={item} isExpense={true} />
|
<BVARow key={item.account_id} item={item} isExpense={true} />
|
||||||
))}
|
))}
|
||||||
{expenseItems.length > 0 && (
|
{expenseItems.length > 0 && (
|
||||||
<Table.Tr style={{ background: expenseBg }}>
|
<Table.Tr style={{ background: '#fde8e8' }}>
|
||||||
<Table.Td colSpan={2} fw={700}>Total Expenses</Table.Td>
|
<Table.Td colSpan={2} fw={700}>Total Expenses</Table.Td>
|
||||||
<Table.Td ta="right" fw={700} ff="monospace">{fmt(expenseItems.reduce((s, i) => s + i.quarter_budget, 0))}</Table.Td>
|
<Table.Td ta="right" fw={700} ff="monospace">{fmt(expenseItems.reduce((s, i) => s + i.quarter_budget, 0))}</Table.Td>
|
||||||
<Table.Td ta="right" fw={700} ff="monospace">{fmt(expenseItems.reduce((s, i) => s + i.quarter_actual, 0))}</Table.Td>
|
<Table.Td ta="right" fw={700} ff="monospace">{fmt(expenseItems.reduce((s, i) => s + i.quarter_actual, 0))}</Table.Td>
|
||||||
|
|||||||
Reference in New Issue
Block a user