3import React, { useEffect, useState } from "react";
4import { apiClient } from "@/lib/client/apiClient";
6interface LoyaltyCampaign {
9 f102?: string; // Start Date
10 f103?: string; // Earn End Date
11 f114?: string; // Redeem End Date
12 f104?: number; // Reward Level
13 f105?: number; // Money Reward Level
14 f106?: number; // Gain (multiplier)
15 f107?: number; // Input Pid
16 f108?: number; // Output Pid
17 f109?: number; // Membership Type
18 f113?: number; // Active Days
21export default function LoyaltyCampaignsPage() {
22 const [campaigns, setCampaigns] = useState<LoyaltyCampaign[]>([]);
23 const [loading, setLoading] = useState(false);
24 const [error, setError] = useState<string>("");
25 const [showNewCampaignModal, setShowNewCampaignModal] = useState(false);
26 const [saving, setSaving] = useState(false);
27 const [searchTerm, setSearchTerm] = useState("");
29 const [newCampaign, setNewCampaign] = useState({
39 membershipType: "1", // All Customers by default
47 const loadCampaigns = async () => {
51 const response = await apiClient.getBuckData({ table: 'retailmax.elink.loyalty.campaign', want: 'all' });
53 if (response.success && response.data) {
54 const appData = response.data.APPD || [];
55 const mapped: LoyaltyCampaign[] = appData.map((item: any) => ({
56 f100: Number(item.f100) || 0,
57 f101: item.f101 || "",
58 f102: item.f102 || "",
59 f103: item.f103 || "",
60 f114: item.f114 || "",
61 f104: Number(item.f104) || 0,
62 f105: Number(item.f105) || 0,
63 f106: Number(item.f106) || 1,
64 f107: Number(item.f107) || 0,
65 f108: Number(item.f108) || 0,
66 f109: Number(item.f109) || 0,
67 f113: Number(item.f113) || 0
70 } else if (response.success) {
71 // API succeeded but no data - this is OK, just means no campaigns exist
75 setError("Error loading campaigns. Please try again.");
78 console.error("Failed to load loyalty campaigns", err);
79 setError("Error loading campaigns. Please try again.");
85 const handleCreateCampaign = async () => {
86 if (!newCampaign.name.trim()) {
87 alert("Please enter a campaign name");
93 // Create campaign using DATI API
96 '<f8_s>retailmax.elink.loyalty.campaign</f8_s>',
98 `<f101_s>${newCampaign.name.replace(/&/g, '&').replace(/</g, '<')}</f101_s>`,
99 newCampaign.startDate ? `<f102_s>${newCampaign.startDate}</f102_s>` : '',
100 newCampaign.earnEndDate ? `<f103_s>${newCampaign.earnEndDate}</f103_s>` : '',
101 newCampaign.redeemEndDate ? `<f114_s>${newCampaign.redeemEndDate}</f114_s>` : '',
102 newCampaign.rewardLevel ? `<f104_E>${newCampaign.rewardLevel}</f104_E>` : '',
103 newCampaign.moneyRewardLevel ? `<f105_E>${newCampaign.moneyRewardLevel}</f105_E>` : '',
104 `<f106_E>${newCampaign.gain || 1}</f106_E>`,
105 newCampaign.inputPid ? `<f107_E>${newCampaign.inputPid}</f107_E>` : '',
106 newCampaign.outputPid ? `<f108_E>${newCampaign.outputPid}</f108_E>` : '',
107 `<f109_E>${newCampaign.membershipType}</f109_E>`,
108 newCampaign.activeDays ? `<f113_E>${newCampaign.activeDays}</f113_E>` : '',
112 const response = await fetch('/dati', {
114 headers: { 'Content-Type': 'application/xml', 'Accept': 'application/json' },
115 credentials: 'include',
120 alert("Campaign created successfully!");
121 setShowNewCampaignModal(false);
128 moneyRewardLevel: "",
138 console.error("Failed to create campaign", err);
139 alert("Failed to create campaign. Please try again.");
145 const filteredCampaigns = campaigns.filter(c =>
147 c.f101.toLowerCase().includes(searchTerm.toLowerCase()) ||
148 String(c.f100).includes(searchTerm)
151 const formatDate = (dateStr?: string) => {
152 if (!dateStr) return "-";
154 const date = new Date(dateStr);
155 if (isNaN(date.getTime())) return dateStr;
156 return date.toLocaleDateString("en-NZ", {
167 <div className="p-6">
169 <div className="mb-6">
170 <h1 className="text-3xl font-bold mb-2">Loyalty Campaigns 🎁</h1>
171 <p className="text-muted">Manage customer loyalty and rewards programs</p>
175 <div className="bg-surface rounded-lg shadow p-4 mb-6">
176 <div className="flex flex-wrap gap-4 items-center justify-between">
177 <div className="flex gap-3">
179 onClick={() => setShowNewCampaignModal(true)}
180 className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium"
185 onClick={loadCampaigns}
187 className="px-4 py-2 border border-border rounded-lg hover:bg-surface-2 disabled:opacity-50"
189 {loading ? "Loading..." : "Refresh"}
193 <div className="flex gap-2 items-center">
196 placeholder="Search campaigns..."
198 onChange={(e) => setSearchTerm(e.target.value)}
199 className="px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
201 <span className="text-sm text-muted">
202 Displaying {filteredCampaigns.length}
208 {/* Error Message */}
210 <div className="mb-4 p-4 bg-red-50 border-l-4 border-red-500 text-red-700">
215 {/* Campaigns Table */}
216 <div className="bg-surface rounded-lg shadow overflow-hidden">
218 <div className="p-8 text-center">
219 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
220 <p className="mt-2 text-muted">Loading campaigns...</p>
223 <div className="overflow-x-auto">
224 <table className="w-full text-sm">
225 <thead className="bg-surface-2 sticky top-0">
227 <th className="px-4 py-3 text-left font-semibold">ID</th>
228 <th className="px-4 py-3 text-left font-semibold">Name</th>
229 <th className="px-4 py-3 text-left font-semibold">Start Date</th>
230 <th className="px-4 py-3 text-left font-semibold">Earn End</th>
231 <th className="px-4 py-3 text-left font-semibold">Redeem End</th>
232 <th className="px-4 py-3 text-right font-semibold">Reward Level</th>
233 <th className="px-4 py-3 text-right font-semibold">$ Reward</th>
234 <th className="px-4 py-3 text-right font-semibold">Gain</th>
235 <th className="px-4 py-3 text-center font-semibold">Options</th>
238 <tbody className="divide-y divide-gray-200">
239 {filteredCampaigns.length === 0 ? (
241 <td colSpan={9} className="px-4 py-12 text-center">
242 <div className="flex flex-col items-center justify-center">
243 <div className="text-6xl mb-4">🎁</div>
244 <h3 className="text-lg font-semibold text-text mb-2">
245 {searchTerm ? "No campaigns match your search" : "No loyalty campaigns yet"}
249 <p className="text-muted mb-4">
250 Create your first loyalty campaign to reward your customers
253 onClick={() => setShowNewCampaignModal(true)}
254 className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium"
264 filteredCampaigns.map((campaign) => (
265 <tr key={campaign.f100} className="hover:bg-surface-2">
266 <td className="px-4 py-3 font-mono text-sm text-green-600">
269 <td className="px-4 py-3 font-medium">
272 <td className="px-4 py-3 text-muted">
273 {formatDate(campaign.f102)}
275 <td className="px-4 py-3 text-muted">
276 {formatDate(campaign.f103)}
278 <td className="px-4 py-3 text-muted">
279 {formatDate(campaign.f114)}
281 <td className="px-4 py-3 text-right">
282 {campaign.f104 || "-"}
284 <td className="px-4 py-3 text-right">
285 {campaign.f105 ? `$${campaign.f105.toFixed(2)}` : "-"}
287 <td className="px-4 py-3 text-right">
288 {campaign.f106 ? `${campaign.f106}x` : "1x"}
290 <td className="px-4 py-3 text-center">
291 <div className="flex gap-2 justify-center">
292 <button className="px-3 py-1 bg-brand text-white rounded hover:bg-brand/90 text-sm font-medium">
295 <button className="px-3 py-1 bg-surface-2 text-text rounded hover:bg-surface-2 text-sm font-medium">
309 {/* New Campaign Modal */}
310 {showNewCampaignModal && (
312 className="fixed inset-0 bg-black/30 z-50 flex items-start justify-center p-4 overflow-y-auto"
313 onClick={() => setShowNewCampaignModal(false)}
316 className="bg-surface rounded-lg shadow-xl max-w-2xl w-full my-8"
317 onClick={(e) => e.stopPropagation()}
319 <div className="border-b border-border p-4 flex items-center justify-between">
320 <h2 className="text-xl font-bold text-green-600">Create New Loyalty Campaign</h2>
322 onClick={() => setShowNewCampaignModal(false)}
323 className="px-3 py-1 text-sm border border-border rounded hover:bg-surface-2"
329 <div className="p-6 space-y-4">
330 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
331 <div className="md:col-span-2">
332 <label className="block text-sm font-medium text-text mb-1">
337 value={newCampaign.name}
338 onChange={(e) => setNewCampaign({...newCampaign, name: e.target.value})}
339 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
340 placeholder="e.g., Summer Rewards 2025"
342 <p className="text-xs text-muted mt-1">Used to name the campaign on your reports</p>
346 <label className="block text-sm font-medium text-text mb-1">
351 value={newCampaign.startDate}
352 onChange={(e) => setNewCampaign({...newCampaign, startDate: e.target.value})}
353 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
358 <label className="block text-sm font-medium text-text mb-1">
363 value={newCampaign.earnEndDate}
364 onChange={(e) => setNewCampaign({...newCampaign, earnEndDate: e.target.value})}
365 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
370 <label className="block text-sm font-medium text-text mb-1">
375 value={newCampaign.redeemEndDate}
376 onChange={(e) => setNewCampaign({...newCampaign, redeemEndDate: e.target.value})}
377 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
382 <label className="block text-sm font-medium text-text mb-1">
387 value={newCampaign.rewardLevel}
388 onChange={(e) => setNewCampaign({...newCampaign, rewardLevel: e.target.value})}
389 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
390 placeholder="e.g., 100"
395 <label className="block text-sm font-medium text-text mb-1">
401 value={newCampaign.moneyRewardLevel}
402 onChange={(e) => setNewCampaign({...newCampaign, moneyRewardLevel: e.target.value})}
403 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
404 placeholder="e.g., 10.00"
409 <label className="block text-sm font-medium text-text mb-1">
415 value={newCampaign.gain}
416 onChange={(e) => setNewCampaign({...newCampaign, gain: e.target.value})}
417 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
419 <p className="text-xs text-muted mt-1">Points multiplier (e.g., 2 = double rewards)</p>
423 <label className="block text-sm font-medium text-text mb-1">
427 value={newCampaign.membershipType}
428 onChange={(e) => setNewCampaign({...newCampaign, membershipType: e.target.value})}
429 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
431 <option value="0">Individually add Customers</option>
432 <option value="1">All Customers Included</option>
437 <label className="block text-sm font-medium text-text mb-1">
442 value={newCampaign.inputPid}
443 onChange={(e) => setNewCampaign({...newCampaign, inputPid: e.target.value})}
444 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
449 <label className="block text-sm font-medium text-text mb-1">
454 value={newCampaign.outputPid}
455 onChange={(e) => setNewCampaign({...newCampaign, outputPid: e.target.value})}
456 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
461 <label className="block text-sm font-medium text-text mb-1">
466 value={newCampaign.activeDays}
467 onChange={(e) => setNewCampaign({...newCampaign, activeDays: e.target.value})}
468 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-600"
469 placeholder="e.g., 365"
471 <p className="text-xs text-muted mt-1">Days of inactivity before customer considered inactive</p>
475 <div className="pt-4 border-t border-border flex justify-end gap-3">
477 onClick={() => setShowNewCampaignModal(false)}
478 className="px-4 py-2 border border-border rounded-lg hover:bg-surface-2"
484 onClick={handleCreateCampaign}
485 disabled={saving || !newCampaign.name.trim()}
486 className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
488 {saving ? "Saving..." : "Save Campaign"}