Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 40 additions & 29 deletions firebase.rules
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,71 @@ service cloud.firestore {
return request.auth != null;
}

function isAdmin() {
return isSignedIn() && request.auth.token.admin == true;
}

function isValidAmount() {
return request.resource.data.amount is number && request.resource.data.amount > 0;
}

function hasValidTimestamps() {
let hasCreatedAt = request.resource.data.createdAt is timestamp;
let hasUpdatedAt = request.resource.data.updatedAt is timestamp;
return hasCreatedAt && hasUpdatedAt;
}

// Common rules
match /{document=**} {
allow read: if isSignedIn();
allow write: if isAdmin();
}

// Specific collection rules
match /donors/{donorId} {
allow read: if isSignedIn();
allow write: if isAdmin();
function isValidFeedingRound() {
let data = request.resource.data;
return data.allocatedAmount is number
&& data.allocatedAmount > 0
&& data.unitPrice is number
&& data.unitPrice > 0
&& data.categoryId is string
&& data.date is string
&& (data.status == 'PENDING' || data.status == 'IN_PROGRESS' || data.status == 'COMPLETED' || data.status == 'CANCELLED')
&& (!data.keys().hasAny(['photos']) || data.photos is list);
}

match /donations/{donationId} {
allow read: if isSignedIn();
allow write: if isAdmin() && isValidAmount() && hasValidTimestamps();
function isValidTreasuryCategory() {
let data = request.resource.data;
return data.name is string
&& data.name.size() > 0
&& data.balance is number
&& data.balance >= 0;
}

match /beneficiaries/{beneficiaryId} {
allow read: if isSignedIn();
allow write: if isAdmin() && hasValidTimestamps();
function isValidTransaction() {
let data = request.resource.data;
return data.type in ['DEBIT', 'CREDIT', 'STATUS_UPDATE']
&& data.amount is number
&& data.amount > 0
&& data.description is string
&& data.category is string
&& data.reference is string
&& data.status in ['PENDING', 'COMPLETED', 'CANCELLED'];
}

match /payments/{paymentId} {
// Common rules - all operations require authentication
match /{document=**} {
allow read: if isSignedIn();
allow write: if isAdmin() && isValidAmount() && hasValidTimestamps();
allow write: if isSignedIn();
}

// Feeding rounds collection
match /feedingRounds/{roundId} {
allow read: if isSignedIn();
allow write: if isAdmin() && isValidAmount() && hasValidTimestamps();
allow create: if isSignedIn() && isValidFeedingRound() && hasValidTimestamps();
allow update: if isSignedIn() && isValidFeedingRound() && hasValidTimestamps();
allow delete: if isSignedIn();
}

// Treasury collection
match /treasury/{categoryId} {
allow read: if isSignedIn();
allow write: if isAdmin() && isValidAmount() && hasValidTimestamps();
allow create: if isSignedIn() && isValidTreasuryCategory() && hasValidTimestamps();
allow update: if isSignedIn() && isValidTreasuryCategory() && hasValidTimestamps();
allow delete: if isSignedIn();
}

// Transactions collection
match /transactions/{transactionId} {
allow read: if isSignedIn();
allow write: if isAdmin() && isValidAmount() && hasValidTimestamps();
allow create: if isSignedIn() && isValidTransaction() && hasValidTimestamps();
allow update: if false; // Transactions should never be updated
allow delete: if false; // Transactions should never be deleted
}
}
}
34 changes: 34 additions & 0 deletions src/components/DataSynchronizer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import { useAllData } from '../hooks/useFirebaseQuery';
import { useStore } from '../store';

/**
* Component that synchronizes data between React Query and Zustand store
*/
export const DataSynchronizer: React.FC = () => {
const { data } = useAllData();
const {
setTreasuryCategories,
setBeneficiaries,
setDonors,
setDonations,
setFeedingRounds,
setPayments,
setTransactions
} = useStore();

React.useEffect(() => {
if (data) {
// Update all store data when React Query data changes
setTreasuryCategories(data.treasury || []);
setBeneficiaries(data.beneficiaries || []);
setDonors(data.donors || []);
setDonations(data.donations || []);
setFeedingRounds(data.feedingRounds || { rounds: [], lastDoc: null });
setPayments(data.payments || []);
setTransactions(data.transactions || []);
}
}, [data, setTreasuryCategories, setBeneficiaries, setDonors, setDonations, setFeedingRounds, setPayments, setTransactions]);

return null;
};
4 changes: 4 additions & 0 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Link, Outlet, useLocation, NavLink } from 'react-router-dom';
import { Heart, Users, Wallet, Utensils, Menu, CircleDollarSign, UserRound, CreditCard, FileText, LogOut, LayoutDashboard, DollarSign, Gift, UserCheck } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import { DebugModeToggle } from './DebugModeToggle';
import { DataSynchronizer } from './DataSynchronizer';

const Layout: React.FC = () => {
const location = useLocation();
Expand All @@ -24,6 +25,9 @@ const Layout: React.FC = () => {

return (
<div className="min-h-screen bg-gray-50">
{/* Data Synchronizer */}
<DataSynchronizer />

{/* Mobile menu button */}
<div className="lg:hidden fixed top-0 left-0 right-0 z-50 bg-white shadow-sm p-4">
<button
Expand Down
80 changes: 80 additions & 0 deletions src/components/TreasuryValidation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from 'react';
import { useStore } from '../store';
import { validateTreasuryPayments } from '../utils/treasuryValidator';
import { Alert, AlertTitle } from '@mui/material';

interface TreasuryValidationProps {
isVisible: boolean;
validationResults: ReturnType<typeof validateTreasuryPayments> | null;
}

export const TreasuryValidation: React.FC<TreasuryValidationProps> = ({
isVisible,
validationResults
}) => {
if (!isVisible || !validationResults) {
return null;
}

return validationResults.isValid ? (
<Alert severity="success" className="mb-4">
<AlertTitle>Treasury Validation Passed</AlertTitle>
All completed payments have been properly deducted from treasury categories.
</Alert>
) : (
<Alert severity="error" className="mb-4">
<AlertTitle>Treasury Validation Failed</AlertTitle>
<div className="mt-2">
<p className="font-medium">Found {validationResults.discrepancies.length} discrepancies:</p>
<ul className="mt-2 list-disc list-inside">
{validationResults.discrepancies.map(discrepancy => (
<li key={discrepancy.categoryId} className="mb-4">
<div className="ml-4">
<p><strong>Category:</strong> {discrepancy.categoryName}</p>
<p><strong>Expected Balance:</strong> ${discrepancy.expectedBalance.toFixed(2)}</p>
<p><strong>Actual Balance:</strong> ${discrepancy.actualBalance.toFixed(2)}</p>
<p><strong>Difference:</strong> ${discrepancy.difference.toFixed(2)}</p>
<p><strong>Completed Payments:</strong> {discrepancy.completedPayments.length}</p>
<details className="mt-2">
<summary className="cursor-pointer text-sm text-blue-600">View Payments</summary>
<ul className="mt-2 list-disc list-inside">
{discrepancy.completedPayments.map(payment => (
<li key={payment.id} className="ml-4 text-sm">
ID: {payment.id} - Amount: ${payment.amount.toFixed(2)}
</li>
))}
</ul>
</details>
</div>
</li>
))}
</ul>
</div>
</Alert>
);
};

export const useTreasuryValidation = () => {
const { payments, treasuryCategories } = useStore();
const [isValidating, setIsValidating] = React.useState(false);
const [validationResults, setValidationResults] = React.useState<ReturnType<typeof validateTreasuryPayments> | null>(null);

const runValidation = React.useCallback(() => {
setIsValidating(true);
try {
const results = validateTreasuryPayments(payments, treasuryCategories);
setValidationResults(results);
} catch (error) {
console.error('Validation error:', error);
} finally {
setIsValidating(false);
}
}, [payments, treasuryCategories]);

return {
runValidation,
isValidating,
validationResults,
setValidationResults
};
};
Loading