Modifying Status

Now that customers can create and view ice cream orders, let's add functionality for shop staff to manage the order lifecycle. In a real ice cream shop, orders need to progress through different stages: from "active" (newly placed) to "processing" (being prepared) to "served" (completed) or "canceled" if needed.
Think of status modification like the workflow in a real ice cream shop. When a customer places an order, it starts as "active" - like a ticket on the order board. Then staff begins preparation ("processing"), and finally serves it to the customer ("served"). This tutorial shows you how to implement this natural workflow with proper validation and user-friendly controls.
Before implementing the functionality, let's understand the business logic behind ice cream order status transitions:
🟢Active → Processing
When a customer places an order, it starts as "active". Staff can begin processing it by clicking "Process".
🔵Processing → Served
While an order is being prepared, it's in "processing" status. Once ready, staff can mark it as "served".
Active → Canceled
Only active orders can be canceled. Once processing begins, cancellation is no longer allowed.
⚠️Business Rules
  • Only active orders can be processed or canceled
  • Only processing orders can be served
  • Served and canceled orders are final states

Implement Document Business Logic

Just like a real ice cream shop has rules about when orders can be processed or canceled, we need to implement these business rules in our code. The document layer is where we define these rules - think of it as the "shop policies" that ensure orders are handled correctly no matter who is working or how busy it gets.
1// File: apps/koyo/lib/icecreamOrder/icecreamOrder.document.ts
2import { beyond, by, Database, into, type SchemaOf } from "@akanjs/document";
3
4import * as cnst from "../cnst";
5import { Revert } from "../dict";
6
7@Database.Input(() => cnst.IcecreamOrderInput)
8export class IcecreamOrderInput extends by(cnst.IcecreamOrderInput) {}
9
10@Database.Document(() => cnst.IcecreamOrder)
11export class IcecreamOrder extends by(cnst.IcecreamOrder) {
12  // Business method to start processing an order
13  process() {
14    if (this.status !== "active") {
15      throw new Revert("icecreamOrder.onlyActiveCanBeProcessed");
16    }
17    this.status = "processing";
18    return this;
19  }
20  
21  // Business method to mark an order as served
22  serve() {
23    if (this.status !== "processing") {
24      throw new Revert("icecreamOrder.onlyProcessingCanBeServed");
25    }
26    this.status = "served";
27    return this;
28  }
29  
30  // Business method to cancel an order
31  cancel() {
32    if (this.status !== "active") {
33      throw new Revert("icecreamOrder.onlyActiveCanBeCanceled");
34    }
35    this.status = "canceled";
36    return this;
37  }
38}
39// ...other code
These methods implement our business rules directly in the data model:
🔄
process(): Checks if status is 'active' before changing to 'processing'
serve(): Validates that status is 'processing' before marking as 'served'
cancel(): Ensures only 'active' orders can be canceled
When validation fails, we throw a Revert with a dictionary key for user-friendly error messages. Let's add these error messages to our dictionary:
1// File: apps/koyo/lib/icecreamOrder/icecreamOrder.dictionary.ts
2// ...other code
3const modelDictionary = {
4  // ...other code
5
6  // Error messages for business rule violations
7  onlyActiveCanBeProcessed: [
8    "Only active orders can be processed", 
9    "활성 상태의 주문만 처리할 수 있습니다"
10  ],
11  onlyProcessingCanBeServed: [
12    "Only processing orders can be served", 
13    "처리중인 주문만 서빙할 수 있습니다"
14  ],
15  onlyActiveCanBeCanceled: [
16    "Only active orders can be canceled", 
17    "활성 상태의 주문만 취소할 수 있습니다"
18  ],
19  // * ==================== Etc ==================== * //
20} satisfies ModelDictionary<IcecreamOrder, IcecreamOrderInsight, IcecreamOrderFilter>;
21// ...other code

Implement Service Layer

