360 lines
10 KiB
TypeScript
360 lines
10 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
IonContent,
|
|
IonPage,
|
|
IonHeader,
|
|
IonToolbar,
|
|
IonTitle,
|
|
IonButtons,
|
|
IonButton,
|
|
IonIcon,
|
|
IonInput,
|
|
IonTextarea,
|
|
IonSpinner,
|
|
IonLabel,
|
|
IonSelect,
|
|
IonSelectOption,
|
|
} from '@ionic/react';
|
|
import { chevronBackOutline } from 'ionicons/icons';
|
|
import { useHistory, useLocation } from 'react-router-dom';
|
|
import { supabase } from '../supabase';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { getTodayISO } from '../utils/dates';
|
|
import MoneyInput from '../components/MoneyInput';
|
|
import { parseMoney } from '../utils/money';
|
|
|
|
interface Customer {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
const CreditFormPage: React.FC = () => {
|
|
const history = useHistory();
|
|
const location = useLocation<{ customerId?: string }>();
|
|
const { user } = useAuth();
|
|
|
|
const [mode, setMode] = useState<'existing' | 'new'>('existing');
|
|
const [customers, setCustomers] = useState<Customer[]>([]);
|
|
const [selectedCustomerId, setSelectedCustomerId] = useState<string>(
|
|
location.state?.customerId || ''
|
|
);
|
|
const [newName, setNewName] = useState('');
|
|
const [phone, setPhone] = useState('');
|
|
const [amount, setAmount] = useState('');
|
|
const [creditDate, setCreditDate] = useState(getTodayISO());
|
|
const [description, setDescription] = useState('');
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [loadingCustomers, setLoadingCustomers] = useState(true);
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchCustomers = async () => {
|
|
if (!user) return;
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('credit_customers')
|
|
.select('id, name')
|
|
.eq('user_id', user.id)
|
|
.order('name');
|
|
|
|
if (error) throw error;
|
|
setCustomers(data || []);
|
|
if (!data || data.length === 0) {
|
|
setMode('new');
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching customers:', err);
|
|
} finally {
|
|
setLoadingCustomers(false);
|
|
}
|
|
};
|
|
|
|
fetchCustomers();
|
|
}, [user]);
|
|
|
|
const validate = () => {
|
|
const newErrors: Record<string, string> = {};
|
|
const parsedAmount = parseMoney(amount);
|
|
|
|
if (mode === 'existing' && !selectedCustomerId) {
|
|
newErrors.customer = 'Please select a customer';
|
|
}
|
|
if (mode === 'new' && !newName.trim()) {
|
|
newErrors.name = 'Customer name is required';
|
|
}
|
|
if (parsedAmount <= 0) {
|
|
newErrors.amount = 'Amount must be greater than 0';
|
|
}
|
|
if (!creditDate) {
|
|
newErrors.date = 'Date is required';
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!validate() || !user) return;
|
|
|
|
setSubmitting(true);
|
|
setMessage(null);
|
|
|
|
try {
|
|
let customerId = selectedCustomerId;
|
|
|
|
if (mode === 'new') {
|
|
const { data: newCustomer, error: customerError } = await supabase
|
|
.from('credit_customers')
|
|
.insert([
|
|
{
|
|
user_id: user.id,
|
|
name: newName.trim(),
|
|
phone: phone.trim() || null,
|
|
},
|
|
])
|
|
.select()
|
|
.single();
|
|
|
|
if (customerError) throw customerError;
|
|
customerId = newCustomer.id;
|
|
}
|
|
|
|
const { error: creditError } = await supabase.from('credits').insert([
|
|
{
|
|
user_id: user.id,
|
|
customer_id: customerId,
|
|
amount: parseMoney(amount),
|
|
credit_date: creditDate,
|
|
description: description.trim() || null,
|
|
},
|
|
]);
|
|
|
|
if (creditError) throw creditError;
|
|
|
|
history.goBack();
|
|
} catch (err: any) {
|
|
console.error('Error saving credit:', err);
|
|
setMessage(err.message || 'Could not save credit');
|
|
setTimeout(() => setMessage(null), 4000);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<IonPage>
|
|
<IonHeader className="ion-no-border">
|
|
<IonToolbar style={{ '--background': '#f0f4ff' }}>
|
|
<IonButtons slot="start">
|
|
<IonButton color="dark" onClick={() => history.goBack()}>
|
|
<IonIcon icon={chevronBackOutline} slot="icon-only" />
|
|
</IonButton>
|
|
</IonButtons>
|
|
<IonTitle className="bold">Add credit</IonTitle>
|
|
</IonToolbar>
|
|
</IonHeader>
|
|
|
|
<IonContent className="ion-padding" style={{ '--background': '#f0f4ff' }}>
|
|
<div className="form-card">
|
|
<div className="pill-toggle" style={{ marginBottom: '8px' }}>
|
|
<button
|
|
className={`pill-toggle-btn ${mode === 'existing' ? 'active' : ''}`}
|
|
onClick={() => setMode('existing')}
|
|
disabled={customers.length === 0}
|
|
>
|
|
Existing Customer
|
|
</button>
|
|
<button
|
|
className={`pill-toggle-btn ${mode === 'new' ? 'active' : ''}`}
|
|
onClick={() => setMode('new')}
|
|
>
|
|
New Customer
|
|
</button>
|
|
</div>
|
|
|
|
{mode === 'existing' ? (
|
|
<div className="form-field">
|
|
<IonLabel
|
|
className="semibold"
|
|
style={{
|
|
marginBottom: '8px',
|
|
display: 'block',
|
|
fontSize: '13px',
|
|
}}
|
|
>
|
|
Select Customer
|
|
</IonLabel>
|
|
<div className="input-wrapper">
|
|
<IonSelect
|
|
interface="action-sheet"
|
|
placeholder="Choose a customer"
|
|
value={selectedCustomerId}
|
|
onIonChange={(e) => setSelectedCustomerId(e.detail.value)}
|
|
style={{ width: '100%' }}
|
|
>
|
|
{customers.map((c) => (
|
|
<IonSelectOption key={c.id} value={c.id}>
|
|
{c.name}
|
|
</IonSelectOption>
|
|
))}
|
|
</IonSelect>
|
|
</div>
|
|
{errors.customer && (
|
|
<IonLabel
|
|
color="danger"
|
|
style={{ fontSize: '12px', marginTop: '4px' }}
|
|
>
|
|
{errors.customer}
|
|
</IonLabel>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="form-field">
|
|
<IonLabel
|
|
className="semibold"
|
|
style={{
|
|
marginBottom: '8px',
|
|
display: 'block',
|
|
fontSize: '13px',
|
|
}}
|
|
>
|
|
Customer Name
|
|
</IonLabel>
|
|
<div className="input-wrapper">
|
|
<IonInput
|
|
placeholder="e.g. John Doe"
|
|
value={newName}
|
|
onIonChange={(e) => setNewName(e.detail.value!)}
|
|
/>
|
|
</div>
|
|
{errors.name && (
|
|
<IonLabel
|
|
color="danger"
|
|
style={{ fontSize: '12px', marginTop: '4px' }}
|
|
>
|
|
{errors.name}
|
|
</IonLabel>
|
|
)}
|
|
</div>
|
|
<div className="form-field">
|
|
<IonLabel
|
|
className="semibold"
|
|
style={{
|
|
marginBottom: '8px',
|
|
display: 'block',
|
|
fontSize: '13px',
|
|
}}
|
|
>
|
|
Phone (optional)
|
|
</IonLabel>
|
|
<div className="input-wrapper">
|
|
<IonInput
|
|
type="tel"
|
|
placeholder="e.g. +1 234 567 890"
|
|
value={phone}
|
|
onIonChange={(e) => setPhone(e.detail.value!)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<MoneyInput
|
|
label="Credit amount"
|
|
value={amount}
|
|
onValueChange={(val) => {
|
|
setAmount(val);
|
|
if (errors.amount) setErrors({ ...errors, amount: '' });
|
|
}}
|
|
error={errors.amount}
|
|
/>
|
|
|
|
<div className="form-field">
|
|
<IonLabel
|
|
className="semibold"
|
|
style={{
|
|
marginBottom: '8px',
|
|
display: 'block',
|
|
fontSize: '13px',
|
|
}}
|
|
>
|
|
Credit date
|
|
</IonLabel>
|
|
<div className="input-wrapper">
|
|
<IonInput
|
|
type="date"
|
|
value={creditDate}
|
|
onIonChange={(e) => setCreditDate(e.detail.value!)}
|
|
/>
|
|
</div>
|
|
{errors.date && (
|
|
<IonLabel
|
|
color="danger"
|
|
style={{ fontSize: '12px', marginTop: '4px' }}
|
|
>
|
|
{errors.date}
|
|
</IonLabel>
|
|
)}
|
|
</div>
|
|
|
|
<div className="form-field">
|
|
<IonLabel
|
|
className="semibold"
|
|
style={{
|
|
marginBottom: '8px',
|
|
display: 'block',
|
|
fontSize: '13px',
|
|
}}
|
|
>
|
|
Description (optional)
|
|
</IonLabel>
|
|
<div
|
|
className="input-wrapper"
|
|
style={{ minHeight: '96px', padding: '8px 16px' }}
|
|
>
|
|
<IonTextarea
|
|
placeholder="e.g. 2 bags of rice, sugar"
|
|
value={description}
|
|
onIonChange={(e) => setDescription(e.detail.value!)}
|
|
rows={4}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{message && (
|
|
<div
|
|
style={{
|
|
background: 'var(--color-danger-soft)',
|
|
color: 'var(--ion-color-danger-shade)',
|
|
padding: '12px',
|
|
borderRadius: '12px',
|
|
fontSize: '13px',
|
|
}}
|
|
>
|
|
{message}
|
|
</div>
|
|
)}
|
|
|
|
<IonButton
|
|
expand="block"
|
|
className="bold"
|
|
style={{
|
|
height: '52px',
|
|
'--border-radius': 'var(--radius-pill)',
|
|
marginTop: '16px',
|
|
}}
|
|
disabled={submitting || (loadingCustomers && mode === 'existing')}
|
|
onClick={handleSave}
|
|
>
|
|
{submitting ? <IonSpinner name="crescent" /> : 'Save credit'}
|
|
</IonButton>
|
|
</div>
|
|
</IonContent>
|
|
</IonPage>
|
|
);
|
|
};
|
|
|
|
export default CreditFormPage;
|