books/frontend/src/stores/companyStore.ts
Nicolaj Hartmann 8e05171b66 Full product audit: fix security, compliance, UX, and wire broken features
Security (Phase 1):
- Add authentication middleware on /graphql endpoint
- Filter company queries by user access (prevent IDOR)
- Add role-based authorization on mutations (owner/accountant)
- Reduce API key cache TTL from 24h to 5 minutes
- Hide exception details in production GraphQL errors
- Fix RBAC in frontend companyStore (was hardcoded)

Wiring broken features (Phase 2):
- Wire Kassekladde submit/void/copy to GraphQL mutations
- Wire Kontooversigt account creation to createAccount mutation
- Wire Settings save to updateCompany mutation
- Wire CreateFiscalYearModal and CloseFiscalYearWizard to mutations
- Replace Momsindberetning mock data with real useVatReport query
- Remove Dashboard hardcoded percentages and fake VAT deadline
- Fix Kreditnotaer invoice selector to use real data
- Fix mutation retry from 1 to 0 (prevent duplicate operations)

Accounting compliance (Phase 3):
- Add balanced entry validation (debit==credit) in JournalEntryDraftAggregate
- Add fiscal year boundary enforcement (status, date range checks)
- Add PostedAt timestamp to posted events (Bogføringsloven §7)
- Add account number uniqueness check within company
- Add fiscal year overlap and gap checks
- Add sequential invoice auto-numbering
- Fix InvoiceLine VAT rate to use canonical VatCodes
- Fix SAF-T account type mapping (financial → Expense)
- Add DraftLine validation (cannot have both debit and credit > 0)

UX improvements (Phase 4):
- Fix Danish character encoding across 15+ files (ø, æ, å)
- Deploy DemoDataDisclaimer on pages with mock/incomplete data
- Adopt PageHeader component universally across all pages
- Standardize active/inactive filtering to Switch pattern
- Fix dead buttons in Header (Help, Notifications)
- Remove hardcoded mock data from Settings
- Fix Sidebar controlled state and Kontooversigt navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:35:26 +01:00

95 lines
2.5 KiB
TypeScript

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { CompanyRole, CompanyWithRole } from '@/types/accounting';
interface CompanyState {
// Current active company (includes role from myCompanies query)
activeCompany: CompanyWithRole | null;
// List of available companies (includes role from myCompanies query)
companies: CompanyWithRole[];
// Loading state
isLoading: boolean;
// Actions
setActiveCompany: (company: CompanyWithRole) => void;
setCompanies: (companies: CompanyWithRole[]) => void;
setLoading: (loading: boolean) => void;
clearActiveCompany: () => void;
}
export const useCompanyStore = create<CompanyState>()(
persist(
(set) => ({
activeCompany: null,
companies: [],
isLoading: false,
setActiveCompany: (company) =>
set({ activeCompany: company }),
setCompanies: (companies) =>
set({ companies }),
setLoading: (isLoading) =>
set({ isLoading }),
clearActiveCompany: () =>
set({ activeCompany: null }),
}),
{
name: 'books-company-storage',
partialize: (state) => ({
activeCompany: state.activeCompany,
}),
}
)
);
// Selector hooks for convenience
export const useActiveCompany = () =>
useCompanyStore((state) => state.activeCompany);
export const useCompanies = () =>
useCompanyStore((state) => state.companies);
// Get the current user's role for the active company
// Returns the role from the myCompanies query data stored on the active company
export const useActiveCompanyRole = (): CompanyRole => {
const activeCompany = useCompanyStore((state) => state.activeCompany);
// Return the actual role from the CompanyWithRole data, default to 'viewer' if not set
return activeCompany?.role ?? 'viewer';
};
// Helper functions for user roles
export function getRoleLabel(role: CompanyRole): string {
switch (role) {
case 'owner':
return 'Ejer';
case 'accountant':
return 'Bogholder';
case 'viewer':
return 'Læser';
default:
return role;
}
}
export function getRoleColor(role: CompanyRole): string {
switch (role) {
case 'owner':
return 'gold';
case 'accountant':
return 'blue';
case 'viewer':
return 'default';
default:
return 'default';
}
}
// Hook to check if current user can administer the company
// Checks if the user has Owner role for the active company
export function useCanAdmin(): boolean {
const role = useActiveCompanyRole();
return role === 'owner';
}