A full-stack React application for tracking and managing French public procurement announcements (Appels d'Offres) from the BOAMP (Bulletin Officiel des Annonces des Marchés Publics) database.
- AO Tracking System: Tag and track announcements through their lifecycle (new → analyzing → in_progress → won/lost, or analyzing → no_go)
- Kanban-Style Interface: Organize AOs in columns by status for visual project management
- Real-time Data: Fetches live data from the BOAMP OpenDataSoft API
- Smart Filtering: Multi-dimensional filtering by publication date, response date, and status
- Expandable Cards: Modern card-based display with smooth expand/collapse animations
- Persistent Storage: SQLite database tracks your AO status and notes locally
- Responsive Design: Built with Tailwind CSS for mobile and desktop
- Type-Safe: Full TypeScript coverage with proper interfaces
- Background Jobs: Automatic AO expiration and sync processes
- Frontend: React 18 + TypeScript + Tailwind CSS + Vite (port 5173)
- Backend: Express.js + SQLite REST API (port 3001)
- Data Source: BOAMP OpenDataSoft API with ODSQL filtering
- State Management: Tag-based organization with separate state arrays
- React 18.3 - Modern UI library with hooks
- Vite 6.0 - Lightning-fast build tool and dev server
- TypeScript 5.7 - Type-safe JavaScript
- Tailwind CSS 3.4 - Utility-first CSS framework
- Framer Motion 12 - Smooth animations and transitions
- Express.js 4.18 - Web application framework
- SQLite3 5.1 - Embedded database for tracking
- CORS - Cross-origin resource sharing
- Body Parser - Request body parsing middleware
boamp/
├── src/ # Frontend React application
│ ├── App.tsx # Main app with tag-based state management
│ ├── components/
│ │ ├── ao/ # AO-specific components
│ │ │ ├── GridCard.tsx # Main record display card
│ │ │ ├── AnalyzeButton.tsx, InProgressButton.tsx, etc.
│ │ │ └── ActionButton.tsx # Base action button component
│ │ ├── ui/ # Reusable UI components
│ │ │ ├── FilterBar.tsx # Multi-dimensional filtering
│ │ │ ├── Badge.tsx # Status badges with colors
│ │ │ └── LoadingSpinner.tsx, Toast.tsx, etc.
│ │ └── layout/ # Layout components
│ │ ├── ColumnContainer.tsx # Kanban-style columns
│ │ ├── Header.tsx, Footer.tsx
│ │ └── FilterBanner.tsx
│ ├── services/
│ │ └── trackingApi.ts # Backend API client for tracking
│ ├── types/
│ │ └── boamp.ts # TypeScript interfaces
│ ├── utils/
│ │ ├── dateFilters.ts # Date filtering logic
│ │ ├── dateUtils.ts # Date formatting utilities
│ │ └── statusColors.ts # Status color mapping
│ └── constants/
│ └── filters.ts # BOAMP filter constants
├── server/ # Backend Express.js application
│ ├── server.js # Main server with job scheduling
│ ├── db.js # SQLite database setup
│ ├── routes/
│ │ └── ao.js # AO tracking REST endpoints
│ └── jobs/ # Background processes
│ ├── expireAOs.js # Auto-expire old AOs
│ └── syncBOAMP.js # Sync with BOAMP API
├── docs/ # Documentation
├── .github/
│ └── copilot-instructions.md # AI agent guidance
└── Configuration files (vite, typescript, tailwind, etc.)
- Node.js 18+ and npm
Install dependencies for both frontend and backend:
# Frontend dependencies
npm install
# Backend dependencies
cd server && npm installThe application requires both frontend and backend servers:
Option 1: Run both simultaneously
# Terminal 1: Frontend (Vite dev server)
npm run dev
# Terminal 2: Backend (Express server with --watch)
cd server && npm run devOption 2: One-liner (runs both in background)
npm run dev & cd server && npm run dev- Frontend:
http://localhost:5173 - Backend API:
http://localhost:3001 - Health check:
http://localhost:3001/health
The SQLite database (server/boamp.db) is automatically created on first run with the following tables:
ao_tracking- AO status and tagsao_blacklist- Discarded AOs
Create a production build:
npm run buildnpm run lint- Endpoint:
https://boamp-datadila.opendatasoft.com/api/explore/v2.1/catalog/datasets/boamp/records - Query Language: ODSQL (Opendatasoft Query Language)
- Default Filters:
descripteur_code:(163 OR 186 OR 183)- Specific procurement categories- Date filters for publication and response deadlines
- Base URL:
http://localhost:3001/api - Endpoints:
GET /ao/:idweb- Get tracking info for an AOPOST /ao/:idweb/tag- Set tag/status for an AOPOST /ao/:idweb/discard- Soft delete an AOGET /ao/status/:tag- Get all AOs with specific tagGET /health- Server health check
Main application with tag-based state management:
const [newRecords, setNewRecords] = useState<BOAMPRecord[]>([]);
const [analyzingRecords, setAnalyzingRecords] = useState<BOAMPRecord[]>([]);
// ... separate state for each tagModern card component featuring:
- Smooth expand/collapse with Framer Motion
- Status badges with color coding
- Action buttons for status transitions
- JSON data tooltip for debugging
Multi-dimensional filtering:
- Publication date (today, yesterday, last week, etc.)
- Response date (today, tomorrow, next week, etc.)
- Status filter (all, new, analyzing, etc.)
- Real-time count display for each filter option
Individual components for each status transition:
AnalyzeButton- Move to analyzingInProgressButton- Move to in_progressWonButton,LostButton,NoGoButton- Final statesRemoveTagButton- Remove status/tagDeleteButton- Soft delete (discard)
### Status Flow
AO announcements progress through specific state transitions:
- **new** → **analyzing** (research/evaluation) OR **archive** (discard)
- **analyzing** → **in_progress** (active pursuit) OR **no_go** (decision not to pursue)
- **in_progress** → **won** (successful) OR **lost** (unsuccessful)
- Final states (**won/lost/no_go**) → **remove tag** (return to untracked)
↓ ↓ ↓ ↓
Remove tag at any stage, or discard (soft delete)
- new: Recently imported AOs awaiting review
- analyzing: AOs under evaluation for relevance/fit
- in_progress: AOs being actively pursued (proposal preparation)
- won: Successfully awarded contracts
- lost: Unsuccessful bids
- no_go: AOs determined not worth pursuing
CREATE TABLE ao_tracking (
idweb TEXT PRIMARY KEY, -- BOAMP record ID
tag TEXT CHECK(tag IN (...)), -- Status enum
discarded INTEGER DEFAULT 0, -- Soft delete flag
record_data TEXT, -- Full BOAMP JSON record
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);The application stores the complete BOAMP record in record_data for offline access and to avoid re-fetching data.
Each BOAMP record includes:
- idweb: Unique identifier
- objet: Announcement title/object
- nomacheteur: Buyer organization name
- datelimitereponse: Response deadline
- dateparution: Publication date
- datefindiffusion: End of distribution date
- etat: Status (publié, archivé, annulé)
- descripteur_code/libelle: Category descriptors (163, 186, 183)
- type_marche: Contract type
- procedure_libelle: Procedure type
- nature_libelle: Nature of the contract
- url_avis: Link to full announcement
- And more...
- expireAOs.js: Automatically moves old AOs out of active status
- syncBOAMP.js: Periodic sync with BOAMP API for new announcements
- Jobs are scheduled in
server.jsusingsetInterval
- Uses ES modules (
import/export) throughout - Tailwind for all styling with custom color schemes
- TypeScript strict mode enabled
- Framer Motion for smooth animations
- CORS enabled for localhost development
Data is provided by BOAMP DataDila, the official source for French public procurement announcements.
MIT