3import React, { useState, useEffect } from 'react';
4import { Icon } from '@/contexts/IconContext';
12 ForeignChangeDt?: string;
14 PrintOnReceipts?: number;
15 OpenCashDrawer?: number;
17 IssuesChange?: number;
18 ObeysRounding?: number;
19 AccountDebit?: number;
20 AccountCredit?: number;
21 RewardLinked?: number;
25 NegativeEffect?: number;
30 MembershipKey: string;
35interface PaymentSetting {
38 MembershipKey: string;
41interface PredefinedPaymentType {
49export default function PaymentTypesPage() {
50 const [paymentTypes, setPaymentTypes] = useState<PaymentType[]>([]);
51 const [memberships, setMemberships] = useState<Membership[]>([]);
52 const [paymentSettings, setPaymentSettings] = useState<PaymentSetting[]>([]);
53 const [foreignEnabled, setForeignEnabled] = useState(false);
54 const [editingPaymentType, setEditingPaymentType] = useState<PaymentType | null>(null);
55 const [showEditModal, setShowEditModal] = useState(false);
56 const [showAddModal, setShowAddModal] = useState(false);
57 const [addingPaymentType, setAddingPaymentType] = useState<PredefinedPaymentType | null>(null);
58 const [laybuyMerchRef, setLaybuyMerchRef] = useState('');
59 const [laybuyApiKey, setLaybuyApiKey] = useState('');
65 const loadData = async () => {
68 // const response = await fetch('/api/payment-types');
69 // const data = await response.json();
70 // setPaymentTypes(data);
72 // Mock data for demonstration
73 const mockPaymentTypes: PaymentType[] = [
74 { Code: 1, CustSpell: 'Cash', OnlineFlags: 0, GlobalCode: 1, PrintOnReceipts: 1, OpenCashDrawer: 1, AffectsCash: 1, IssuesChange: 1, ObeysRounding: 1 },
75 { Code: 2, CustSpell: 'Cheque', OnlineFlags: 0, GlobalCode: 2, PrintOnReceipts: 1, ItemiseEOD: 1 },
76 { Code: 3, CustSpell: 'EFTPOS', OnlineFlags: 0, GlobalCode: 3, PrintOnReceipts: 1, ItemiseEOD: 1 },
77 { Code: 4, CustSpell: 'Verified Cheque', OnlineFlags: 0, GlobalCode: 4, PrintOnReceipts: 1 },
78 { Code: 5, CustSpell: 'EFTPOS failure', OnlineFlags: 0, GlobalCode: 5 },
79 { Code: 6, CustSpell: 'Change', OnlineFlags: 0, GlobalCode: 6 },
80 { Code: 7, CustSpell: 'Rounding', OnlineFlags: 0, GlobalCode: 7 },
81 { Code: 8, CustSpell: 'Return Refund', OnlineFlags: 0, GlobalCode: 8 },
82 { Code: 9, CustSpell: 'Credit Card', OnlineFlags: 0, GlobalCode: 9, PrintOnReceipts: 1, ItemiseEOD: 1 },
83 { Code: 10, CustSpell: 'Credit Card failure', OnlineFlags: 0, GlobalCode: 10 },
84 { Code: 11, CustSpell: 'Sale discount', OnlineFlags: 0, GlobalCode: 11 },
85 { Code: 20, CustSpell: 'Account', OnlineFlags: 0, GlobalCode: 200, PrintOnReceipts: 1, AccountDebit: 1 },
86 { Code: 21, CustSpell: 'Voucher', OnlineFlags: 0 },
87 { Code: 22, CustSpell: 'Direct Debit', OnlineFlags: 0 },
89 setPaymentTypes(mockPaymentTypes);
92 const mockMemberships: Membership[] = [
93 { MembershipKey: 'membership1', Name: 'Agency', AmOwner: true }
95 setMemberships(mockMemberships);
97 // Load payment settings
98 setPaymentSettings([]);
100 // Load foreign currency setting
101 setForeignEnabled(false);
104 console.error('Error loading payment types:', error);
108 const canEnableOnline = (paymentType: PaymentType): boolean => {
109 const disabledCodes = [5, 6, 7, 8, 10, 11];
110 const disabledGlobalCodes = [164001];
112 if (disabledCodes.includes(paymentType.Code)) return false;
113 if (paymentType.GlobalCode && disabledGlobalCodes.includes(paymentType.GlobalCode)) return false;
118 const handleToggleOnline = (index: number) => {
119 const paymentType = paymentTypes[index];
120 const newValue = paymentType.OnlineFlags ? 0 : 1;
122 console.log('Toggle online for payment type:', paymentType.Code, 'to', newValue);
124 // Update local state
125 const updated = [...paymentTypes];
126 updated[index] = { ...updated[index], OnlineFlags: newValue };
127 setPaymentTypes(updated);
130 // API call would go here
133 const handleSaveRate = (paymentTypeId: number, index: number, value: string) => {
134 const paymentType = paymentTypes[index];
135 if (paymentType.RateBuy === value) return;
137 console.log('Save rate for payment type:', paymentTypeId, 'value:', value);
139 // Update local state
140 const updated = [...paymentTypes];
141 updated[index] = { ...updated[index], RateBuy: value };
142 setPaymentTypes(updated);
147 const handleEditSettings = (index: number) => {
148 setEditingPaymentType(paymentTypes[index]);
149 setShowEditModal(true);
152 const handleSaveSettings = () => {
153 if (!editingPaymentType) return;
155 console.log('Saving payment type settings:', editingPaymentType);
158 const index = paymentTypes.findIndex(pt => pt.Code === editingPaymentType.Code);
160 const updated = [...paymentTypes];
161 updated[index] = editingPaymentType;
162 setPaymentTypes(updated);
166 setShowEditModal(false);
167 setEditingPaymentType(null);
170 const handleToggleMembership = (globalCode: number, membershipIndex: number, checked: boolean) => {
171 console.log('Toggle membership:', globalCode, membershipIndex, checked);
175 const handleAddPaymentType = (paymentType: PredefinedPaymentType) => {
176 if (paymentType.id === 164001) {
177 // Special handling for Laybuy
178 const existing = paymentTypes.find(pt => pt.GlobalCode === 164001);
180 setLaybuyMerchRef(existing.Code.toString()); // This would be Interface1
181 setLaybuyApiKey(''); // This would be Interface2
183 setAddingPaymentType(paymentType);
184 setShowAddModal(true);
186 // Check if foreign currency and not enabled
187 if (!foreignEnabled && paymentType.id >= 300000 && paymentType.id <= 399999) {
188 alert('Please enable foreign currency handling before attempting to add foreign currency payment types');
192 setAddingPaymentType(paymentType);
193 setShowAddModal(true);
197 const confirmAddPaymentType = () => {
198 if (!addingPaymentType) return;
200 console.log('Adding payment type:', addingPaymentType);
203 setShowAddModal(false);
204 setAddingPaymentType(null);
210 const handleSaveLaybuy = () => {
211 if (laybuyMerchRef.length < 4) {
212 alert('A merchant reference is required');
215 if (laybuyApiKey.length < 4) {
216 alert('An API Key is required');
220 console.log('Saving Laybuy settings:', { laybuyMerchRef, laybuyApiKey });
223 setShowAddModal(false);
224 setLaybuyMerchRef('');
231 const predefinedPaymentTypes: PredefinedPaymentType[] = [
232 { id: 161001, name: 'ZipPay' },
233 { id: 164001, name: 'Laybuy.com', link: 'https://laybuy.com' },
234 { id: 164002, name: 'Farmlands Card' },
235 { id: 164003, name: 'Ruralco Card' },
236 { id: 164004, name: 'MTA Voucher' },
237 { id: 164005, name: 'C.R.T.' },
238 { id: 164006, name: 'TradeMe Ping' },
239 { id: 564000, name: 'Web Sale', description: 'Externally paid web sale' },
240 { id: 164007, name: 'Bartercard (NZ)' },
241 { id: 164008, name: 'Genoapay (NZ)' },
242 { id: 164009, name: 'Gem Finance (NZ)' },
243 { id: 200100, name: 'Direct Bank Transfer' },
244 { id: 200101, name: 'Manual Eftpos' },
245 { id: 200102, name: 'AMEX' },
246 { id: 200103, name: 'Diners' },
247 { id: 200104, name: 'Union Pay' },
248 { id: 200105, name: 'Agency Payment' },
249 { id: 200106, name: 'WeChat Pay' },
250 { id: 200107, name: 'Alipay' },
251 { id: 200108, name: 'PxPay' },
252 { id: 200114, name: 'PxFusion' },
253 { id: 200115, name: 'Store Discount Voucher', warning: 'This payment type has side effects with sales handling. Check documentation' },
254 { id: 200109, name: 'Windcave', description: 'Manual Entry' },
255 { id: 200111, name: 'Manual Customer Credit' },
256 { id: 200112, name: 'Shopify' },
257 { id: 200113, name: 'EAN-99 Coupon', description: 'Reserved for manufacturer cash off coupons' },
258 { id: 200120, name: 'Wise.com NZ$', link: 'https://wise.com' },
259 { id: 200121, name: 'Wise.com AU$', link: 'https://wise.com' },
260 { id: 200122, name: 'Wise.com UK£', link: 'https://wise.com' },
261 { id: 200123, name: 'Wise.com CAD$', link: 'https://wise.com' },
262 { id: 200124, name: 'Wise.com Euro €', link: 'https://wise.com' },
263 { id: 200125, name: 'Wise.com US$', link: 'https://wise.com' },
264 { id: 200130, name: 'PayPal' },
267 const foreignCurrencyTypes: PredefinedPaymentType[] = [
268 { id: 300062, name: 'Australian Dollar' },
269 { id: 309001, name: 'Canadian Dollar' },
270 { id: 309997, name: 'Euro' },
271 { id: 300081, name: 'Japanese Yen' },
272 { id: 300044, name: 'UK Pound' },
273 { id: 300001, name: 'US Dollars' },
276 const isPaymentTypeAdded = (id: number): boolean => {
277 return paymentTypes.some(pt => pt.GlobalCode === id || pt.Code === id);
280 const formatDateTime = (dateStr?: string) => {
281 if (!dateStr) return '';
282 const date = new Date(dateStr);
283 return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
287 <div className="min-h-screen bg-bg">
289 <div className="bg-brand text-surface px-6 py-4 shadow-lg">
290 <div className="flex items-center gap-4">
291 <div className="text-sm">
292 <a href="/pages/settings" className="hover:underline">Settings</a>
293 <span className="mx-2">›</span>
294 <span>Payment Types</span>
299 <div className="max-w-[1600px] mx-auto p-6">
301 <div className="mb-6">
302 <h1 className="text-3xl font-bold text-text mb-2 flex items-center gap-3">
303 <Icon name="payments" size={32} className="text-brand" />
304 Configure Payment Types
306 <div className="text-sm text-muted">
307 <a href="https://fieldpine.com/docs/database/db_r_paymenttypes.htm" target="_blank" className="text-brand hover:underline">
308 Database structure (technical)
313 {/* Current Payment Types */}
314 <div className="bg-surface rounded-lg shadow-md p-6 mb-8">
315 <h2 className="text-xl font-bold text-text mb-4">Current Payment Types</h2>
317 <div className="overflow-x-auto">
318 <table className="min-w-full border-collapse">
320 <tr className="bg-surface-2 border-b-2 border-border">
321 <th className="px-4 py-3 text-left text-sm font-semibold text-text">ID</th>
322 <th className="px-4 py-3 text-left text-sm font-semibold text-text">Description</th>
323 <th className="px-4 py-3 text-left text-sm font-semibold text-text">Fieldpine Online Enabled</th>
324 {foreignEnabled && <th className="px-4 py-3 text-left text-sm font-semibold text-text">Rate Buy</th>}
325 <th className="px-4 py-3 text-left text-sm font-semibold text-text">Options</th>
326 <th className="px-4 py-3 text-left text-sm font-semibold text-text">Notes</th>
327 <th className="px-4 py-3 text-left text-sm font-semibold text-text">GlobalCode</th>
328 {memberships.map((membership, idx) => (
329 <th key={idx} className="px-4 py-3 text-left text-sm font-semibold text-text">
330 Publish to<br />Membership:<br />{membership.Name}
336 {paymentTypes.map((paymentType, index) => {
337 const canEnable = canEnableOnline(paymentType);
338 const notes: string[] = [];
339 if (paymentType.GlobalCode === 164001) notes.push('Not supported by Fieldpine Online');
340 if (paymentType.Deleted) notes.push('Deleted');
343 <tr key={paymentType.Code} className="border-b border-border hover:bg-surface-2">
344 <td className="px-4 py-3 text-sm text-muted">{paymentType.Code}</td>
345 <td className="px-4 py-3 text-sm font-medium text-text">{paymentType.CustSpell}</td>
346 <td className="px-4 py-3">
348 <label className="relative inline-flex items-center cursor-pointer">
351 className="sr-only peer"
352 checked={paymentType.OnlineFlags === 1}
353 onChange={() => handleToggleOnline(index)}
355 <div className="w-11 h-6 bg-surface-2 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-surface after:border-border after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand"></div>
360 <td className="px-4 py-3">
361 {paymentType.Code >= 300000 && paymentType.Code <= 399999 ? (
366 value={paymentType.RateBuy || ''}
368 const updated = [...paymentTypes];
369 updated[index] = { ...updated[index], RateBuy: e.target.value };
370 setPaymentTypes(updated);
372 onBlur={(e) => handleSaveRate(paymentType.Code, index, e.target.value)}
373 className="border border-border rounded px-2 py-1 text-sm"
375 <div className="text-xs text-muted mt-1">
376 {formatDateTime(paymentType.ForeignChangeDt)}
382 <td className="px-4 py-3">
384 onClick={() => handleEditSettings(index)}
385 className="px-3 py-1 bg-brand text-surface rounded hover:bg-brand2 text-sm"
390 <td className="px-4 py-3 text-sm text-muted">{notes.join('; ')}</td>
391 <td className="px-4 py-3 text-sm text-muted">{paymentType.GlobalCode || ''}</td>
392 {memberships.map((membership, mIdx) => (
393 <td key={mIdx} className="px-4 py-3">
394 {paymentType.GlobalCode ? (
395 <label className="flex items-center">
398 onChange={(e) => handleToggleMembership(paymentType.GlobalCode!, mIdx, e.target.checked)}
399 className="w-4 h-4 text-brand border-border rounded focus:ring-brand"
401 <span className="ml-2 text-sm">Publish</span>
414 {/* Add New Payment Types */}
415 <div className="bg-surface rounded-lg shadow-md p-6 mb-8">
416 <h2 className="text-xl font-bold text-text mb-4">Enable New Payment Types</h2>
418 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
419 {predefinedPaymentTypes.filter(pt => !isPaymentTypeAdded(pt.id)).map(paymentType => (
420 <div key={paymentType.id} className="border border-border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer">
422 onClick={() => handleAddPaymentType(paymentType)}
423 className="text-[#00946b] hover:text-[#00543b] font-medium text-left w-full cursor-pointer"
426 {paymentType.description && <div className="text-sm text-muted mt-1">{paymentType.description}</div>}
428 {paymentType.link && (
429 <div className="mt-2">
430 <a href={paymentType.link} target="_blank" className="text-xs text-[#00946b] hover:underline">
431 Visit {paymentType.name}
435 <div className="text-xs text-muted mt-2">(#{paymentType.id})</div>
440 <p className="text-sm text-muted mt-6">
441 Need a new payment type added? Payment types are allocated by Fieldpine to ensure reports can properly classify them.
442 If you need a new payment type, please contact your Fieldpine agent to request it be added.
446 {/* Foreign Currency */}
447 <div className="bg-surface rounded-lg shadow-md p-6">
448 <h3 className="text-lg font-bold text-text mb-4">Foreign Currency</h3>
450 <p className="text-sm text-muted mb-4">
451 Enable foreign currency only when you accept these as <u>cash</u> and they are <em>not</em> the normal currency in your country
454 <div className="flex items-start gap-4 mb-6">
455 <label className="relative inline-flex items-center cursor-pointer">
458 className="sr-only peer"
459 checked={foreignEnabled}
460 onChange={(e) => setForeignEnabled(e.target.checked)}
462 <div className="w-11 h-6 bg-surface-2 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-surface after:border-border after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand"></div>
465 <div className="font-medium text-text">Enable Foreign Currency Handling</div>
466 <div className="text-sm text-muted">You may need to manually maintain exchange rates for the POS to use.</div>
470 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
471 {foreignCurrencyTypes.filter(pt => !isPaymentTypeAdded(pt.id)).map(paymentType => (
472 <div key={paymentType.id} className="border border-border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer">
474 onClick={() => handleAddPaymentType(paymentType)}
475 className="text-[#00946b] hover:text-[#00543b] font-medium text-left w-full cursor-pointer"
476 disabled={!foreignEnabled}
480 <div className="text-xs text-muted mt-2">(#{paymentType.id})</div>
487 {/* Edit Settings Modal */}
488 {showEditModal && editingPaymentType && (
489 <div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4">
490 <div className="bg-surface/95 rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
491 <div className="sticky top-0 bg-brand text-surface px-6 py-4 flex items-center justify-between rounded-t-lg">
492 <h2 className="text-xl font-bold">{editingPaymentType.CustSpell}</h2>
493 <button onClick={() => setShowEditModal(false)} className="text-surface hover:text-muted/70">
494 <Icon name="close" size={24} />
498 <div className="p-6">
499 <table className="w-full">
502 { key: 'PrintOnReceipts', label: 'Printed on receipts' },
503 { key: 'OpenCashDrawer', label: 'Open Cash drawer' },
504 { key: 'AffectsCash', label: 'Affects Cash' },
505 { key: 'IssuesChange', label: 'Issues Change' },
506 { key: 'ObeysRounding', label: 'Obeys rounding rules' },
507 { key: 'AccountDebit', label: 'Account debit' },
508 { key: 'AccountCredit', label: 'Account credit' },
509 { key: 'RewardLinked', label: 'Reward linked' },
510 { key: 'SVC', label: 'Prepay' },
511 { key: 'NoLoyalty', label: 'No loyalty' },
512 { key: 'ItemiseEOD', label: 'Itemise on end of day' },
513 { key: 'NegativeEffect', label: 'Negative sale effect' },
514 { key: 'Invisible', label: 'Hidden/Invisible' },
515 { key: 'Deleted', label: 'Deleted' },
518 <td className="py-2 pr-4 text-text">{field.label}</td>
519 <td className="py-2">
522 checked={(editingPaymentType as any)[field.key] === 1}
524 setEditingPaymentType({
525 ...editingPaymentType,
526 [field.key]: e.target.checked ? 1 : 0
529 className="w-4 h-4 text-[#00946b] border-border rounded focus:ring-[#00946b]"
535 <td className="py-2 pr-4 text-text">Global Code</td>
536 <td className="py-2">
540 value={editingPaymentType.GlobalCode || ''}
541 onChange={(e) => setEditingPaymentType({ ...editingPaymentType, GlobalCode: Number(e.target.value) || undefined })}
542 className="border border-border rounded px-2 py-1 text-sm"
549 <div className="mt-6 flex gap-3">
551 onClick={handleSaveSettings}
552 className="px-6 py-2 bg-brand text-surface rounded hover:bg-brand2 font-medium flex items-center gap-2"
554 <Icon name="save" size={18} />
558 onClick={() => setShowEditModal(false)}
559 className="px-6 py-2 bg-surface-2 text-text rounded hover:bg-surface-2/80 font-medium"
569 {/* Add Payment Type Modal */}
570 {showAddModal && addingPaymentType && (
571 <div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4">
572 <div className="bg-surface/95 rounded-lg shadow-2xl max-w-2xl w-full">
573 <div className="sticky top-0 bg-brand text-surface px-6 py-4 flex items-center justify-between rounded-t-lg">
574 <h2 className="text-xl font-bold">{addingPaymentType.name}</h2>
575 <button onClick={() => setShowAddModal(false)} className="text-surface hover:text-muted/70">
576 <Icon name="close" size={24} />
580 <div className="p-6">
581 {addingPaymentType.id === 164001 ? (
582 // Laybuy special form
584 <p className="mb-4 text-text">
585 Laybuy.com allows customers to purchase using a hire purchase like system.
586 You need to register with Laybuy.com and enter your details below.
588 <table className="w-full mb-4">
591 <td className="py-2 pr-4 text-text">Merchant Reference</td>
592 <td className="py-2">
596 value={laybuyMerchRef}
597 onChange={(e) => setLaybuyMerchRef(e.target.value)}
598 className="border border-border rounded px-3 py-1"
603 <td className="py-2 pr-4 text-text">API Key</td>
604 <td className="py-2">
609 onChange={(e) => setLaybuyApiKey(e.target.value)}
610 className="border border-border rounded px-3 py-1 w-full"
616 <div className="flex gap-3">
618 onClick={handleSaveLaybuy}
619 className="px-6 py-2 bg-brand text-surface rounded hover:bg-brand2 font-medium flex items-center gap-2"
621 <Icon name="save" size={18} />
625 onClick={() => setShowAddModal(false)}
626 className="px-6 py-2 bg-surface-2 text-text rounded hover:bg-surface-2/80 font-medium"
633 // Standard add confirmation
635 <p className="mb-4 text-text">
636 Confirm to add payment type #{addingPaymentType.id} of "{addingPaymentType.name}"
638 {addingPaymentType.warning && (
639 <p className="mb-4 text-red-600 font-bold">{addingPaymentType.warning}</p>
641 {addingPaymentType.id >= 300000 && addingPaymentType.id <= 399999 && (
642 <p className="mb-4 text-text">
643 This payment type is a foreign currency type.<br />
644 It should only be added when this is <em>not</em> the primary currency in your country.
647 <div className="flex gap-3">
649 onClick={confirmAddPaymentType}
650 className="px-6 py-2 bg-brand text-surface rounded hover:bg-brand2 font-medium flex items-center gap-2"
652 <Icon name="check" size={18} />
653 Yes. Add this payment type
656 onClick={() => setShowAddModal(false)}
657 className="px-6 py-2 bg-surface-2 text-text rounded hover:bg-surface-2/80 font-medium"