npm package: @kodeme-io/next-core-patterns
Description: Reusable business patterns for Next.js applications
Author: ABC Food
Latest version: 0.8.4

# @kodeme-io/next-core-patterns

Reusable business patterns for Next.js applications - offline-first, type-safe, and framework-agnostic.

## Installation

```bash
npm install @kodeme-io/next-core-patterns
# or
pnpm add @kodeme-io/next-core-patterns
```

## Patterns

### OfflineSyncQueue

Generic offline-first sync queue with automatic retry and persistence.

```typescript
import { OfflineSyncQueue } from '@kodeme-io/next-core-patterns'

// Define your sync handler
const syncHandler = async (item) => {
  await fetch(`/api/${item.type}`, {
    method: 'POST',
    body: JSON.stringify(item.data)
  })
}

// Create queue instance
const queue = new OfflineSyncQueue({
  storageKey: 'my-sync-queue',
  syncHandler,
  maxRetries: 3
})

// Add items to queue
queue.enqueue({
  type: 'photo',
  data: { url: '...', metadata: {} }
})

// Subscribe to changes
queue.subscribe((items) => {
  console.log('Queue updated:', items.length, 'pending')
})
```

### PricingCalculator

Offline-first pricing engine with tax calculations.

```typescript
import { PricingCalculator } from '@kodeme-io/next-core-patterns'

const calculator = new PricingCalculator()

// Load price data
await calculator.loadPriceData({
  priceList: [
    { productId: 1, price: 100 },
    { productId: 2, price: 200 }
  ],
  taxes: [
    { id: 1, name: 'VAT', amount: 10, type: 'percent' }
  ]
})

// Calculate line item
const line = calculator.calculateLine({
  productId: 1,
  quantity: 5,
  taxIds: [1]
})
// Result: { subtotal: 500, tax: 50, total: 550 }

// Calculate order
const order = calculator.calculateOrder([
  { productId: 1, quantity: 5, taxIds: [1] },
  { productId: 2, quantity: 2, taxIds: [1] }
])
// Result: { subtotal: 900, tax: 90, total: 990 }
```

### ScheduleUtils

Date and schedule calculation utilities - 25+ functions for common business date operations.

```typescript
import {
  getCurrentWeekNumber,
  isOddWeek,
  addBusinessDays,
  getDateRange,
  formatRelativeDate,
  getBusinessDaysBetween,
  getWeekStart,
  getMonthStart,
  getQuarterStart
} from '@kodeme-io/next-core-patterns'

// ISO week calculations
const week = getCurrentWeekNumber() // 42
const isOdd = isOddWeek(week) // true

// Business days (skip weekends)
const deliveryDate = addBusinessDays(new Date(), 5)
const workDays = getBusinessDaysBetween(startDate, endDate)

// Date ranges
const nextWeek = getDateRange(new Date(), 7)
const weekStart = getWeekStart(new Date())
const monthStart = getMonthStart()

// Relative dates
const display = formatRelativeDate(date) // "Today", "Tomorrow", or day name

// Period calculations
const q1Start = getQuarterStart()
const yearEnd = getYearEnd()
```

**Available Functions:**
- Week: `getCurrentWeekNumber`, `isOddWeek`, `isCurrentWeekOdd`, `getWeekStart`, `getWeekEnd`
- Business Days: `addBusinessDays`, `getBusinessDaysBetween`, `isBusinessDay`, `isWeekend`
- Date Ranges: `getDateRange`, `getDaysBetween`, `isSameDay`
- Formatting: `formatRelativeDate`
- Next Occurrence: `getNextDayOfWeek`
- Period Boundaries: `getMonthStart`, `getMonthEnd`, `getQuarterStart`, `getQuarterEnd`, `getYearStart`, `getYearEnd`

### GeofenceUtils

Location-based validation and GPS distance calculations using the Haversine formula.

