A mobile-first Flutter Web tattoo catalog and order manager for Iraq. Customers select designs and quantities, generate portrait images with a tracking code, and contact @ge.to.ge on Instagram or 07778700244 on WhatsApp. Customer accounts and online payments are not required.
Apply all pending migrations, including 202610030001_orders.sql, 202610030002_staff_order_confirmation.sql, 202610040003_order_codes_and_source.sql, 202610040004_manual_orders.sql, and 202610060001_share_source.sql, before using real orders. The order-code migration defaults new orders to Instagram and saves tattoo codes with order items, filling missing codes on older orders from available catalog records. Existing saved sources stay unchanged. Administration order details show each tattoo’s name and code; booking messages omit zero-value discount lines. The default test mode demonstrates the workflow in memory: orders, employees, and settings reset on reload. Production orders persist in Supabase. No production migration or deployment is performed by editing this repository.
/admin/orders; customer name, social username, and source platform are optional. Customers do not see the completion form or initial confirmation button, including on the image-sharing screen. Customer tokens cannot edit these details or initially confirm an order. Available sources are Instagram, WhatsApp, TikTok, Facebook, Website, and Other./admin/orders to search by code, phone, name, or username, filter by status, edit orders, see their creator, and progress fulfillment. Private order reads and mutations require an allowlisted administrator or employee. Database functions enforce transitions, prices, accepted quantities, and optimistic versions; stale edits are rejected instead of silently overwriting another change. Audit events record actor, time, version, and state transitions, including edits that retain the current state./admin/orders, search available tattoos by name or code, and enter quantities. إنشاء الطلب وإكمال البيانات saves a pending order and opens the existing customer/delivery form; fill it out, adjust prices as needed, then confirm. No customer account, browser, or selection-image export is needed. Manual selections do not alter this device’s customer basket or receipts. Apply 202610040004_manual_orders.sql before using this in production; the RPC requires an allowlisted administrator (or employee after the employee-access migration) and records staff creation in the audit log. Retry a failed save from the same screen to recover the same order; after leaving/reloading, check the order list before starting again./admin/settings sets the default delivery fee for new orders (initially zero). Existing orders retain their delivery snapshot; staff may edit it before shipping./admin/team creates employees with name, phone, login email and password. Each account receives employee permissions automatically. Team performance counts orders by their original creator, including all-time order counts, fulfilled orders, accepted pieces and accepted order value. Partial-delivery value charges delivery once when any pieces are accepted and allocates the additional discount proportionally across merchandise, rounded to the nearest dinar. These are fulfillment metrics, not a payment ledger.Apply all pending migrations through supabase/migrations/202611020002_employee_accounts.sql, deploy the save-employee Edge Function, then release the updated app:
npx supabase db push
npx supabase functions deploy save-employee
These commands target the linked Supabase project; repository changes do not deploy automatically. The function validates the signed-in user with Auth and checks is_admin() before creating an account. Its server-only service role key is provided by Supabase; never place it in Flutter configuration. The Auth Admin API creates the confirmed login, while a database trigger atomically creates/links the employee profile and adds employee_users membership. Passwords are only sent to Auth and are never stored in employee records, app metadata or logs. Public signup stays disabled. Reference: Supabase Auth Admin API.
In الموظفون, add the employee’s name, phone, email and password (at least 8 characters). They can immediately sign in at /admin with those credentials and see only الطلبات (orders) and صور للزبون (customer images). Direct links to other administration sections show orders. They can create, edit, confirm and fulfill orders and export visible catalog images. They cannot manage catalog/offer/storage content, employees, team performance or default delivery settings. Do not add employees to admin_users, which grants full administration access.
Existing name/phone-only employee records ask for credentials when edited. An email already registered in Auth is rejected rather than taking over that account; use a new email or have a trusted administrator link the existing restricted Auth account to the employee record. Linked login emails are read-only in the employee editor; subsequent edits update name and phone. Change existing login credentials through trusted Supabase Auth administration. Removing a user from employee_users immediately revokes order access, independently of the profile.
New orders record the signed-in creator on the server. أُنشئ بواسطة replaces the employee selector; retries, later edits and staff changes preserve the original identity and name. Customer-created orders stay attributed to the customer, including when staff later confirm them. Existing creation events backfill known creators; legacy assignment alone is not treated as proof of who created an order. Historical employee names unavailable in the event/profile data display a generic employee label. Demo mode supports employee creation and sign-in in memory only; accounts and orders reset on reload.
Order checks: flutter test test/employee_account_test.dart test/employee_access_test.dart test/test_admin_service_test.dart test/order_repository_test.dart test/order_ui_test.dart; database authorization and lifecycle checks: run node tool/employee_function_test.cjs and execute supabase/tests/employee_accounts.sql, supabase/tests/employee_access.sql, supabase/tests/orders.sql and supabase/tests/manual_orders.sql against a migrated test database. SQL fixtures are rolled back.
Open العروض in administration (/admin/offers) and choose إضافة عرض. Enter a name and a total price in IQD, search existing tattoos by name or code, and select one or more designs (up to 100). Selections remain when searching or loading more results. Each offer includes one piece of every selected tattoo. Save, edit, hide, or delete the offer without changing the original tattoos or their individual prices.
The main catalog shows the newest available offers in a swipeable carousel above search, with previous/next buttons and a page counter. It disappears when no offers are available. Customers can open كل العروض or the price-tag icon (/offers) to see all offers and included artwork. Both the carousel and full offers page show the total price excluding delivery and support Instagram and WhatsApp inquiries. Instagram opens the store chat at @ge.to.ge, where customers mention the offer name; WhatsApp opens a prefilled draft. Customers send messages manually. Offers are displayed for inquiry and do not automatically apply bundle discounts to the regular selection/order flow.
Apply supabase/migrations/202610140001_offers.sql after all earlier pending migrations before using offers with Supabase. Only administrators may save or delete offers. Public reads hide inactive offers and the entire bundle when any included tattoo or all its categories are hidden. Remove a tattoo from its offers before deleting that tattoo; deleting an offer preserves all tattoos. Test-mode offers stay in memory and reset on reload. No production migration is applied by editing this repository.
Checks: flutter test test/offer_repository_test.dart test/offers_ui_test.dart; run supabase/tests/offers.sql against a disposable migrated Supabase database for atomic saves, membership validation, and access control.
index.html.flutter pub get
Test mode is enabled by default for now. Run flutter run -d chrome to browse 36 named temporary designs across four categories and six exact sizes, with search tags, bundled placeholder galleries, and no Supabase connection. Search, sorting, pagination, details, selections, and image export work locally. Open /admin and sign in with test@gmail.com / 1234 to test admin management, including category/product edits and image uploads. This demo account works only with USE_TEST_DATA=true; all changes and the login session stay local and reset when the app reloads. The inventory resets with the app; test selections persist separately from real catalog selections.
Control the mode with USE_TEST_DATA in your configuration JSON or --dart-define=USE_TEST_DATA=true / --dart-define=USE_TEST_DATA=false. Restart/rebuild after changing it; hot reload does not apply compile-time flags. Test mode takes precedence even when Supabase credentials are supplied.
To connect to real data, copy config.example.json to config.local.json, set USE_TEST_DATA to false, then provide your project URL and public anon/publishable key. Both values are public by design. Never put a service-role/secret key, database password, or real admin password in this file or any Flutter build.
{
"USE_TEST_DATA": false,
"SUPABASE_URL": "https://your-project.supabase.co",
"SUPABASE_ANON_KEY": "your-public-publishable-key"
}
The legacy name SUPABASE_ANON_KEY accepts either an anon key or the newer publishable key. Configuration is compiled into Flutter Web; changing it requires a rebuild. Temporary inventory lives in lib/repositories/test_catalog_repository.dart and its placeholder images in assets/test_catalog. With test mode disabled and no Supabase configuration, the public catalog shows a retry state and /admin explains the missing setup.
On a new project, run all files in supabase/migrations in filename order in the Supabase SQL editor. For an existing installation, apply any pending migrations in order, including 202610120001_optional_tattoo_names.sql, 202610120002_tattoo_code_name_fallback.sql, and 202610120003_category_thumbnails.sql, before using this version with Supabase. Tattoo codes are required, accept letters, numbers, symbols, and mixed text, preserve leading zeros, and must be unique. Empty or whitespace-only codes are rejected. Tattoo names are optional; tattoos with null, empty, or whitespace-only names display their code throughout the app and generated images. Alternatively link a project and apply migrations using the Supabase CLI:
npx supabase login
npx supabase link --project-ref YOUR_PROJECT_REF
npx supabase db push
The migration creates:
categories, products, many-to-many product_categories, and an administrator allowlist admin_users.catalog_products view aggregates category memberships before pagination and applies caller RLS. Product saves use the transactional save_catalog_product RPC.catalog_available_sizes). Selecting several sizes matches any complete pair, preserving orientation; 5 × 8 and 8 × 5 are different sizes. The existing size sort still orders by area.202610010002_tag_filters.sql for the tag-options RPC and array index when using Supabase.admin_users. Signing up or merely holding an authenticated token does not grant management access. Clients cannot read or modify the allowlist.tattoo-images (10 MiB), tattoo-thumbnails (2 MiB), and category-images (10 MiB), with PNG/JPEG/WebP allowlists. Only allowlisted administrators can upload, update, or delete objects.Public bucket URLs remain readable to anyone who already knows the URL after a product is hidden. Deactivation removes catalog discoverability; it is not a revocation of previously shared images. For actual image removal, an administrator can delete the object in Supabase Storage. Do not remove a file still referenced by another record.
Apply 202610300001_specific_body_placements.sql before deploying the expanded body-location choices. The admin editor and customer filters share the same 20 options, ordered from ذراع كامل through الفخذ. Existing arm and hand assignments are retained and displayed as ذراع كامل and الكف.
insert into public.admin_users (user_id)
values ('REPLACE-WITH-AUTH-USER-UUID');
/admin, then sign in. Never share this account with customers.admin_users; database/storage rules apply immediately. Revoke their Auth sessions as well when appropriate.وشومات سوار, upload its image if available, set the numeric sort order, and save.001. Use إضافة صورة again for each additional image. The first image is the cover for cards and selection exports; use the star button to promote another image, or the delete button to remove an image from the gallery. At least one image is required. Customers can swipe, use arrows, or tap thumbnails to see every image and zoom into details.Uploads validate file extension, magic bytes, decoded dimensions, and size. Images above 24 megapixels are rejected before full decode. Original tattoo and category image bytes are stored unmodified. New uploads generate proportional previews with a maximum edge of 480 pixels (without enlarging small images): JPEG at quality 78 for opaque images, or compressed PNG for transparent cutouts. Previews respect camera orientation. Catalog cards load only the preview; opening tattoo details or the full-screen viewer loads the original images. Every gallery image receives a preview so promoting another image to the cover stays fast. Categories store a separate thumbnail URL and use the optimized preview in customer cards, admin lists, and the editor. Existing categories fall back to their current image until re-uploaded. Apply 202610120003_category_thumbnails.sql before uploading category images. Existing uploads keep their current thumbnails; re-upload an image to generate the smaller preview. Failed/cancelled uploads attempt to remove newly staged objects. Replaced, previously saved images are intentionally retained; periodically review unreferenced objects in Storage before deleting them.
Extra images are stored in the ordered additional_images array, with image_url and optional thumbnail_url per image. Existing single-image tattoos need no changes. New uploads removed before saving or left in a cancelled editor are cleaned up; removing a previously saved image only removes its gallery reference and does not delete shared Storage objects.
Order delivery updates require supabase/migrations/202611020001_order_delivery_details.sql (after earlier migrations) before releasing the updated app. Orders now move from مؤكد to مرفوع للتوصيل, then packing and shipping. Staff record the governorate, area, nearest landmark and optional ملاحظات للمندوب; landmark and courier notes are included in copied booking details. Existing orders keep their addresses, with the new fields initially blank. Confirmed orders now queue a server-side Prime upload using the employee’s Prime account. A verified successful upload advances the order automatically; exhausted retries leave it confirmed. Apply 202611030001_prime_uploads.sql and 202611030002_prime_order_options.sql, deploy prime-upload and prime-locations, and configure its scheduler as described in Prime setup and recovery.
Staff can add tattoos with إضافة وشومات, remove unwanted tattoos, and save the revised selection with حفظ التعديلات while an order is pending, confirmed, uploaded, or packed. Adding/removing tattoos resets the final-total override to automatic calculation; re-enter any extra discount after revising the selection. Retained tattoos keep their saved names, codes, prices, and item discounts. Orders require 1–100 distinct tattoos, with 1–99 pieces each. Shipping locks the selection in both the editor and database. Apply supabase/migrations/202611020003_edit_order_tattoos.sql after earlier migrations before releasing this editor; verify it with supabase/tests/edit_order_tattoos.sql against a migrated test database.
flutter run -d chrome --dart-define-from-file=config.local.json
For a local Supabase backend:
npx supabase start -x realtime,imgproxy,edge-runtime,logflare,vector,supavisor,studio,mailpit,postgres-meta
npx supabase status
This project’s local ports are API 15421, PostgreSQL 15422, and shadow DB 15420, avoiding common Windows reserved ports. Copy the local API URL and public key into config.local.json. Local migrations run at first startup. Do not run db reset against a database containing important work. supabase/seed.sql is intentionally empty; test fixtures are kept out of production migrations.
flutter analyze
flutter test
node tool/share_bridge_test.cjs
flutter build web --release --dart-define-from-file=config.production.json --dart-define=USE_TEST_DATA=false
Upload only build/web to static hosting. Do not deploy source files or private configuration. Deploy over HTTPS for native sharing. Flutter’s initial runtime download is larger than an HTML-only site; the app keeps subsequent product requests paginated and uses thumbnails, lazy grid construction, browser image caching, and locally bundled Arabic typography.
Direct URLs must return the SPA entry point:
web/_redirects is copied to the build.deployment/nginx.conf and serve build/web.vercel.json declares the output and route rewrite. Build Flutter locally or in a CI environment with Flutter installed, then deploy the prebuilt output./index.html.Avoid caching index.html, flutter_bootstrap.js, and main.dart.js indefinitely. The supplied Nginx example forces revalidation. Browser installation is never required. The manifest is lightweight; no offline service-worker promise is made. Full offline catalog browsing is not guaranteed, but selected record snapshots persist and loaded data stays visible after pagination errors.
Local release preview, including deep links:
python tool/serve.py --port 8090 --directory build/web
[d] commitThe workflow in .github/workflows/deploy-pages.yml builds and deploys when the latest commit in a push to the repository’s default branch starts with [d], for example [d] update catalog. Other pushes show a skipped workflow. When pushing several commits together, only the latest commit’s message controls deployment. For squash merges, put the prefix in the final squash commit message.
USE_TEST_DATA=false). Add repository secrets named SUPABASE_URL and SUPABASE_ANON_KEY under Settings → Secrets and variables → Actions → Secrets. The build stops if either secret is missing. Use the public anon/publishable key; these settings are compiled into the public website, so never use a service-role key. Local configuration files and repository variables are not used by this workflow. Push a new deployment after changing secrets; an existing build will not pick up changes automatically.Commit and push to the default branch:
git commit -m "[d] deploy site"
git push
To deploy without changing files, use git commit --allow-empty -m "[d] deploy site", then push.
The workflow uses Flutter 3.47.4, detects the Pages base path (including repository subdirectories and custom domains), and uploads only build/web. The deployment URL appears in the Actions run’s github-pages environment. It also copies index.html to 404.html so direct links such as /admin load the Flutter router. GitHub Pages still returns HTTP 404 for these deep-link requests; navigation within the app works normally.
See GitHub’s custom Pages workflow documentation for repository setup and deployment permissions.
In Settings → Pages → Custom domain, enter getoge.com and save before updating DNS. At your DNS provider, set these records:
| Type | Name | Value |
|---|---|---|
| A | @ | 185.199.108.153 |
| A | @ | 185.199.109.153 |
| A | @ | 185.199.110.153 |
| A | @ | 185.199.111.153 |
| CNAME | www | YOUR_USERNAME.github.io |
Replace YOUR_USERNAME with the GitHub repository owner’s username. Once GitHub validates DNS and provisions the certificate, enable Enforce HTTPS. Push a new [d] commit after saving the custom domain so the workflow rebuilds for the domain’s root path.
This Actions deployment reads the domain from GitHub Pages settings; no repository CNAME file is required. See GitHub’s custom domain instructions.
Open صور للزبون in administration (/admin/images), apply catalog filters, then choose تجهيز صور النتائج للزبون. All matching visible tattoos are exported, including results beyond the first loaded page. These images include names, product codes, dimensions and unit prices in IQD, without an order code, order creation, quantities, or changes to the customer basket. Missing prices display السعر عند الاستفسار; zero remains an explicit price. Download the images and attach them to the customer’s conversation, or use مشاركة مع الزبون when native file sharing is available.
On HTTPS/localhost, exports persist in this browser’s Cache Storage and can be reused after reload. Each request refreshes all matching catalog records before comparing filters, result order, IDs, image URLs, names, codes, dimensions, prices and renderer version. A change generates new images. Admin uploads use new image URLs; replacing artwork externally at an unchanged URL requires cache invalidation. Storage keeps up to 12 exports within 32 MiB, evicting older entries. Clearing site data or browser eviction removes saved exports. If storage is unavailable/full, images can still be downloaded and the screen reports that an automatic copy was not saved. Native demo builds have session-only caching.
Apply 202610060001_share_source.sql along with all earlier pending migrations before using the new sharing screen with Supabase. The Instagram/WhatsApp selector saves the destination on the pending order using its private capability; it cannot edit customer details or confirm an order. Failed saves keep the previous destination selected. Manual orders continue to default to Instagram.
SelectionRenderer downloads each selected cover image as bytes, decodes it with Flutter, and draws it proportionally using BoxFit.contain into a 1200 × 1680 PNG. Six designs fit on each page; the last page expands for fewer designs, and one design receives a large single layout. Pages contain the brand, actual artwork, tattoo names/dimensions, page count, and Instagram handle. Full-resolution requests happen on details/export, not initial card loading.
Generation is sequential. Images are decoded to fit their export cells and kept alive until the page has been rendered and encoded as PNG, preventing browser-backed artwork from being released before iOS renders it. Each page’s images are then disposed, including on failure. If an image fails to load, generation fails visibly and the local selections remain. It never shares an incomplete grid while silently skipping an image. Availability and current product data are rechecked before generation; removed designs are reported and the customer can review before retrying.
The customer flow saves the generated selection images, then opens the selected Instagram or WhatsApp chat. It does not show the device’s app/recipient sharing picker. Each generated page is saved separately because mobile browsers may block batches of downloads. Only on iPhone browsers, Save opens a native HTML image view: press and hold the image, choose Save Image / Add to Photos, then tap حفظت الصورة، متابعة. Flutter’s canvas preview itself does not offer the browser’s image-saving menu. The page advances only after that explicit confirmation; closing the view or failing to load the image leaves the current page pending. In-app browsers that suppress image saving show guidance to open the site in Safari. Other browsers, including Android, iPad, and desktop, download the image directly without this view and advance after the request; a request cannot confirm that a file was actually saved. Once all pages have been confirmed/requested, the main button opens the selected chat, with a reminder to check the images and attach them manually. Customers can save an image again. web/share.js keeps image object URLs alive while the preview is open and revokes them when it closes.
Instagram opens https://ig.me/m/ge.to.ge; WhatsApp opens https://wa.me/9647778700244 (Iraq +964, omitting the local leading zero). These links do not attach files automatically. The customer attaches the saved images manually in that chat.
Supabase public Storage normally serves the CORS headers required for byte fetching. If using a custom CDN/proxy, it must also allow GET requests from your site origin and must not strip those headers. Test a real uploaded URL from the deployed site origin, then generate a selection. Do not work around CORS with an untrusted public proxy. All share image rendering uses fetched bytes, not tainted HTML image canvases.
lib/config.dart: Supabase settings, brand, currency formatting, Instagram/WhatsApp destinations, pagination, export limits.lib/models: typed catalog records and queries.lib/repositories: public/admin catalog queries with bounded product pages and explicit public availability filtering.lib/services/admin_service.dart: authentication, authorization checks, mutations, validated uploads, thumbnails, cleanup.lib/state: debounced/race-safe pagination and serialized local selection persistence.lib/services/selection_renderer.dart: portable Dart/Flutter image composition.lib/services/share: conditional web adapter; replace the stub with a mobile sharing adapter for future Android/iOS delivery.lib/services/analytics.dart: no-op, non-PII event boundary for future analytics.lib/ui/customer, lib/ui/admin, lib/ui/widgets: screens and reusable presentation.lib/app.dart: clean, directly loadable routes.No database queries are embedded in UI widgets. Bulk upload calls save_catalog_product with the required name_ar, code, width_cm, height_cm, category_ids, audiences, and body_placements, plus optional tags and gallery fields. Migrations backfill existing category assignments and default existing tattoos to both audiences, with no assumed body placement. Previously unnamed tattoos receive the neutral name وشم عشبي; replace it when editing. Existing missing dimensions are not invented and do not appear as size options until completed. Saved selections from older formats still load. SQL uniqueness is the final protection against concurrent duplicate product codes.
To replace the wordmark, bundle your logo asset in pubspec.yaml and set LOGO_ASSET in the configuration JSON. Both the UI and export renderer read it. The Arabic font is bundled with its OFL license in assets/fonts.
See docs/VERIFICATION.md for executed checks and remaining real-device/live deployment checks.
flutter analyze
flutter test
node tool/share_bridge_test.cjs
Against a disposable local Supabase backend, execute supabase/tests/catalog_security.sql as PostgreSQL administrator. It runs in a transaction and rolls back fixtures. It covers public filtering, anonymous writes, authenticated non-admin writes, privilege escalation, Storage policies, admin management, duplicate codes, and safe category deletion.
The tests’ synthetic rectangular artwork is only a fixture for measuring export proportions; it is not G2G inventory. Add your own actual tattoo artwork through the admin area.