# Project export: MealPing - Local Food Rescue Network

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: MealPing connects food businesses with nearby verified NGOs, making surplus-food pickup simple, accountable, and timely before good food goes to waste.
- Devpost: https://devpost.com/software/mealping-local-food-rescue-network
- GitHub: https://github.com/jitupv/mealping
- Demo: https://mealping.mejitu.com/
- Video: https://www.youtube.com/embed/4bhT_b-mCHM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Jitu (1 commits)

## Devpost submission (written by the team)

### Inspiration

MealPing started with one simple question: what happens when usable food is left after service, but a nearby NGO does not know it exists? Food waste is often treated as only a food problem. While working on this idea, I kept coming back to the coordination problem behind it. A restaurant may have food ready now. An NGO may have the capacity to collect it. But they may not have a clear, trusted way to find each other in time. I wanted to build a product that focuses on that one local handoff.

### What it does

MealPing connects food businesses with nearby verified NGOs so surplus food can be collected before it becomes waste. Food partners can create a post with food type, quantity, pickup window, and pickup location. Suitable verified NGOs can view available food based on location, pickup radius, capacity, food preference, and availability. An NGO can accept a post and coordinate the next step. MealPing also includes an admin workflow for reviewing and verifying food partners and NGOs. This keeps the network more accountable than a public listing board. Location access is optional. When it is available, it supports more accurate matching. When it is not available, MealPing falls back to the local pickup area and pincode details entered by the organization. AI in the product MealPing includes an optional AI helper that turns rough food notes into a clearer donation draft and handoff summary. The AI does not decide who can receive food, verify an organization, or replace the manual workflow. People still make the important decisions. If the AI helper is busy or unavailable, users can continue with the regular form.

### How we built it

I am primarily a UI developer with Angular experience. I led the product idea, user journeys, responsive frontend, visual design, and testing. I built the frontend with Angular, TypeScript, SCSS, PrimeNG, and GSAP. Supabase provides authentication, PostgreSQL data storage, role-based access rules, and Edge Functions. PostGIS supports the location-based matching logic. For areas where I had less experience, including backend logic, database structure, security rules, and deployment, I used GPT-5.6 as a development collaborator. It helped me understand unfamiliar areas, compare approaches, and turn an idea I had been thinking about for a long time into a working product. I tested the key workflows myself and made the final product and UX decisions.

### Challenges we ran into

The hardest part was building a product that works beyond attractive screens. First, trust matters. Food partners and NGOs need clear verification and visible status before they coordinate a pickup. I added a manual admin review flow instead of treating every new account as trusted. Second, location is not always perfect. Users may decline location permission or enter incomplete details. MealPing handles this by using precise location when it is available and a pincode and local-area fallback when it is not. Third, AI reliability matters. Free-tier AI limits and temporary API errors can happen. I designed the AI helper as an optional assistant, with a manual form that always remains available.

### Accomplishments we're proud of

Built MealPing, an end-to-end platform connecting food donors with verified NGOs through a structured, trust-based system. Developed both frontend and backend features, including location matching, role-based access, and verification mechanisms. Expanded beyond UI into backend and deployment, using AI as support while maintaining full control over design and functionality.

### What we learned

I learned that a useful product is not only about its interface. It also needs clear roles, fallbacks, safe defaults, and practical next actions. I also learned that AI is most useful when it removes small friction without becoming a single point of failure. In MealPing, AI helps prepare information, while people and verified organizations remain responsible for the actual handoff.

### What's next

The next step is to pilot MealPing in one local area with a small group of verified food partners and NGOs. The goal is to learn from real handoffs, improve verification and pickup coordination, and prove a repeatable local rescue loop before expanding to more areas.

## README (from the GitHub repository)

# MealPing

**Ping surplus. Rescue meals.**

MealPing is an AI-assisted food-rescue network for Food Partners, NGOs and network operators. A restaurant can photograph or type a surplus-food description; AI structures it; a person confirms it; AI can prepare a human-reviewable pickup handoff brief; deterministic rules rank eligible nearby NGOs; and both organizations track pickup and delivery.

This repository is an Angular 21 + PrimeNG 21 hackathon MVP. It starts in demo mode with synthetic Mumbai–Thane data and no paid services.

## Run locally

```bash
npm install
npm start
```

Open `http://localhost:4200`. Useful demo routes:

