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>
95 lines
2.5 KiB
TypeScript
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';
|
|
}
|