2024-11-21
Od Mockupa do Produkcije za 3 Dana: PMFA Workflow Magic
Petak, 9:00 - Sastanak sa klijentom. Traži novi Customer Relationship Management (CRM) modul za B2B sales team.
Requirements:
- ✅ Kontakti (companies + people)
- ✅ Deals pipeline (stages: Prospect → Qualified → Proposal → Negotiation → Won/Lost)
- ✅ Email tracking
- ✅ Tasks & reminders
- ✅ Custom fields per industry (Real Estate, Manufacturing, IT Services)
- ✅ Reporting dashboard
Tradicionalni pristup: "Procena je 3 meseca development + 2 nedelje testiranja."
PMFA pristup: "Mogu imati beta verziju spremnu za ponedeljak ujutro."
Klijent: "Nemoguće."
Evo kako:
Dan 1 (Petak): Mockup + Metadata Definition
09:00-12:00 - Wireframes u Figma
Dizajner kreira basic wireframes:
- Contacts list page (tabela sa filterima)
- Contact detail page (info + deal history)
- Deal pipeline (Kanban board)
- Task list (todo items sa due dates)
Export: Figma → PDF + annotated screenshots.
13:00-17:00 - YAML Metadata Definition
Backend dev piše YAML entitete. Nema TypeScript koda. Samo metadata.
Contact Entity
# metadata/entities/contact.yml
entity: Contact
table: contacts
icon: "👤"
description: "B2B kontakti (companies & people)"
fields:
- name: id
type: uuid
primary: true
generated: auto
- name: type
type: enum
values: [company, person]
default: company
required: true
label: "Tip kontakta"
- name: companyName
type: string
maxLength: 200
required: true
searchable: true
label: "Naziv kompanije"
- name: industry
type: enum
values: [real_estate, manufacturing, it_services, finance, healthcare, other]
required: true
label: "Industrija"
- name: website
type: url
label: "Website"
- name: phone
type: phone
format: international
label: "Telefon"
- name: email
type: email
required: true
unique: true
label: "Email"
- name: address
type: object
fields:
- name: street
type: string
- name: city
type: string
- name: country
type: string
default: "Serbia"
- name: postalCode
type: string
- name: assignedTo
type: relation
target: User
required: true
label: "Assigned Sales Rep"
- name: deals
type: relation
target: Deal
relation: one-to-many
backRef: contact
- name: notes
type: text
label: "Internal Notes"
- name: tags
type: array
itemType: string
label: "Tags"
- name: customFields
type: jsonb
label: "Custom Industry Fields"
dynamic: true # Per-tenant custom fields
- name: createdAt
type: timestamp
generated: auto
- name: updatedAt
type: timestamp
generated: auto-update
permissions:
create: [sales_rep, sales_manager, admin]
read: [sales_rep, sales_manager, admin]
update: [assignedTo, sales_manager, admin]
delete: [admin]
ui:
listView:
columns: [companyName, industry, email, assignedTo, createdAt]
filters: [industry, assignedTo, createdAt]
sortBy: createdAt
sortOrder: desc
detailView:
layout: two-column
sections:
- title: "Basic Info"
fields: [companyName, type, industry, website]
- title: "Contact Details"
fields: [email, phone, address]
- title: "Assignment"
fields: [assignedTo, tags]
- title: "Related"
fields: [deals]
Trajanje: 30 minuta za jedan entitet.
Deal Entity
# metadata/entities/deal.yml
entity: Deal
table: deals
icon: "💼"
description: "Sales pipeline deals"
fields:
- name: id
type: uuid
primary: true
- name: title
type: string
maxLength: 200
required: true
searchable: true
- name: contact
type: relation
target: Contact
required: true
- name: stage
type: enum
values: [prospect, qualified, proposal, negotiation, won, lost]
default: prospect
required: true
indexed: true
- name: value
type: decimal
precision: 15
scale: 2
required: true
label: "Deal Value (EUR)"
- name: probability
type: integer
min: 0
max: 100
default: 10
label: "Win Probability (%)"
computed: |
CASE stage
WHEN 'prospect' THEN 10
WHEN 'qualified' THEN 25
WHEN 'proposal' THEN 50
WHEN 'negotiation' THEN 75
WHEN 'won' THEN 100
WHEN 'lost' THEN 0
END
- name: expectedCloseDate
type: date
required: true
- name: actualCloseDate
type: date
required: false
- name: lostReason
type: string
maxLength: 500
required: false
visibleIf: "stage == 'lost'"
- name: assignedTo
type: relation
target: User
required: true
- name: createdAt
type: timestamp
- name: updatedAt
type: timestamp
workflow:
states:
- name: prospect
label: "🔍 Prospect"
color: gray
transitions: [qualify, mark_lost]
- name: qualified
label: "✅ Qualified"
color: blue
transitions: [send_proposal, mark_lost]
- name: proposal
label: "📄 Proposal Sent"
color: purple
transitions: [start_negotiation, mark_lost]
- name: negotiation
label: "💬 Negotiating"
color: orange
transitions: [mark_won, mark_lost]
- name: won
label: "🎉 Won"
color: green
final: true
- name: lost
label: "❌ Lost"
color: red
final: true
transitions:
- name: qualify
from: prospect
to: qualified
action: "Qualify Lead"
- name: send_proposal
from: qualified
to: proposal
action: "Send Proposal"
notifications:
- target: assignedTo
message: "Proposal sent for deal: {{ title }}"
- name: start_negotiation
from: proposal
to: negotiation
action: "Start Negotiation"
- name: mark_won
from: negotiation
to: won
action: "Mark as Won"
requires_field: actualCloseDate
notifications:
- target: assignedTo
message: "🎉 Congratulations! Deal won: {{ title }}"
- name: mark_lost
from: [prospect, qualified, proposal, negotiation]
to: lost
action: "Mark as Lost"
requires_field: lostReason
permissions:
create: [sales_rep, sales_manager, admin]
read: [sales_rep, sales_manager, admin]
update: [assignedTo, sales_manager, admin]
delete: [admin]
ui:
listView:
defaultView: kanban
kanbanColumn: stage
columns: [title, contact, value, probability, expectedCloseDate, assignedTo]
filters: [stage, assignedTo, expectedCloseDate]
detailView:
layout: single-column
sections:
- title: "Deal Info"
fields: [title, contact, value, probability]
- title: "Timeline"
fields: [stage, expectedCloseDate, actualCloseDate]
- title: "Assignment"
fields: [assignedTo]
Trajanje: 45 minuta za kompleksan entitet sa workflow-om.
Task Entity
# metadata/entities/task.yml
entity: Task
table: tasks
icon: "✅"
description: "CRM tasks & reminders"
fields:
- name: id
type: uuid
primary: true
- name: title
type: string
maxLength: 200
required: true
- name: description
type: text
- name: relatedTo
type: polymorphic
targets: [Contact, Deal]
label: "Related To"
- name: dueDate
type: datetime
required: true
- name: completed
type: boolean
default: false
- name: priority
type: enum
values: [low, medium, high, urgent]
default: medium
- name: assignedTo
type: relation
target: User
required: true
- name: createdAt
type: timestamp
permissions:
create: [sales_rep, sales_manager, admin]
read: [assignedTo, admin]
update: [assignedTo, admin]
delete: [assignedTo, admin]
ui:
listView:
columns: [title, relatedTo, dueDate, priority, completed, assignedTo]
filters: [completed, priority, assignedTo, dueDate]
sortBy: dueDate
Trajanje: 20 minuta za simple entitet.
Total YAML Writing Time: ~2 sata
Definisali smo:
- 3 entiteta (Contact, Deal, Task)
- Workflow state machine
- Validations
- Permissions
- UI layout hints
Nema TypeScript koda. Nema database migration-a. Nema REST kontrolera.
Dan 2 (Subota): PMFA Code Generation + Frontend Scaffolding
09:00-10:00 - Generisanje Backend Koda
# PMFA CLI command
pmfa generate --entities=all --output=./backend
Generating entities...
✓ Contact (metadata/entities/contact.yml)
→ backend/src/entities/Contact.ts (TypeScript interface)
→ backend/src/db/migrations/001_create_contacts.sql
→ backend/src/api/controllers/ContactController.ts (REST API)
→ backend/src/api/routes/contacts.ts
✓ Deal (metadata/entities/deal.yml)
→ backend/src/entities/Deal.ts
→ backend/src/db/migrations/002_create_deals.sql
→ backend/src/api/controllers/DealController.ts
→ backend/src/workflows/DealWorkflow.ts (state machine)
✓ Task (metadata/entities/task.yml)
→ backend/src/entities/Task.ts
→ backend/src/db/migrations/003_create_tasks.sql
→ backend/src/api/controllers/TaskController.ts
Generated:
- 12 TypeScript files
- 3 SQL migration files
- 15 REST API endpoints
- 1 workflow state machine
Total time: 12 seconds
PMFA automatski generiše:
✅ TypeScript interfejsi
✅ PostgreSQL tabele, indeksi, foreign keys
✅ REST API endpoints (GET /contacts, POST /deals, PATCH /deals/:id/qualify, etc.)
✅ Workflow state machine za Deal entity
✅ Permission checks
✅ Validation rules
10:00-12:00 - Database Setup + API Testing
# Pokreni migracije
pmfa migrate --up
Running migrations...
✓ 001_create_contacts.sql
✓ 002_create_deals.sql
✓ 003_create_tasks.sql
# Pokreni dev server
pmfa serve --port=3000
PMFA server running on http://localhost:3000
API endpoints:
GET /api/contacts
POST /api/contacts
GET /api/contacts/:id
PATCH /api/contacts/:id
DELETE /api/contacts/:id
GET /api/deals
POST /api/deals
POST /api/deals/:id/qualify
POST /api/deals/:id/send_proposal
POST /api/deals/:id/mark_won
...
API testing sa Postman:
# Kreiranje kontakta
POST http://localhost:3000/api/contacts
{
"companyName": "Acme Corp",
"type": "company",
"industry": "manufacturing",
"email": "contact@acme.com",
"assignedTo": "user-uuid-123"
}
# Response: 201 Created
{
"id": "contact-uuid-456",
"companyName": "Acme Corp",
...
}
✅ Sve radi out-of-the-box.
13:00-17:00 - Frontend UI Scaffolding
PMFA generiše React komponente iz metadata:
pmfa generate:ui --framework=react --output=./frontend
Generating React components...
✓ ContactListView.tsx (tabela sa filterima)
✓ ContactDetailView.tsx (form sa validacijama)
✓ DealKanbanView.tsx (kanban board)
✓ TaskListView.tsx (todo lista)
Generated:
- 12 React komponenti
- 8 custom hooks (useContacts, useDeals, useTasks...)
- Form validation schemas (Zod)
- API client functions (React Query)
Rezultat:
// frontend/src/pages/ContactsPage.tsx (auto-generated)
import { useContacts } from '../hooks/useContacts';
import { ContactListView } from '../components/ContactListView';
export function ContactsPage() {
const { data: contacts, isLoading, filters, setFilters } = useContacts();
return (
<ContactListView
contacts={contacts}
isLoading={isLoading}
filters={filters}
onFilterChange={setFilters}
columns={['companyName', 'industry', 'email', 'assignedTo', 'createdAt']}
/>
);
}
100% type-safe. Zero ručni kod.
Dan 3 (Nedelja): Custom Logic + Industry-Specific Fields
09:00-12:00 - Custom Fields Per Industry
Klijent traži custom fields:
- Real Estate:
property_type,square_meters,location - Manufacturing:
annual_production,certifications - IT Services:
tech_stack,team_size
PMFA pristup:
# metadata/custom-fields/real-estate.yml
entity: Contact
condition: "industry == 'real_estate'"
customFields:
- name: propertyType
type: enum
values: [residential, commercial, industrial, land]
label: "Property Type"
- name: squareMeters
type: integer
label: "Square Meters"
- name: location
type: object
fields:
- name: address
type: string
- name: latitude
type: decimal
- name: longitude
type: decimal
PMFA dinamički dodaje ova polja u:
- ✅ Database schema (JSONB kolona ili separate tabele)
- ✅ TypeScript tipovi
- ✅ Frontend forme
- ✅ Validacije
Trajanje: 30 minuta za 3 industrije.
13:00-15:00 - Custom Business Logic
Klijent traži: "Kada deal bude mark as won, automatski kreiraj task za onboarding."
PMFA Hook System:
# metadata/hooks/deal-won-onboarding.yml
entity: Deal
trigger: after_transition
transition: mark_won
action: create_task
params:
title: "Onboarding: {{ deal.contact.companyName }}"
description: "Schedule onboarding call for new client"
relatedTo: "{{ deal.contact.id }}"
assignedTo: "{{ deal.assignedTo }}"
dueDate: "{{ now() + 3days }}"
priority: high
Alternative (TypeScript za kompleksnu logiku):
// backend/src/hooks/dealWonOnboarding.ts
import { Hook } from '@pmfa/core';
export const dealWonOnboarding: Hook = {
entity: 'Deal',
trigger: 'after_transition',
transition: 'mark_won',
async execute(ctx) {
const { deal } = ctx.data;
await ctx.tasks.create({
title: `Onboarding: ${deal.contact.companyName}`,
description: `Schedule onboarding call for new client`,
relatedTo: deal.contact.id,
assignedTo: deal.assignedTo,
dueDate: addDays(new Date(), 3),
priority: 'high'
});
// Pošalji Slack notifikaciju
await ctx.slack.sendMessage({
channel: '#sales',
text: `🎉 New deal won: ${deal.title} (${deal.value} EUR)`
});
}
};
Trajanje: 1 sat za custom logic.
16:00-17:00 - Testing & Bug Fixes
- Testiranje UI flow-ova
- Fixing edge cases
- Performance tuning (query optimizacija)
Ponedeljak, 09:00 - Demo Sa Klijentom
Rezultat:
✅ Pun CRM modul sa Contacts, Deals, Tasks
✅ Kanban board za deal pipeline
✅ Custom fields per industry
✅ Workflow automation (won deal → onboarding task)
✅ Email notifikacije
✅ Role-based permissions
✅ Mobile-responsive UI
Klijent: "Ovo je gotovo?! Za 3 dana??"
Mi: "Da. Ovo je metadata-driven development."
Traditional vs PMFA: Breakdown
| Task | Traditional | PMFA |
|---|---|---|
| Backend Development | 6 nedelja | 2 sata (YAML metadata) |
| Database Schema | 1 nedelja (migrations) | 10 sekundi (auto-generated) |
| REST API | 3 nedelje (controllers, validations) | 10 sekundi (auto-generated) |
| Frontend UI | 4 nedelje (React komponente) | 3 sata (scaffolding + customizacija) |
| Workflow Logic | 2 nedelje (state machine, notifications) | 30 minuta (YAML workflow) |
| Custom Fields | 1 nedelja (database, backend, frontend) | 30 minuta (YAML custom fields) |
| Testing | 2 nedelje | 1 dan (samo integration testing) |
| TOTAL | 3 meseca | 3 dana |
Ušteda: 90% vremena.
Conclusion
Metadata-first development = 30x brži delivery.
Umesto da pišemo:
- TypeScript modele
- SQL migracije
- REST kontrolere
- React forme
- Validacije
- Permissions checks
Mi pišemo YAML metadata, a PMFA automatski generiše sve ostalo.
Rezultat:
- ✅ 3 dana development umesto 3 meseca
- ✅ Zero tehnički dug
- ✅ Perfect type safety
- ✅ Instant izmene (samo edit YAML, regenerate)
PMFA omogućava shipping production-ready software za dane, ne mesece.
Želiš da probaš? Kontaktirajte nas na office@nonnotech.com za early access program.