Now we need service methods that act like the shop manager - coordinating between the business rules (document layer) and the actual data storage (database). When a staff member wants to process an order, the service layer fetches the order, applies the business rules, and saves the changes safely.
1// File: apps/koyo/lib/icecreamOrder/icecreamOrder.service.ts
2import { DbService, Service } from "@akanjs/service";
3
4import * as cnst from "../cnst";
5import * as db from "../db";
6
7@Service("IcecreamOrderService")
8export class IcecreamOrderService extends DbService(db.icecreamOrderDb) {
9  
10  // Service method to process an ice cream order
11  async processIcecreamOrder(icecreamOrderId: string) {
12    const icecreamOrder = await this.getIcecreamOrder(icecreamOrderId);
13    return await icecreamOrder.process().save();
14  }
15  
16  // Service method to serve an ice cream order
17  async serveIcecreamOrder(icecreamOrderId: string) {
18    const icecreamOrder = await this.getIcecreamOrder(icecreamOrderId);
19    return await icecreamOrder.serve().save();
20  }
21  
22  // Service method to cancel an ice cream order
23  async cancelIcecreamOrder(icecreamOrderId: string) {
24    const icecreamOrder = await this.getIcecreamOrder(icecreamOrderId);
25    return await icecreamOrder.cancel().save();
26  }
27}
Each service method follows the same pattern:
1️⃣
Fetch: Retrieve the order from database using getIcecreamOrder()
2️⃣
Execute: Call the business logic method (process(), serve(), or cancel())
3️⃣
Save: Persist the changes to database with save()
This pattern ensures that business rules are enforced at the document level while the service handles database transactions safely.

Create Signal Endpoints