- `/partner/dashboard` — Food Partner workspace
- `/partner/donations/new` — typed description → AI extraction → confirmation → publish
- `/ngo/dashboard` — ranked rescue opportunities
- `/ngo/opportunities/MP-24071` — acceptance flow
- `/ngo/rescues/MP-24071` — pickup OTP and delivery flow
- `/admin/dashboard` — live network operations

Every workspace route is login-protected. Use the role-specific login tabs with:

| Workspace | Email | Password |
| --- | --- | --- |
| Food Partner | `partner@mealping.demo` | `mealping123` |
| NGO | `ngo@mealping.demo` | `mealping123` |
| Admin | `admin@mealping.demo` | `mealping123` |

Each account can open only its own workspace. The registration form also creates a reusable local account in demo mode. When Supabase is configured and `demoMode` is disabled, login and registration use Supabase Auth instead.

## Hackathon judge accounts

The hosted Supabase demo includes three intentionally fake, ready-to-test accounts. They are for product review only — do not use them with real people, food, or personal information.

| Workspace | Email | Password | What a reviewer can check |
| --- | --- | --- | --- |
| Food Partner | `partner.demo@mealping.test` | `Demo@MealPing26` | Verified restaurant workspace and surplus-food posting |
| NGO | `ngo.demo@mealping.test` | `Demo@MealPing26` | Verified NGO workspace, opportunities and rescue workflow |
| Network Admin | `admin.demo@mealping.test` | `Demo@MealPing26` | Organization review and network operations |

The partner and NGO profiles use fictional Thane data and are already verified so reviewers can reach the main product flows immediately. The seed steps are retained in [`supabase/hackathon-demo-accounts.sql`](supabase/hackathon-demo-accounts.sql) for a fresh Supabase project.

## What is implemented

- Premium responsive public site, authentication and registration screens
- Role-specific desktop sidebar and mobile bottom navigation
- Login guards, role authorization, persisted sessions and logout
- Partner dashboards, multi-step donation flow, details, history, outlets and profile
- NGO opportunities, transparent matching reasons, acceptance, pickup and delivery
- Admin network health, organizations, donations, matches, exceptions and rule settings
- PWA manifest/service-worker configuration
- Supabase schema with Row Level Security policies
- Secure Supabase Edge Function for Gemini extraction and NGO-ready handoff briefs
- Demo services that make the full product story usable before backend setup

## Connect Supabase and Gemini

1. In the Supabase SQL editor, run `supabase/schema.sql`, then run `supabase/launch-readiness-migration.sql`, then run `supabase/organization-resubmission-migration.sql`.
   These add recorded admin reviews, read-state for notifications, missed-pickup handling, organization profile editing, and suspended-account resubmission.
2. The included environment files already point to the MealPing Supabase project. For a different project, change only the URL and **anon key** in both `src/environments/environment.ts` and `src/environments/environment.prod.ts`.
   Production builds automatically use `environment.prod.ts`.
3. Keep the Gemini key server-side and deploy the AI function:

   ```bash
   supabase secrets set GEMINI_API_KEY=your_gemini_key GEMINI_MODEL=gemini-3.5-flash
   supabase functions deploy extract-donation
   ```

   AI is an assistive shortcut, never a requirement: people can continue with the manual form whenever Gemini is busy or unavailable. The handoff brief summarizes confirmed pickup facts, flags missing operational information, and drafts a coordination message. It never certifies food safety, approves an organization, or chooses the receiving NGO.

   The `extract-donation` deployment must be refreshed after pulling code changes; it handles both form extraction and the AI handoff brief. The product labels this helper as a pilot and keeps the manual workflow available at every point.

4. Schedule deadline housekeeping so expired posts are closed even when nobody has the website open. No Edge Function deployment is needed for this option:

   - In Supabase, open **Integrations → Cron** (or **Cron → Jobs**).
   - Choose **Create job**, name it `expire-donations-every-5-minutes`, and set the schedule to `*/5 * * * *`.
   - Choose **Database function**, then select `refresh_expired_donations` and save.

   The included `expire-donations` Edge Function is only an optional alternative for an external scheduler; you can ignore it for now.

5. GPS pins are optional. When a user allows location access, MealPing stores only the coordinates for distance matching; the user still enters and confirms their address, city, state and pincode.

The Supabase anon key is intended for browser use when Row Level Security is enabled. Never put the Supabase service-role key or Gemini key in Angular environment files.

## Matching boundary

