Skip to content

Manager MVC → AngularJS SPA migration plan

Status: Proposed — pending review (Zafer) Created: 2026-06-13 Goal: Eliminate the old MVC Razor view layer in the Manager admin app.


1. The real situation: a hybrid app, ~70% already migrated

The Manager admin runs two apps side by side:

  • OLD — ASP.NET MVC Razor pages at /Manager/{Controller}/{Action}, in Application/Areas/Manager/Views/{Folder}/*.cshtml plus controllers in Application/Areas/Manager/Controllers/*.cs.
  • NEW — AngularJS (ui-router) SPA at /ng#/manager/{state}, in Raklet.Backend/Content/scripts/core/manager/{module}/. Each module declares $stateProvider.state('manager.{area}...') in its *.module.js.

The page that triggered this review (/Manager/Account/DonationCampaignCreate) is a legacy leftover: the SPA already has manager.fundraising.campaigns.create, and even the old nav header (Views/Shared/ContentHeader/_Fundraising.cshtml) links to /ng#/manager/fundraising/campaigns/. The old URL is reachable by direct link but is no longer the wired path. Most "old pages" are in this state.

2. Migration status of all 14 Manager controllers

Coverage = full (SPA has equivalent states): Home → dashboard · CorporateMembers / FastAccess → contacts · CustomMemberTypes → membership · Tags → settings/tags · JobPostings → jobs

Coverage = partial (SPA covers main screens; sub-pages still on Razor):

Controller In SPA Still on Razor
Account (fundraising+payments) campaigns, donations, scheduled payment edit/delete, campaign image+thumbnail upload/crop, scheduled-failed payments/donations
EventV3 list, create, edit, details, settings manual ticket add/register/delete, ticket payment details, manage custom fields, mail/SMS to participants
MemberLists contacts segments filter add/remove builder, export
Members contacts family members (view/create/edit/remove)
Reports overview, payments, refunds, logs name changes, email activities, phone activities
Settings general, webhooks, fields payment-account OAuth/activation, Stripe bank account, Iyzico setup

Coverage = none:

  • OfferedServices: Index, Create, Edit, Delete, Preview, ServicesAwaitingApproval (no SPA module; only a nav reference in settings-navigation.controller.js)
  • Announcement: redirects out, but the SPA target is unconfirmed

3. Three findings that correct the goal

  1. Redirect shims are load-bearing. TagsController.cs is a 301 RedirectPermanent to the SPA. It has no view — it keeps old bookmarks, email links, and external deep-links alive. Deleting it 404s those URLs. The thin redirect layer must survive.
  2. Payment-account flows can't fully leave the server. SettingsController.cs has Stripe OAuth authorize/callback handlers. Providers redirect back to a server URL, so those callback endpoints stay as MVC actions. The UI can move to the SPA; the controller can't be deleted.
  3. Coverage ≠ safe-to-delete. JobPostingsController.Create() still return View(...) even though manager.job.create exists. A Razor view is only deletable when the SPA has parity AND nothing links to the old action. The sweep is a per-page verification exercise, not a trust-the-classification delete.

4. Premises

  1. End state = zero .cshtml views under Application/Areas/Manager/Views/, not deleting the area. A thin controller layer survives for (a) 301 redirects and (b) provider OAuth/callback endpoints.
  2. "Migrate" splits three ways — delete (dead duplicates + dead features), finish (partial areas), build (net-new modules). Not "rewrite 50 pages."
  3. The expensive 20% (payment-account OAuth UI, OfferedServices) must not gate the cheap, high-value cleanup of the already-migrated 70%.

4a. Production usage data (App Insights, 90d) — corrects the strategy

Before committing to "cleanup first," we pulled real traffic for every /Manager/* path, splitting each hit into rendered-200 (old Razor page actually showed) vs redirect-3xx (bounced to the SPA — already a shim). Reproduce with scripts/dev/manager-page-usage.ps1. (Counts are floors due to App Insights sampling; trust the relative distinct-org breadth.)

The data flips the priority:

  • The value is in EventV3 ticketing, not cleanup. Highest traffic + broadest org usage is on not-migrated EventV3 flows, all still rendering Razor: ticketpaymentdetails (880 renders / 34 orgs), addmanuallyticket (382 / 51 orgs), createticketoptions (51 / 27), managefields (24 / 5), deleteticketoption (16 / 7).
  • "Fully migrated" fundraising isn't cut over. donationcampaigncreate still renders for 21 orgs and donationcampaignedit for 17, despite SPA equivalents existing — the cutover is incomplete, not a dead leftover.
  • Already-migrated shims are working (0 renders, all 3xx): members/edit (35 orgs), reports/loginactivities (41 orgs), eventv3 create/edit/details, account/payments, corporatemembers/edit, custommembertypes/edit, fastaccess, Settings OAuth-return pages. Leave them.
  • Genuine cleanup is a short, low-traffic tail (≤2-5 orgs each): memberlists/index, reports name/email/phone activities (no SPA equivalent — build-vs-kill), scheduledfaileddonations, deletemanualticket, deletestripepaymentaccount.

5. Strategy: usage-led — finish EventV3 first, cleanup is the tail

Originally "cleanup sweep first for the biggest visible win." The usage data doesn't support that — pure cleanup touches ~6 low-traffic pages. Re-sequenced by real org breadth on Razor:

  1. EventV3 ticketing → SPA (ticketpaymentdetails, add/create/delete manual ticket, managefields). Highest org count still on Razor; biggest UX win.
  2. Fundraising create/edit cutover — verify SPA parity, repoint entry points, 301 the old pages (21 + 17 orgs affected).
  3. Settings payment-account UI (addorupdate, paymentedit) — keep OAuth callbacks server-side.
  4. Trivial tail — retire the low-traffic list; for reports activity pages with no SPA equivalent, decide build-vs-kill (2-5 orgs each).

The dead-view sweep below still runs, but as the tail (step 4), not the lead.

Phase 1 — Dead-view sweep (one PR, low risk)

Gated on a per-page inbound-link audit. For every old Manager action, grep the whole solution for inbound references before deleting its view:

  • Url.Action("Action", "Controller"), @Html.ActionLink, @Url.Action
  • RedirectToAction("Action", "Controller")
  • hardcoded href="/Manager/Controller/Action"
  • SPA nav templates (manager-navigation, ContentHeader/*.cshtml)
  • emails/templates under Services/, Raklet.Webjob/, public-side Application/Controllers/

Decision per page:

  • Zero inbound links + SPA parity → delete the .cshtml, collapse the GET action into a RedirectPermanent to the SPA state (preserves the URL).
  • Has inbound links → repoint the link at the SPA first, then delete.
  • POST/callback only → leave the action; it's an API/callback endpoint, not a page.

Strong delete candidates (each still requires the audit): the full-coverage set — Tags (done), CorporateMembers, FastAccess, CustomMemberTypes shims; JobPostings Create/Edit/Delete/Preview views; the DonationCampaign* Account views whose SPA states already exist.

Deliverable: a one-time audit script (scripts/dev/manager-deadview-audit.ps1) that emits, per old action: inbound-link count, SPA-parity Y/N, recommendation. PR #1 deletes everything it marks zero-inbound + parity.

Phase 2+ — Gaps become ranked backlog items

Not part of the sweep. Each is a normal product card, ranked by real usage (App Insights page-hit telemetry), not by guess:

  • Finish partials (SPA module exists, add states): Account payment/image/scheduled, EventV3 ticket+participant flows, MemberLists export/filter, Members family, Reports activity reports, Settings payment-account UI.
  • Build net-new: OfferedServices — but first pull usage; if it's a dead feature, delete it instead of migrating. Confirm Announcement's SPA target.
  • Keep server-side (never deleted, may get SPA UI wrappers): Stripe/Iyzico OAuth authorize+callback, AddBankAccountToStripe, ActivateAccount.

6. Risks

  • Silent 404s from deleting a view that still has one inbound link. Mitigated by the audit script + 301 collapse.
  • Killing an OAuth callback breaks live payment onboarding. Mitigated by the "POST/callback only → leave it" rule.
  • Assumed SPA parity that isn't real (SPA create page missing a field the Razor one has). Each "finish partial" card needs a parity check before the old view dies.
  • Scope creep: the partial/none piles are tempting to over-build. Rank by usage; delete unused features rather than migrating them.

7. First action

Write scripts/dev/manager-deadview-audit.ps1: for each public GET action across the 14 Manager controllers, print {controller, action, inbound_link_count, spa_state_exists, recommendation}. Run it, eyeball the zero-inbound + parity rows, and ship PR #1 deleting that confirmed-dead set with GET actions collapsed to RedirectPermanent. That single PR proves the pattern and removes the largest, safest chunk of Razor in one move — before any feature-build work starts.