Think of signal endpoints as the communication system between the frontend (like the shop's order display screen) and the backend (the kitchen and management system). When staff clicks a "Process" button on the screen, it needs to communicate with the backend to actually update the order. Akan.js automatically creates both REST and GraphQL versions of these endpoints, so different parts of your system can communicate however they prefer.
1// File: apps/koyo/lib/icecreamOrder/icecreamOrder.signal.ts
2import { ID, Int } from "@akanjs/base";
3import { SortOf } from "@akanjs/constant";
4import { Arg, DbSignal, Mutation, Query, resolve, Signal } from "@akanjs/signal";
5
6import * as cnst from "../cnst";
7
8@Signal(() => cnst.IcecreamOrder)
9export class IcecreamOrderSignal extends DbSignal(cnst.icecreamOrderCnst, cnst.Srvs, {
10  guards: { get: Query.Public, cru: Mutation.Public },
11}) {
12  // ...existing code for CRUD operations...
13
14  // Signal endpoint to process an ice cream order
15  @Mutation.Public(() => cnst.IcecreamOrder)
16  async processIcecreamOrder(@Arg.Param("icecreamOrderId", () => ID) icecreamOrderId: string) {
17    const icecreamOrder = await this.icecreamOrderService.processIcecreamOrder(icecreamOrderId);
18    return resolve<cnst.IcecreamOrder>(icecreamOrder);
19  }
20  
21  // Signal endpoint to serve an ice cream order
22  @Mutation.Public(() => cnst.IcecreamOrder)
23  async serveIcecreamOrder(@Arg.Param("icecreamOrderId", () => ID) icecreamOrderId: string) {
24    const icecreamOrder = await this.icecreamOrderService.serveIcecreamOrder(icecreamOrderId);
25    return resolve<cnst.IcecreamOrder>(icecreamOrder);
26  }
27  
28  // Signal endpoint to cancel an ice cream order
29  @Mutation.Public(() => cnst.IcecreamOrder)
30  async cancelIcecreamOrder(@Arg.Param("icecreamOrderId", () => ID) icecreamOrderId: string) {
31    const icecreamOrder = await this.icecreamOrderService.cancelIcecreamOrder(icecreamOrderId);
32    return resolve<cnst.IcecreamOrder>(icecreamOrder);
33  }
34}
Each signal endpoint is decorated with @Mutation.Public() and takes the order ID as a parameter. The resolve() function ensures the returned data is properly formatted for both API formats.
We also need to add dictionary entries for these API endpoints so they display properly in the UI:
1// File: apps/koyo/lib/icecreamOrder/icecreamOrder.dictionary.ts
2// ...other code
3const signalDictionary = {
4  ...getBaseSignalTrans("icecreamOrder" as const),
5  // * ==================== Endpoint ==================== * //
6  // ...existing endpoints...
7
8  // API endpoint translations for status operations
9  "api-processIcecreamOrder": ["Process", "작업시작"],
10  "apidesc-processIcecreamOrder": ["Start processing an ice cream order", "아이스크림 주문 처리를 시작합니다"],
11  "arg-processIcecreamOrder-icecreamOrderId": ["Order ID", "주문 ID"],
12  "argdesc-processIcecreamOrder-icecreamOrderId": ["ID of the order to process", "처리할 주문의 ID"],
13
14  "api-serveIcecreamOrder": ["Serve", "서빙완료"],
15  "apidesc-serveIcecreamOrder": ["Mark an ice cream order as served", "아이스크림 주문을 서빙완료로 표시합니다"],
16  "arg-serveIcecreamOrder-icecreamOrderId": ["Order ID", "주문 ID"],
17  "argdesc-serveIcecreamOrder-icecreamOrderId": ["ID of the order to serve", "서빙할 주문의 ID"],
18
19  "api-cancelIcecreamOrder": ["Cancel", "주문취소"],
20  "apidesc-cancelIcecreamOrder": ["Cancel an ice cream order", "아이스크림 주문을 취소합니다"],
21  "arg-cancelIcecreamOrder-icecreamOrderId": ["Order ID", "주문 ID"],
22  "argdesc-cancelIcecreamOrder-icecreamOrderId": ["ID of the order to cancel", "취소할 주문의 ID"],
23  // * ==================== Endpoint ==================== * //
24} satisfies SignalDictionary<IcecreamOrderSignal, IcecreamOrder>;

Create Frontend Store Actions

The store layer is like the control panel that staff actually interact with. It takes the complex API communications and turns them into simple actions like "processOrder()" that components can easily use. When a button is clicked, the store handles calling the API and updating the display automatically - just like how pressing a button on a shop's POS system updates both the backend and the screen.
1// File: apps/koyo/lib/icecreamOrder/icecreamOrder.store.ts
2import { stateOf, Store } from "@akanjs/store";
3
4import * as cnst from "../cnst";
5import { fetch } from "../sig";
6
7@Store(() => cnst.IcecreamOrder)
8export class IcecreamOrderStore extends stateOf(fetch.icecreamOrderGql, {
9  // state
10}) {
11  
12  // Store action to process an ice cream order
13  async processIcecreamOrder(icecreamOrderId: string) {
14    const icecreamOrder = await fetch.processIcecreamOrder(icecreamOrderId);
15    this.setIcecreamOrder(icecreamOrder);
16  }
17  
18  // Store action to serve an ice cream order
19  async serveIcecreamOrder(icecreamOrderId: string) {
20    const icecreamOrder = await fetch.serveIcecreamOrder(icecreamOrderId);
21    this.setIcecreamOrder(icecreamOrder);
22  }
23  
24  // Store action to cancel an ice cream order
25  async cancelIcecreamOrder(icecreamOrderId: string) {
26    const icecreamOrder = await fetch.cancelIcecreamOrder(icecreamOrderId);
27    this.setIcecreamOrder(icecreamOrder);
28  }
29}
Each store action follows this pattern:
📡
API Call: Make an API request to signal endpoints through fetch methods
🔄
State Update: Update the local store state with the new order data using setIcecreamOrder()
This ensures that when status changes happen, the UI automatically reflects the updated state without requiring a page refresh.

Create Utility Components

Just like a real ice cream shop might have labeled buttons or stamps for different order stages, we'll create reusable button components for each action. These "digital buttons" can be placed anywhere in our interface - on order cards, in detailed views, or on staff dashboards. By creating them once as utility components, we ensure consistent behavior and styling throughout the entire application.
1// File: apps/koyo/lib/icecreamOrder/IcecreamOrder.Util.tsx
2"use client";
3import { clsx } from "@akanjs/client";
4import { cnst, IcecreamOrder, st, usePage } from "@koyo/client";
5import { Model } from "@akanjs/ui";
6
7// ...existing ViewWrapper component...
8
9// Process button component
10interface ProcessProps {
11  className?: string;
12  icecreamOrderId: string;
13  disabled?: boolean;
14}
15export const Process = ({ className, icecreamOrderId, disabled }: ProcessProps) => {
16  const { l } = usePage();
17  return (
18    <button
19      className={clsx("btn btn-secondary", className)}
20      disabled={disabled}
21      onClick={() => {
22        void st.do.processIcecreamOrder(icecreamOrderId);
23      }}
24    >
25      {l("icecreamOrder.api-processIcecreamOrder")}
26    </button>
27  );
28};
29
30// Serve button component
31interface ServeProps {
32  className?: string;
33  icecreamOrderId: string;
34  disabled?: boolean;
35}
36export const Serve = ({ className, icecreamOrderId, disabled }: ServeProps) => {
37  const { l } = usePage();
38  return (
39    <button
40      className={clsx("btn btn-accent", className)}
41      disabled={disabled}
42      onClick={() => {
43        void st.do.serveIcecreamOrder(icecreamOrderId);
44      }}
45    >
46      {l("icecreamOrder.api-serveIcecreamOrder")}
47    </button>
48  );
49};
50
51// Cancel button component
52interface CancelProps {
53  className?: string;
54  icecreamOrderId: string;
55  disabled?: boolean;
56}
57export const Cancel = ({ className, icecreamOrderId, disabled }: CancelProps) => {
58  const { l } = usePage();
59  return (
60    <button
61      className={clsx("btn btn-warning", className)}
62      disabled={disabled}
63      onClick={() => {
64        void st.do.cancelIcecreamOrder(icecreamOrderId);
65      }}
66    >
67      {l("icecreamOrder.api-cancelIcecreamOrder")}
68    </button>
69  );
70};
Each button component includes:
🎨Consistent Styling
Each button has appropriate styling: primary for Process, secondary for Serve, outlined warning for Cancel
🔒Disabled State
Buttons can be disabled when actions aren't allowed based on current status
🌍Internationalization
Button labels come from dictionary entries for proper multilingual support

Apply To Unit & View Components

Now comes the exciting part - putting all the pieces together! Just like adding action buttons to the order tickets in a real shop, we'll integrate our status management buttons directly into the order cards and detailed views. This means staff won't need to navigate to separate pages or menus - they can process orders right from wherever they're viewing them, making the workflow fast and intuitive.
Let's update the Unit component to include status management buttons:
1// File: apps/koyo/lib/icecreamOrder/IcecreamOrder.Unit.tsx
2import { clsx, ModelProps } from "@akanjs/client";
3import { cnst, IcecreamOrder, usePage } from "@koyo/client";
4
5export const Card = ({ icecreamOrder }: ModelProps<"icecreamOrder", cnst.LightIcecreamOrder>) => {
6  const { l } = usePage();
7  return (
8    <div className="group flex h-auto w-full flex-col overflow-hidden rounded-xl bg-gradient-to-br from-pink-100 via-yellow-50 to-pink-200 p-6 shadow-md transition-all duration-300 hover:shadow-xl">
9      {/* Order information section */}
10      <div className="flex w-full flex-col justify-center space-y-4">
11        <div className="flex items-center gap-2 text-lg font-semibold text-pink-700">
12          <span className="inline-block rounded bg-pink-200 px-2 py-1 text-xs font-bold tracking-wider uppercase">
13            {l.field("icecreamOrder", "id")}
14          </span>
15          <span className="ml-2 font-mono text-pink-900">{icecreamOrder.id}</span>
16        </div>
17        <div className="flex items-center gap-2">
18          <span className="inline-block rounded bg-yellow-200 px-2 py-1 text-xs font-bold tracking-wider text-yellow-800 uppercase">
19            {l.field("icecreamOrder", "status")}
20          </span>
21          <span
22            className={clsx("ml-2 rounded-full px-3 py-1 text-sm font-semibold", {
23              "bg-green-100 text-green-700": icecreamOrder.status === "active",
24              "bg-blue-100 text-blue-700": icecreamOrder.status === "processing",
25              "bg-red-100 text-red-700": icecreamOrder.status === "served",
26              "bg-gray-100 text-gray-700": icecreamOrder.status === "canceled",
27            })}
28          >
29            {l.enum("icecreamOrder", "status", icecreamOrder.status)}
30          </span>
31        </div>
32      </div>
33      
34      {/* Action buttons section */}
35      <div className="bg-base-100/50 flex items-center justify-center gap-2 rounded-xl p-4">
36        <Model.ViewWrapper sliceName="icecreamOrder" modelId={icecreamOrder.id}>
37          <button className="btn btn-primary btn-xl">
38            <span>{l.trans({ en: "View", ko: "보기" })}</span>
39          </button>
40        </Model.ViewWrapper>
41        <IcecreamOrder.Util.Process icecreamOrderId={icecreamOrder.id} disabled={icecreamOrder.status !== "active"} />
42        <IcecreamOrder.Util.Serve icecreamOrderId={icecreamOrder.id} disabled={icecreamOrder.status !== "processing"} />
43        <IcecreamOrder.Util.Cancel icecreamOrderId={icecreamOrder.id} disabled={icecreamOrder.status !== "active"} />
44      </div>
45    </div>
46  );
47};
Now let's also add the buttons to the detailed view modal:
1// File: apps/koyo/lib/icecreamOrder/IcecreamOrder.View.tsx
2import { clsx } from "@akanjs/client";
3import { cnst, IcecreamOrder, usePage } from "@koyo/client";
4
5interface IcecreamOrderViewProps {
6  className?: string;
7  icecreamOrder: cnst.IcecreamOrder;
8}
9
10export const General = ({ className, icecreamOrder }: IcecreamOrderViewProps) => {
11  const { l } = usePage();
12  return (
13    <div className={clsx(className, "mx-auto w-full space-y-6 rounded-xl p-8 shadow-lg")}>
14      {/* Header section */}
15      <div className="flex items-center gap-3 border-b pb-4">
16        <span className="text-3xl font-extrabold text-pink-600">🍦</span>
17        <span className="text-2xl font-bold">{l("icecreamOrder.modelName")}</span>
18        <span className="ml-auto text-xs text-base-content/50">#{icecreamOrder.id}</span>
19      </div>
20      
21      {/* Order details grid - same as before */}
22      <div className="grid grid-cols-2 gap-x-6 gap-y-4">
23        {/* ... existing field displays ... */}
24      </div>
25      
26      {/* Action buttons section */}
27      <div className="flex items-center justify-end gap-2">
28        <IcecreamOrder.Util.Process icecreamOrderId={icecreamOrder.id} disabled={icecreamOrder.status !== "active"} />
29        <IcecreamOrder.Util.Serve icecreamOrderId={icecreamOrder.id} disabled={icecreamOrder.status !== "processing"} />
30        <IcecreamOrder.Util.Cancel icecreamOrderId={icecreamOrder.id} disabled={icecreamOrder.status !== "active"} />
31      </div>
32    </div>
33  );
34};
Key features of this implementation:
Smart Disabling: Buttons are disabled when actions aren't allowed based on current status
📱
Responsive Layout: Buttons wrap gracefully on smaller screens with flex-wrap
🎨
Visual Hierarchy: Different button styles indicate action priority and type

Test Status Management

Let's test our status management implementation to ensure everything works correctly:
Testing Steps:
  1. Navigate to http://localhost:4201/icecreamOrder
  2. Create a new ice cream order (it will start as 'active')
  3. Notice that only 'Process' and 'Cancel' buttons are enabled
  4. Click 'Process' - the status should change to 'processing'
  5. Now only the 'Serve' button should be enabled
  6. Click 'Serve' - the status should change to 'served'
  7. All action buttons should now be disabled (final state)
Expected Behavior:
  • Status changes should be instant and visible
  • Button states should update automatically
  • Invalid actions should be prevented
  • Error messages should appear if business rules are violated

Status Management Best Practices

Here are important best practices for implementing status management in Akan.js:
🛡️Enforce Business Rules
Always validate state transitions at the document level using business methods. This ensures data integrity regardless of how the API is called.
💡Smart UI Controls
Disable buttons and hide actions that aren't valid for the current state. This provides immediate feedback to users about what actions are possible.
🔄Consistent Patterns
Follow the same pattern across all status operations: Document → Service → Signal → Store → Component. This makes your code predictable and maintainable.
📝Proper Error Handling
Use dictionary-based error messages with Revert exceptions. This ensures error messages are properly translated and user-friendly.

What's Next?

Excellent work! You've successfully implemented a complete status management system for your ice cream orders. Shop staff can now efficiently manage the order lifecycle with proper business rule enforcement.
🎉 What You've Accomplished:
  • Implemented business logic with validation
  • Created service layer for status operations
  • Built signal endpoints for status changes
  • Added frontend store actions
  • Created reusable utility components
  • Integrated smart UI controls
In the next tutorial, we'll learn how to edit existing data by implementing order modification functionality. This will allow customers to update their ice cream orders before they're processed, completing the full CRUD operations for our ice cream shop.
Released under the MIT License
Official Akan.js Consulting onAkansoft
Copyright © 2025 Akan.js. All rights reserved.
System managed bybassman