AI only extracts fields and identifies missing details. It never declares food safe and does not choose the receiving NGO. Matching should remain deterministic and auditable:

1. Filter by verified status, service radius, food policy, capacity and pickup window.
2. Rank eligible NGOs by configurable distance, timing, reliability and fairness weights.
3. Alert in ranked waves and record every outcome.

Before a real pilot, complete legal review, food-safety policy, organization verification, consent and incident-response procedures.


## Detected evidence (automated analysis)

Indexed codebase: 36 recognized source files, 262 KB.
- Angular (technology) — detected in the code
- HTML (language) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (55 of 55)

```
.editorconfig
.env.example
.gitignore
.vscode/extensions.json
.vscode/launch.json
.vscode/tasks.json
angular.json
ngsw-config.json
package.json
public/_headers
public/_redirects
public/.htaccess
public/manifest.webmanifest
README.md
src/app/app.component.html
src/app/app.component.scss
src/app/app.component.spec.ts
src/app/app.component.ts
src/app/app.config.ts
src/app/app.routes.ts
src/app/core/ai-extraction.service.ts
src/app/core/auth.guards.ts
src/app/core/auth.service.spec.ts
src/app/core/auth.service.ts
src/app/core/demo-data.service.ts
src/app/core/models.ts
src/app/core/supabase.service.ts
src/app/pages/admin/admin-pages.component.ts
src/app/pages/ngo/ngo-pages.component.ts
src/app/pages/partner/partner-pages.component.ts
src/app/pages/public/public-pages.component.ts
src/app/pages/shared/utility-page.component.ts
src/app/shared/app-shell.component.ts
src/app/shared/brand.component.ts
src/app/shared/public-header.component.ts
src/environments/environment.example.ts
src/environments/environment.prod.ts
src/environments/environment.ts
src/index.html
src/main.ts
src/styles.scss
supabase/admin-directory-migration.sql
supabase/bootstrap-admin.sql
supabase/cleanup-old-test-data.sql
supabase/config.toml
supabase/functions/expire-donations/index.ts
supabase/functions/extract-donation/index.ts
supabase/hackathon-demo-accounts.sql
supabase/launch-readiness-migration.sql
supabase/organization-resubmission-migration.sql
supabase/README.md
supabase/schema.sql
tsconfig.app.json
tsconfig.json
tsconfig.spec.json
```

### Dependencies

- package.json: @angular-devkit/build-angular@^21.2.0, @angular/animations@^21.2.0, @angular/cli@^21.2.0, @angular/common@^21.2.0, @angular/compiler@^21.2.0, @angular/compiler-cli@^21.2.0, @angular/core@^21.2.0, @angular/forms@^21.2.0, @angular/platform-browser@^21.2.0, @angular/platform-browser-dynamic@^21.2.0, @angular/router@^21.2.0, @angular/service-worker@^21.2.0, @primeuix/themes@^2.0.0, @supabase/supabase-js@^2.57.4, @types/jasmine@~5.1.0, gsap@^3.15.0, jasmine-core@~5.5.0, karma@~6.4.0, karma-chrome-launcher@~3.2.0, karma-coverage@~2.2.0, karma-jasmine@~5.1.0, karma-jasmine-html-reporter@~2.1.0, prettier@^3.6.2, primeicons@^7.0.0, primeng@^21.1.9, rxjs@~7.8.0, tslib@^2.3.0, typescript@~5.9.2, zone.js@~0.16.0

### Recent commits (newest first)

- Initial MealPing release

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "mealping",
  "version": "0.1.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "ng test --watch=false",
    "format": "npx prettier --write ."
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^21.2.0",
    "@angular/common": "^21.2.0",
    "@angular/compiler": "^21.2.0",
    "@angular/core": "^21.2.0",
    "@angular/forms": "^21.2.0",
    "@angular/platform-browser": "^21.2.0",
    "@angular/platform-browser-dynamic": "^21.2.0",
    "@angular/router": "^21.2.0",
    "@angular/service-worker": "^21.2.0",
    "@primeuix/themes": "^2.0.0",
    "@supabase/supabase-js": "^2.57.4",
    "gsap": "^3.15.0",
    "primeicons": "^7.0.0",
    "primeng": "^21.1.9",
    "rxjs": "~7.8.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.16.0"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^21.2.0",
    "@angular/cli": "^21.2.0",
    "@angular/compiler-cli": "^21.2.0",
    "@types/jasmine": "~5.1.0",
    "jasmine-core": "~5.5.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.2.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "~2.1.0",
    "prettier": "^3.6.2",
    "typescript": "~5.9.2"
  }
}