```typescript
import {
  calculateDistance,
  validateGeofence,
  formatDistance,
  hasValidGPSCoordinates,
  getCenterPoint,
  getBoundingBox
} from '@kodeme-io/next-core-patterns'

// Calculate distance between two GPS coordinates
const distance = calculateDistance(
  -6.2088, 106.8456,  // Jakarta
  -6.9175, 107.6191   // Bandung
)
// Returns: ~120000 (120km in meters)

// Validate geofence (e.g., check-in at customer location)
const result = validateGeofence(
  userLat, userLon,      // User's current location
  customerLat, customerLon, // Customer location
  100                     // 100m radius
)

if (result.isWithinGeofence) {
  console.log(`✅ Within range (${result.distance}m away)`)
} else {
  console.log(`❌ Too far: ${formatDistance(result.distance)}`)
}

// Validate GPS coordinates
if (hasValidGPSCoordinates(lat, lon)) {
  // Coordinates are valid
}

// Get center point of multiple locations
const center = getCenterPoint([
  { lat: -6.2088, lon: 106.8456 },
  { lat: -6.9175, lon: 107.6191 }
])

// Get bounding box for map viewport
const bounds = getBoundingBox(locations)
map.fitBounds([[bounds.minLat, bounds.minLon], [bounds.maxLat, bounds.maxLon]])
```

**Available Functions:**
- `calculateDistance` - Haversine distance calculation
- `validateGeofence` - Check if within radius
- `formatDistance` - Format meters/kilometers
- `hasValidGPSCoordinates` - Validate lat/lon
- `getCenterPoint` - Center of multiple points
- `getBoundingBox` - Min/max lat/lon bounds
- `isWithinBounds` - Check if point in bounds

### MediaUtils

Image compression and storage management for PWA applications.

```typescript
import {
  compressPhoto,
  checkStorageQuota,
  fileToBase64,
  base64ToBlob,
  batchCompressPhotos
} from '@kodeme-io/next-core-patterns'

// Compress photo from camera
const result = await compressPhoto(photoDataUrl, {
  maxSizeKB: 500,    // Target 500KB
  maxWidth: 1920,    // Max 1920px width
  maxHeight: 1080,   // Max 1080px height
  format: 'image/jpeg'
})

console.log(`Compressed: ${result.originalSizeKB}KB → ${result.compressedSizeKB}KB`)
console.log(`Compression ratio: ${(result.compressionRatio * 100).toFixed(1)}%`)

// Check storage quota
const quota = await checkStorageQuota()
if (quota.percentUsed > 90) {
  alert(`Storage almost full: ${quota.usedMB.toFixed(0)}MB / ${quota.availableMB.toFixed(0)}MB`)
}

// Convert file to base64
const file = event.target.files[0]
const dataUrl = await fileToBase64(file)
const compressed = await compressPhoto(dataUrl)

// Convert back to blob for upload
const blob = base64ToBlob(compressed.dataUrl)
const formData = new FormData()
formData.append('photo', blob, 'photo.jpg')

// Batch compress multiple photos
const results = await batchCompressPhotos(
  photoArray,
  { maxSizeKB: 500 },
  (current, total) => console.log(`${current}/${total}`)
)
```

**Available Functions:**
- `compressPhoto` - Compress image to size limit
- `getBase64SizeKB` - Calculate base64 size
- `checkStorageQuota` - Check browser storage
- `fileToBase64` - Convert File to base64
- `base64ToBlob` - Convert base64 to Blob
- `downloadImage` - Trigger download
- `batchCompressPhotos` - Compress multiple

### Formatters

Number, currency, phone, and text formatting utilities with Indonesian locale support.

```typescript
import {
  formatCurrency,
  formatRupiah,
  formatNumber,
  formatCompactNumber,
  formatPhoneIndonesia,
  formatKTP,
  formatPercentage,
  formatFileSize,
  pluralize
} from '@kodeme-io/next-core-patterns'

// Currency formatting
formatRupiah(1500000)  // "Rp1.500.000"
formatRupiah(1500000, true)  // "Rp1.500.000,00"
formatCurrency(1500, { currency: 'USD', locale: 'en-US' })  // "$1,500.00"

// Number formatting
formatNumber(1500000)  // "1.500.000" (Indonesian)
formatCompactNumber(1500000)  // "1.5M"
formatPercentage(0.156)  // "15.6%"
formatFileSize(1536000)  // "1.5 MB"

// Phone & ID formatting
formatPhoneIndonesia('081234567890')  // "0812-3456-7890"
formatPhoneIndonesia('081234567890', true)  // "+62 812-3456-7890"
formatKTP('3216012345678901')  // "3216-012345-678901"

// Text formatting
pluralize(5, 'item')  // "5 items"
truncateText('Long text...', 10)  // "Long te..."
```

**Available Functions** (13):
- Currency: `formatCurrency`, `formatRupiah`, `formatNumber`, `formatCompactNumber`
- Phone/ID: `formatPhoneIndonesia`, `formatKTP`
- Numbers: `formatPercentage`, `formatFileSize`, `formatCoordinates`
- Text: `truncateText`, `formatDateIndonesia`, `formatDateRange`, `pluralize`

### Validators

Form validation and data integrity checks with helpful error messages.

```typescript
import {
  isValidEmail,
  isValidPhoneIndonesia,
  validatePassword,
  validateRequired,
  validateRange,
  isNumeric
} from '@kodeme-io/next-core-patterns'

// Email & phone validation
isValidEmail('user@example.com')  // true
isValidPhoneIndonesia('081234567890')  // true

// Password validation
const result = validatePassword('weak')
// { isValid: false, error: 'Password must be at least 8 characters' }

validatePassword('Password123')
// { isValid: true }

// Field validation
validateRequired('', 'Email')
// { isValid: false, error: 'Email is required' }

validateRange(15, 1, 10)
// { isValid: false, error: 'Value must be between 1 and 10' }

// Simple checks
isNumeric('12345')  // true
isAlphanumeric('abc123')  // true
```

**Available Functions** (18):
- Email/Phone/ID: `isValidEmail`, `isValidPhoneIndonesia`, `isValidKTP`, `isValidPostalCode`
- URL/Card: `isValidURL`, `isValidCreditCard`
- Password: `validatePassword`
- Range/Length: `validateRange`, `validateMinLength`, `validateMaxLength`
- Required/Match: `validateRequired`, `validateMatch`
- String Type: `isNumeric`, `isAlphanumeric`
- Date: `validateFutureDate`, `validateDateRange`
- File: `validateFileSize`, `validateFileType`

### String Utilities

String manipulation and transformation functions.

```typescript
import {
  slugify,
  titleCase,
  toCamelCase,
  toSnakeCase,
  getInitials,
  maskString,
  randomString
} from '@kodeme-io/next-core-patterns'

// Case conversion
slugify('Hello World!')  // "hello-world"
titleCase('hello world')  // "Hello World"
toCamelCase('hello world')  // "helloWorld"
toSnakeCase('helloWorld')  // "hello_world"
toKebabCase('helloWorld')  // "hello-world"

// Name utilities
getInitials('John Doe')  // "JD"
maskString('1234567890123456', 4, 4)  // "1234********3456"

// Text manipulation
randomString(8)  // "a7B3xY9k"
countWords('hello world')  // 2
contains('Hello World', 'world')  // true (case insensitive)

// HTML safety
escapeHtml('<script>alert("xss")</script>')
// "&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"
```

**Available Functions** (20):
- Case Conversion: `slugify`, `titleCase`, `sentenceCase`, `camelToTitle`, `snakeToTitle`
- Case Transforms: `toCamelCase`, `toSnakeCase`, `toKebabCase`, `capitalize`
- Text Manipulation: `removeExtraSpaces`, `getInitials`, `maskString`, `reverse`
- Text Analysis: `countWords`, `contains`, `stripHtml`, `escapeHtml`
- Generation: `randomString`, `pad`, `wordWrap`

### React Query Helpers

Utilities for @tanstack/react-query - standardized patterns for data fetching.

```typescript
import {
  createQueryKeys,
  StaleTime,
  createPagination,
  useDebouncedQueryKey
} from '@kodeme-io/next-core-patterns'

// Create query key factory
const customerKeys = createQueryKeys('customers')

useQuery({
  queryKey: customerKeys.list({ active: true }),
  queryFn: () => fetchCustomers({ active: true }),
  staleTime: StaleTime.MEDIUM  // 5 minutes
})

useQuery({
  queryKey: customerKeys.detail(123),
  queryFn: () => fetchCustomer(123),
  staleTime: StaleTime.LONG  // 10 minutes
})

// Pagination
const pagination = createPagination(20)
const { data } = useQuery({
  queryKey: ['items', page],
  queryFn: () => fetchItems(pagination.getOffset(page), pagination.pageSize)
})

// Debounced search
const [search, setSearch] = useState('')
const debouncedKey = useDebouncedQueryKey(['search'], search, 500)
const { data } = useQuery({
  queryKey: debouncedKey,
  queryFn: () => searchItems(search)
})
```

**Available Functions** (10):
- Keys: `createQueryKeys` - Type-safe query key factory
- Cache: `StaleTime`, `RefetchInterval` - Standard timing presets
- Updates: `createOptimisticUpdate` - Optimistic update handlers
- Pagination: `createPagination`, `createInfiniteQueryHelpers`
- Search: `useDebouncedQueryKey` - Debounced keys for search
- Errors: `getQueryErrorMessage`, `isQueryLoading` - Error handling