```

### src/main.ts

```typescript
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

```

### supabase/functions/expire-donations/index.ts

```typescript
import { createClient } from 'npm:@supabase/supabase-js@2';

/**
 * Server-only pickup-deadline housekeeping.
 *
 * Deploy it after setting CRON_SECRET, then call it every five minutes from a
 * scheduler with `x-mealping-cron-secret`. The browser also invokes the same
 * database routine as a safety net, but this function makes expiry reliable
 * when nobody has the app open.
 */
Deno.serve(async (request: Request) => {
  if (request.method !== 'POST') return response({ error: 'Method not allowed.' }, 405);

  const expectedSecret = Deno.env.get('CRON_SECRET');
  const receivedSecret = request.headers.get('x-mealping-cron-secret') ?? '';
  if (!expectedSecret || !safeEqual(receivedSecret, expectedSecret)) {
    return response({ error: 'Unauthorized.' }, 401);
  }

  const url = Deno.env.get('SUPABASE_URL');
  const serviceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');
  if (!url || !serviceRoleKey) return response({ error: 'Function environment is incomplete.' }, 500);

  const supabase = createClient(url, serviceRoleKey, {
    auth: { persistSession: false, autoRefreshToken: false }
  });
  const { data, error } = await supabase.rpc('refresh_expired_donations');
  if (error) {
    console.error('Donation expiry failed', error.message);
    return response({ error: 'Unable to refresh expired donations.' }, 500);
  }
  return response({ expired: Number(data ?? 0) });
});

function response(value: unknown, status = 200): Response {
  return new Response(JSON.stringify(value), {
    status,
    headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' }
  });
}

// Compare every position before returning so a timing response cannot reveal
// how much of the schedule secret matched.
function safeEqual(left: string, right: string): boolean {
  const length = Math.max(left.length, right.length);
  let result = left.length ^ right.length;
  for (let index = 0; index < length; index += 1) {
    result |= (left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);
  }
  return result === 0;
}

```

### supabase/functions/extract-donation/index.ts

```typescript
import { createClient } from 'npm:@supabase/supabase-js@2';

const maxRequestCharacters = 6_000_000;
const allowedImageTypes = new Set(['image/jpeg', 'image/png', 'image/webp']);

const extractionSystemPrompt = `You extract structured fields from a food donor's message for MealPing.
Return JSON only with: foodName, meals, category, dietary, preparedAt, pickupBy, outlet, address, notes, missing, confidence.
dietary must be Vegetarian, Non-vegetarian, or Mixed. meals is a positive integer.
Do not decide that food is safe. Do not invent missing values; list their field names in missing.
Keep times in the user's phrasing when a timezone/date cannot be established.
Always include every field. Use an empty string for an unavailable text field, 0 for unknown meals, and keep notes concise.`;

const handoffSystemPrompt = `You prepare a human-reviewable food-pickup coordination brief for MealPing.
Use only the supplied donation facts. Return JSON only with: pickupSummary, checklist, missing, ngoMessage.
pickupSummary: concise factual pickup summary, maximum two sentences.
checklist: 3 to 5 practical pickup-coordination actions. Do not state a fact unless it was supplied.
missing: only field names that are genuinely absent from the supplied facts, maximum 6.
ngoMessage: a concise draft message the food partner may copy after a verified NGO accepts.
Never say food is safe, fresh, approved, eligible, matched, or guaranteed. Never choose or rank an NGO. Never add allergens, packaging, contact information, dates, times, or food facts that were not supplied.`;

const extractionResponseSchema = {
  type: 'object',
  properties: {
    foodName: { type: 'string' },
    meals: { type: 'integer' },
    category: { type: 'string' },
    dietary: { type: 'string', enum: ['Vegetarian', 'Non-vegetarian', 'Mixed'] },
    preparedAt: { type: 'string' },
    pickupBy: { type: 'string' },
    outlet: { type: 'string' },
    address: { type: 'string' },
    notes: { type: 'string' },
    missing: { type: 'array', items: { type: 'string' } },
    confidence: { type: 'number' }
  },
  required: ['foodName', 'meals', 'category', 'dietary', 'preparedAt', 'pickupBy', 'outlet', 'address', 'notes', 'missing', 'confidence']
};

const handoffResponseSchema = {
  type: 'object',
  properties: {
    pickupSummary: { type: 'string' },
    checklist: { type: 'array', items: { type: 'string' } },
    missing: { type: 'array', items: { type: 'string' } },
    ngoMessage: { type: 'string' }
  },
  required: ['pickupSummary', 'checklist', 'missing', 'ngoMessage']
};

Deno.serve(async (request: Request) => {
  const origin = request.headers.get('Origin');
  const headers = corsHeaders(origin);

  if (!originAllowed(origin)) return json({ error: 'Origin is not allowed.' }, 403, headers);
  if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers });
  if (request.method !== 'POST') return json({ error: 'Method not allowed.' }, 405, headers);

  try {
    const authorization = request.headers.get('Authorization') ?? '';
    const token = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
    if (!token) return json({ error: 'Authentication is required.' }, 401, headers);

    const supabaseUrl = Deno.env.get('SUPABASE_URL');
    const anonKey = Deno.env.get('SUPABASE_ANON_KEY');
    if (!supabaseUrl || !anonKey) throw new Error('Supabase function environment is incomplete');

    const supabase = createClient(supabaseUrl, anonKey, {
      global: { headers: { Authorization: authorization } },
      auth: { persistSession: false, autoRefreshToken: false }
    });
    const { data: userData, error: userError } = await supabase.auth.getUser(token);
    if (userError || !userData.user) return json({ error: 'Invalid or expired session.' }, 401, headers);

    const contentLength = Number(request.headers.get('Content-Length') ?? 0);
    if (contentLength > maxRequestCharacters) return json({ error: 'Request is too large.' }, 413, headers);
    const rawBody = await request.text();
    if (rawBody.length > maxRequestCharacters) return json({ error: 'Request is too large.' }, 413, headers);

    let body: Record<string, unknown>;
    try {
      body = JSON.parse(rawBody) as Record<string, unknown>;
    } catch {
      return json({ error: 'Invalid JSON request.' }, 400, headers);
    }

    const mode = body.mode === 'handoff' ? 'handoff' : 'extract';
    if (mode === 'handoff') {
      const donation = readHandoffDonation(body.donation);
      if (!donation.foodName || Number(donation.meals) < 1 || !donation.outlet || !donation.address || !donation.pincode || !donation.pickupBy) {
        return json({ error: 'Complete the food and pickup details before preparing a handoff.' }, 400, headers);
      }

      const { data: quotaAvailable, error: quotaError } = await supabase.rpc('consume_ai_quota');
      if (quotaError) throw new Error('AI quota check failed');
      if (quotaAvailable !== true) return json({ error: 'AI usage limit reached. Please try again later.' }, 429, headers);

      const apiKey = Deno.env.get('GEMINI_API_KEY');
      const model = Deno.env.get('GEMINI_MODEL') ?? 'gemini-3.5-flash';
      if (!apiKey) throw new Error('GEMINI_API_KEY is not configured');
      if (!/^[a-zA-Z0-9._-]+$/.test(model)) throw new Error('GEMINI_MODEL is invalid');

      const source = JSON.stringify(donation);
      let parsed: Record<string, unknown>;
      try {
        parsed = await requestExtractionFromGemini(apiKey, model, [{ text: `${handoffSystemPrompt}\n\nConfirmed donation facts:\n${source}` }], handoffResponseSchema);
      } catch (error) {
        if (!(error instanceof Error) || error.message !== 'Gemini returned malformed structured data') throw error;
        parsed = await requestExtractionFromGemini(apiKey, model, [{ text: `${handoffSystemPrompt}\n\nConfirmed donation facts:\n${source}\n\nReturn one compact, complete JSON object only. Do not add explanation or markdown.` }], handoffResponseSchema);
      
[truncated — 8308 more characters]
```

### supabase/bootstrap-admin.sql

```sql
-- Replace the email, create that user in Supabase Auth, then run this once.
-- The app reads the role from public.profiles, not editable browser metadata.

update public.profiles
set role = 'admin', organization_id = null
where id = (select id from auth.users where email = 'YOUR_ADMIN_EMAIL');

delete from public.organizations o
where not exists (select 1 from public.profiles p where p.organization_id = o.id);

```

### supabase/admin-directory-migration.sql

```sql
-- Run this once in Supabase SQL Editor for an existing MealPing project.
-- It gives only authenticated admins access to the complete organization directory.

create or replace function public.get_admin_organizations()
returns table (
  id uuid,
  name text,
  role public.app_role,
  area text,
  address text,
  city text,
  state text,
  pincode text,
  status public.organization_status,
  accepting_donations boolean,
  service_radius_km numeric,
  max_meals_per_pickup integer,
  accepts_non_vegetarian boolean,
  created_at timestamptz
)
language sql stable security definer set search_path = public as $$
  select o.id, o.name, o.role, o.area, o.address, o.city, o.state, o.pincode,
    o.status, o.accepting_donations, o.service_radius_km, o.max_meals_per_pickup,
    o.accepts_non_vegetarian, o.created_at
  from public.organizations o
  where private.is_admin()
  order by o.created_at desc
$$;

grant execute on function public.get_admin_organizations() to authenticated;

```

### src/index.html

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self' https://jdhyqprkukqqrbjgmowl.supabase.co wss://jdhyqprkukqqrbjgmowl.supabase.co; worker-src 'self' blob:; manifest-src 'self'; form-action 'self'; require-trusted-types-for 'script'; trusted-types angular angular#bundler;" />
  <meta name="referrer" content="strict-origin-when-cross-origin" />
  <title>MealPing — Ping surplus. Rescue meals.</title>
  <meta name="description" content="AI-assisted surplus food rescue for restaurants, hotels and nearby NGOs." />
  <meta name="theme-color" content="#123c2f" />
  <link rel="icon" type="image/svg+xml" href="mealping-mark.svg" />
  <link rel="manifest" href="manifest.webmanifest" />
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Manrope:wght@500;600;700;800&family=Playfair+Display:wght@500;600;700&display=swap" rel="stylesheet" />
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <app-root></app-root>
</body>
</html>

```

### supabase/cleanup-old-test-data.sql

```sql
-- Remove the previous experimental MealPing test data while retaining only the
-- three intentional hackathon review accounts created on 19 July 2026.
--
-- Kept accounts:
--   partner.demo@mealping.test
--   ngo.demo@mealping.test
--   admin.demo@mealping.test
--
-- This deletes only the old Auth users and their dependent sample activity.

begin;

-- Review records use a restrictive reviewer foreign key, so remove those first.
with target_users as (
  select id
  from auth.users
  where email in (
    'mejitu99@gmail.com',
    'mejitu99+2@gmail.com',
    'mejitu99+3@gmail.com',
    'mejitu99+9@gmail.com'
  )
)
delete from public.organization_review_logs log
using target_users target
where log.reviewer_id = target.id;

-- Clear activity tied to old test users. Donation notifications and related
-- responses cascade when their donation is removed.
with target_users as (
  select id
  from auth.users
  where email in (
    'mejitu99@gmail.com',
    'mejitu99+2@gmail.com',
    'mejitu99+3@gmail.com',
    'mejitu99+9@gmail.com'
  )
)
delete from public.donation_responses response
using target_users target
where response.responded_by = target.id;

with target_users as (
  select id
  from auth.users
  where email in (
    'mejitu99@gmail.com',
    'mejitu99+2@gmail.com',
    'mejitu99+3@gmail.com',
    'mejitu99+9@gmail.com'
  )
)
delete from public.donations donation
using target_users target
where donation.created_by = target.id;

-- The profile is deleted explicitly for clarity; Auth would also cascade it.
with target_users as (
  select id
  from auth.users
  where email in (
    'mejitu99@gmail.com',
    'mejitu99+2@gmail.com',
    'mejitu99+3@gmail.com',
    'mejitu99+9@gmail.com'
  )
)
delete from public.profiles profile
using target_users target
where profile.id = target.id;

with target_users as (
  select id
  from auth.users
  where email in (
    'mejitu99@gmail.com',
    'mejitu99+2@gmail.com',
    'mejitu99+3@gmail.com',
    'mejitu99+9@gmail.com'
  )
)
delete from auth.users account
using target_users target
where account.id = target.id;

-- Remove only leftover organizations that no longer belong to any profile.
-- The two active hackathon organizations have profiles and are not affected.
delete from public.organizations organization
where not exists (
  select 1 from public.profiles profile
  where profile.organization_id = organization.id
);

commit;

-- Expected remaining Auth accounts:
-- partner.demo@mealping.test, ngo.demo@mealping.test, admin.demo@mealping.test

```

### supabase/hackathon-demo-accounts.sql

```sql
-- MealPing hackathon demo-account setup.
-- Run only after registering these three accounts through the MealPing app:
--   partner.demo@mealping.test
--   ngo.demo@mealping.test
--   admin.demo@mealping.test
--
-- This script promotes the dedicated test admin and verifies the two test
-- organizations. It is intentionally limited to the exact demo email addresses
-- and can be run again safely.

do $$
declare
  v_admin_id uuid;
  v_partner_org_id uuid;
  v_ngo_org_id uuid;
  v_partner_previous public.organization_status;
  v_ngo_previous public.organization_status;
begin
  select id into v_admin_id
  from auth.users
  where email = 'admin.demo@mealping.test';

  select p.organization_id into v_partner_org_id
  from public.profiles p
  join auth.users u on u.id = p.id
  where u.email = 'partner.demo@mealping.test';

  select p.organization_id into v_ngo_org_id
  from public.profiles p
  join auth.users u on u.id = p.id
  where u.email = 'ngo.demo@mealping.test';

  if v_admin_id is null or v_partner_org_id is null or v_ngo_org_id is null then
    raise exception 'Create the three MealPing demo accounts before running this script.';
  end if;

  -- The admin account is first registered through the normal app so it has a
  -- safe Auth password; it is then promoted and detached from its placeholder organization.
  update public.profiles
  set role = 'admin', organization_id = null
  where id = v_admin_id;

  delete from public.organizations o
  where o.name = 'MealPing Demo Operations'
    and not exists (select 1 from public.profiles p where p.organization_id = o.id);

  select status into v_partner_previous from public.organizations where id = v_partner_org_id;
  update public.organizations set status = 'verified', updated_at = now() where id = v_partner_org_id;

  select status into v_ngo_previous from public.organizations where id = v_ngo_org_id;
  update public.organizations set status = 'verified', updated_at = now() where id = v_ngo_org_id;

  insert into public.organization_review_logs (
    organization_id, reviewer_id, previous_status, new_status,
    location_confirmed, phone_confirmed, evidence_confirmed, note
  )
  select v_partner_org_id, v_admin_id, v_partner_previous, 'verified', true, true, true,
    'Hackathon demo account verified for judge testing.'
  where not exists (
    select 1 from public.organization_review_logs
    where organization_id = v_partner_org_id
      and note = 'Hackathon demo account verified for judge testing.'
  );

  insert into public.organization_review_logs (
    organization_id, reviewer_id, previous_status, new_status,
    location_confirmed, phone_confirmed, evidence_confirmed, note
  )
  select v_ngo_org_id, v_admin_id, v_ngo_previous, 'verified', true, true, true,
    'Hackathon demo account verified for judge testing.'
  where not exists (
    select 1 from public.organization_review_logs
    where organization_id = v_ngo_org_id
      and note = 'Hackathon demo account verified for judge testing.'
  );

  insert into public.notifications (user_id, title, body, tone)
  select p.id, 'Organization verified', 'Your hackathon demo organization is ready to use.', 'success'
  from public.profiles p
  where p.organization_id in (v_partner_org_id, v_ngo_org_id)
    and not exists (
      select 1 from public.notifications n
      where n.user_id = p.id
        and n.title = 'Organization verified'
        and n.body = 'Your hackathon demo organization is ready to use.'
    );
end;
$$;

-- Public test credentials for judges. These accounts contain only dummy data.
-- Food Partner: partner.demo@mealping.test / Demo@MealPing26
-- NGO:          ngo.demo@mealping.test / Demo@MealPing26
-- Admin:        admin.demo@mealping.test / Demo@MealPing26

```

### supabase/organization-resubmission-migration.sql

```sql
-- MealPing organization profile editing and resubmission.
-- Run this AFTER launch-readiness-migration.sql.

create or replace function public.update_my_organization(
  p_name text,
  p_full_name text,
  p_phone text,
  p_area text,
  p_address text,
  p_city text,
  p_state text,
  p_pincode text,
  p_latitude double precision default null,
  p_longitude double precision default null,
  p_service_radius_km numeric default null,
  p_max_meals_per_pickup integer default null,
  p_accepts_non_vegetarian boolean default null,
  p_resubmission_note text default ''
) returns public.organizations
language plpgsql security definer set search_path = public, extensions as $$
declare
  v_profile public.profiles;
  v_organization public.organizations;
  v_updated public.organizations;
  v_note text := trim(coalesce(p_resubmission_note, ''));
begin
  select * into v_profile from public.profiles where id = auth.uid();
  if v_profile.id is null or v_profile.role not in ('partner', 'ngo') or v_profile.organization_id is null then
    raise exception 'Only an organization account can update these details';
  end if;
  select * into v_organization from public.organizations where id = v_profile.organization_id for update;
  if v_organization.id is null then raise exception 'Organization not found'; end if;

  if nullif(trim(p_name), '') is null or char_length(trim(p_name)) not between 2 and 160
    or nullif(trim(p_full_name), '') is null or char_length(trim(p_full_name)) not between 2 and 120
    or nullif(trim(p_phone), '') is null or char_length(trim(p_phone)) not between 7 and 30
    or nullif(trim(p_area), '') is null or char_length(trim(p_area)) > 100
    or nullif(trim(p_address), '') is null or char_length(trim(p_address)) not between 3 and 500
    or nullif(trim(p_city), '') is null or char_length(trim(p_city)) > 100
    or nullif(trim(p_state), '') is null or char_length(trim(p_state)) > 100
  then raise exception 'Enter valid organization, contact, and address details'; end if;
  if trim(p_pincode) !~ '^[1-9][0-9]{5}$' then raise exception 'Enter a valid 6-digit Indian pincode'; end if;
  if (p_latitude is null) <> (p_longitude is null) then raise exception 'Location coordinates must include both latitude and longitude'; end if;
  if p_latitude is not null and (p_latitude not between -90 and 90 or p_longitude not between -180 and 180) then
    raise exception 'Location coordinates are invalid';
  end if;
  if v_profile.role = 'ngo' then
    if p_service_radius_km is null or p_service_radius_km not between 1 and 100 then raise exception 'Pickup radius must be between 1 and 100 km'; end if;
    if p_max_meals_per_pickup is null or p_max_meals_per_pickup not between 1 and 10000 then raise exception 'Maximum pickup must be between 1 and 10000 meals'; end if;
  end if;
  if char_length(v_note) > 1000 then raise exception 'Resubmission note is too long'; end if;
  if v_organization.status = 'suspended' and char_length(v_note) < 10 then
    raise exception 'Explain what you corrected in at least 10 characters before requesting another review';
  end if;

  update public.profiles
  set full_name = trim(p_full_name), phone = trim(p_phone)
  where id = auth.uid();

  update public.organizations set
    name = trim(p_name), area = trim(p_area), address = trim(p_address), city = trim(p_city),
    state = trim(p_state), pincode = trim(p_pincode), latitude = p_latitude, longitude = p_longitude,
    service_radius_km = case when v_profile.role = 'ngo' then p_service_radius_km else service_radius_km end,
    max_meals_per_pickup = case when v_profile.role = 'ngo' then p_max_meals_per_pickup else max_meals_per_pickup end,
    accepts_non_vegetarian = case when v_profile.role = 'ngo' then coalesce(p_accepts_non_vegetarian, false) else accepts_non_vegetarian end,
    status = case when status = 'suspended' then 'pending'::public.organization_status else status end,
    updated_at = now()
  where id = v_organization.id
  returning * into v_updated;

  if v_organization.status = 'suspended' then
    insert into public.organization_review_logs (
      organization_id, reviewer_id, previous_status, new_status,
      location_confirmed, phone_confirmed, evidence_confirmed, note
    ) values (
      v_organization.id, auth.uid(), 'suspended', 'pending', false, false, false,
      'Organization resubmission: ' || v_note
    );

    insert into public.notifications (user_id, title, body, tone)
    select p.id, 'Organization resubmitted for review',
      v_updated.name || ' updated its details and requested another verification review.', 'info'
    from public.profiles p where p.role = 'admin';

    insert into public.notifications (user_id, title, body, tone)
    values (auth.uid(), 'Details sent for review', 'Your corrected organization details were submitted. An admin will review them before restoring workspace access.', 'info');
  else
    insert into public.notifications (user_id, title, body, tone)
    values (auth.uid(), 'Organization details updated', 'Your account details were updated successfully.', 'success');
  end if;

  return v_updated;
end;
$$;

grant execute on function public.update_my_organization(text, text, text, text, text, text, text, text, double precision, double precision, numeric, integer, boolean, text) to authenticated;

```

[25 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]