### Odoo Helpers

Utilities for working with Odoo ERP data structures and APIs.

```typescript
import {
  extractMany2oneId,
  extractMany2oneName,
  toOdooDatetime,
  toOdooDate,
  buildDomain,
  formatOdooMonetary
} from '@kodeme-io/next-core-patterns'

// Many2one fields
const userId = extractMany2oneId(partner.user_id)  // [5, "John"] → 5
const userName = extractMany2oneName(partner.user_id)  // [5, "John"] → "John"

// Date/datetime conversion
toOdooDatetime(new Date())  // "2024-12-25 10:30:00"
toOdooDate(new Date())  // "2024-12-25"

// Domain builder (type-safe)
const domain = buildDomain()
  .equals('active', true)
  .ilike('name', 'john')
  .greaterThan('age', 18)
  .build()
// [['active', '=', true], ['name', 'ilike', 'john'], ['age', '>', 18]]

// OR conditions
const domain = buildDomain()
  .or()
  .equals('state', 'draft')
  .equals('state', 'pending')
  .build()
// ['|', ['state', '=', 'draft'], ['state', '=', 'pending']]

// Format monetary
formatOdooMonetary(1500000)  // "Rp 1.500.000"
```

**Available Functions** (12):
- Many2one: `extractMany2oneId`, `extractMany2oneName`
- Dates: `toOdooDatetime`, `toOdooDate`, `fromOdooDatetime`, `fromOdooDate`
- Domains: `buildDomain` - Type-safe domain builder
- Formatting: `formatOdooMonetary`, `parseOdooSelection`
- Records: `isOdooRecordActive`, `buildOdooContext`, `normalizeOdooRecord`

### Array Utilities

Common array operations for filtering, grouping, sorting, and analysis.

```typescript
import {
  groupBy,
  unique,
  sortBy,
  chunk,
  sum,
  average,
  partition
} from '@kodeme-io/next-core-patterns'

// Group by key
const customers = [
  { id: 1, city: 'Jakarta', total: 100 },
  { id: 2, city: 'Jakarta', total: 200 },
  { id: 3, city: 'Bandung', total: 150 }
]

const byCity = groupBy(customers, 'city')
// Map { 'Jakarta' => [{...}, {...}], 'Bandung' => [{...}] }

// Remove duplicates
unique([1, 2, 2, 3, 3, 3])  // [1, 2, 3]
unique(items, 'id')  // Remove by id

// Sort
sortBy(customers, 'total', 'desc')  // Highest first

// Chunk for pagination
chunk([1, 2, 3, 4, 5], 2)  // [[1, 2], [3, 4], [5]]

// Math operations
sum(orders, 'total')  // Sum all totals
average(scores, 'value')  // Average score
min(products, 'price')  // Lowest price
max(products, 'price')  // Highest price

// Partition by condition
const [active, inactive] = partition(users, (u) => u.active)
```

**Available Functions** (23):
- Grouping: `groupBy`, `unique`, `filterBy`, `findBy`
- Sorting: `sortBy`, `shuffle`
- Chunking: `chunk`, `take`, `takeLast`, `partition`
- Math: `sum`, `average`, `min`, `max`
- Manipulation: `flatten`, `flattenDeep`, `compact`
- Generation: `range`
- Checks: `isEmpty`

## Features

- ✅ **Offline-First**: All patterns work offline with automatic sync when online
- ✅ **Type-Safe**: Full TypeScript support
- ✅ **Framework-Agnostic**: Works with any backend or API
- ✅ **Persistent**: Uses localStorage for data persistence
- ✅ **Automatic Retry**: Failed operations retry automatically
- ✅ **Zero Dependencies**: Pure TypeScript implementation
- ✅ **GPS & Geofencing**: Haversine distance calculations
- ✅ **Image Compression**: Canvas-based photo compression
- ✅ **Storage Management**: Quota checking and monitoring

## Use Cases

- **Field Workers**: Offline sync, geofencing, photo compression
- **E-commerce**: Pricing calculations, order totals
- **Logistics**: Delivery schedules, route validation
- **Sales**: Visit schedules, customer check-ins
- **PWA Apps**: Storage management, media optimization

## License

MIT © ABC Food