feat(medical-store): add supplier creation and editing modal in Supplier Directory

This commit is contained in:
DevSoftTech Server 2026-09-10 03:53:33 +05:30
parent 758bc4c452
commit 101e0631a8
3 changed files with 449 additions and 16 deletions

View File

@ -1671,6 +1671,7 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct
$routes->get('medical-store/sales/recent', 'MedicalStoreApi::recentSales');
$routes->get('medical-store/suppliers', 'MedicalStoreApi::suppliers');
$routes->post('medical-store/suppliers/save', 'MedicalStoreApi::saveSupplier');
$routes->post('medical-store/suppliers/delete', 'MedicalStoreApi::deleteSupplier');
$routes->post('medical-store/purchase/save', 'MedicalStoreApi::savePurchase');
$routes->post('medical-store/purchase/payment', 'MedicalStoreApi::saveSupplierPayment');
$routes->post('medical-store/transfer/request', 'MedicalStoreApi::requestTransfer');

View File

@ -1641,6 +1641,25 @@ class MedicalStoreApi extends BaseController
return $this->response->setJSON(['status' => 1, 'message' => 'Supplier saved successfully.', 'supplier_id' => $id]);
}
public function deleteSupplier()
{
$json = $this->request->getJSON(true) ?: $this->request->getPost();
$id = (int)($json['supplier_id'] ?? 0);
if ($id <= 0) {
return $this->response->setStatusCode(400)->setJSON(['status' => 0, 'message' => 'Invalid supplier ID.']);
}
// Check if supplier is referenced in purchases
$hasPurchases = $this->db->table('mst_purchases')->where('supplier_id', $id)->countAllResults();
if ($hasPurchases > 0) {
$this->db->table('mst_suppliers')->where('supplier_id', $id)->update(['is_active' => 0]);
return $this->response->setJSON(['status' => 1, 'message' => 'Supplier deactivated (cannot delete because purchase bills are linked).']);
}
$this->db->table('mst_suppliers')->where('supplier_id', $id)->delete();
return $this->response->setJSON(['status' => 1, 'message' => 'Supplier removed successfully.']);
}
public function savePurchase()
{
$json = $this->request->getJSON(true) ?: $this->request->getPost();

View File

@ -4021,6 +4021,26 @@ function PurchaseView({ store, notify }) {
const [invoiceNo, setInvoiceNo] = useState('');
const [invoiceDate, setInvoiceDate] = useState(new Date().toISOString().split('T')[0]);
// Supplier Add/Edit Modal State
const [showSupplierModal, setShowSupplierModal] = useState(false);
const [supplierSearch, setSupplierSearch] = useState('');
const [isSavingSupplier, setIsSavingSupplier] = useState(false);
const [supplierForm, setSupplierForm] = useState({
supplier_id: 0,
supplier_name: '',
contact_person: '',
phone: '',
email: '',
gstin: '',
pan_no: '',
dl_no_20b: '',
dl_no_21b: '',
state_code: '07',
credit_days: 30,
address: '',
bank_details: ''
});
const [rows, setRows] = useState([
{ item_name: '', generic_name: '', batch_no: '', expiry_date: '', qty_packs: 1, free_qty_packs: 0, units_per_pack: 10, mrp: 0, ptr: 0, discount_pct: 0, gst_rate: 12 }
]);
@ -4039,6 +4059,107 @@ function PurchaseView({ store, notify }) {
}
};
const openAddSupplier = () => {
setSupplierForm({
supplier_id: 0,
supplier_name: '',
contact_person: '',
phone: '',
email: '',
gstin: '',
pan_no: '',
dl_no_20b: '',
dl_no_21b: '',
state_code: '07',
credit_days: 30,
address: '',
bank_details: ''
});
setShowSupplierModal(true);
};
const openEditSupplier = (s) => {
setSupplierForm({
supplier_id: s.supplier_id,
supplier_name: s.supplier_name || '',
contact_person: s.contact_person || '',
phone: s.phone || '',
email: s.email || '',
gstin: s.gstin || '',
pan_no: s.pan_no || '',
dl_no_20b: s.dl_no_20b || '',
dl_no_21b: s.dl_no_21b || '',
state_code: s.state_code || '07',
credit_days: s.credit_days || 30,
address: s.address || '',
bank_details: s.bank_details || ''
});
setShowSupplierModal(true);
};
const handleSaveSupplier = async (e) => {
if (e) e.preventDefault();
if (!supplierForm.supplier_name.trim()) {
notify('Supplier / Distributor name is required.', 'error');
return;
}
setIsSavingSupplier(true);
try {
const res = await fetch(API_BASE + 'suppliers/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(supplierForm)
});
const data = await res.json();
if (data.status) {
notify(supplierForm.supplier_id ? 'Supplier updated successfully!' : 'New supplier added successfully!', 'success');
setShowSupplierModal(false);
await fetchSuppliers();
if (data.supplier_id) {
setSelectedSupplierId(String(data.supplier_id));
}
} else {
notify(data.message || 'Failed to save supplier.', 'error');
}
} catch (err) {
notify('Network error saving supplier: ' + err.message, 'error');
} finally {
setIsSavingSupplier(false);
}
};
const handleDeleteSupplier = async (supplierId, supplierName) => {
if (!confirm(`Are you sure you want to remove / deactivate supplier "${supplierName}"?`)) return;
try {
const res = await fetch(API_BASE + 'suppliers/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ supplier_id: supplierId })
});
const data = await res.json();
if (data.status) {
notify(data.message || 'Supplier removed successfully.', 'success');
await fetchSuppliers();
} else {
notify(data.message || 'Failed to remove supplier.', 'error');
}
} catch (err) {
notify('Error: ' + err.message, 'error');
}
};
const filteredSuppliers = suppliers.filter(s => {
if (!supplierSearch.trim()) return true;
const q = supplierSearch.toLowerCase();
return (s.supplier_name && s.supplier_name.toLowerCase().includes(q)) ||
(s.gstin && s.gstin.toLowerCase().includes(q)) ||
(s.dl_no_20b && s.dl_no_20b.toLowerCase().includes(q)) ||
(s.dl_no_21b && s.dl_no_21b.toLowerCase().includes(q)) ||
(s.phone && s.phone.toLowerCase().includes(q)) ||
(s.contact_person && s.contact_person.toLowerCase().includes(q));
});
const addRow = () => {
setRows([...rows, { item_name: '', generic_name: '', batch_no: '', expiry_date: '', qty_packs: 1, free_qty_packs: 0, units_per_pack: 10, mrp: 0, ptr: 0, discount_pct: 0, gst_rate: 12 }]);
};
@ -4122,7 +4243,7 @@ function PurchaseView({ store, notify }) {
<i className="bi bi-file-earmark-spreadsheet me-1"></i> Marg ERP / Excel Import
</button>
<button className={`btn btn-sm ${subTab === 'suppliers' ? 'btn-primary' : 'btn-outline-secondary'}`} onClick={() => setSubTab('suppliers')}>
<i className="bi bi-truck me-1"></i> Supplier Directory
<i className="bi bi-truck me-1"></i> Supplier Directory ({suppliers.length})
</button>
</div>
@ -4131,7 +4252,12 @@ function PurchaseView({ store, notify }) {
<h5 className="fw-bold mb-3"><i className="bi bi-receipt text-primary me-1"></i> Distributor Purchase Inward (Stock Entry)</h5>
<div className="row g-2 mb-3">
<div className="col-md-4">
<label className="form-label small fw-bold">Supplier / Distributor</label>
<div className="d-flex justify-content-between align-items-center mb-1">
<label className="form-label small fw-bold mb-0">Supplier / Distributor</label>
<button type="button" className="btn btn-link btn-sm p-0 text-decoration-none fw-semibold" style={{ fontSize: '11px' }} onClick={openAddSupplier}>
<i className="bi bi-plus-circle me-1"></i>+ Add New
</button>
</div>
<select className="form-select form-select-sm" value={selectedSupplierId} onChange={(e) => setSelectedSupplierId(e.target.value)}>
<option value="">-- Select Supplier --</option>
{suppliers.map(s => (
@ -4273,35 +4399,322 @@ function PurchaseView({ store, notify }) {
{subTab === 'suppliers' && (
<div className="counter-card">
<h5 className="fw-bold mb-3"><i className="bi bi-truck text-primary me-1"></i> Approved Drug Distributors &amp; Suppliers</h5>
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<div>
<h5 className="fw-bold mb-0 text-dark">
<i className="bi bi-truck text-primary me-1"></i> Approved Drug Distributors &amp; Suppliers
</h5>
<small className="text-muted">Manage licensed pharmaceutical distributors, DL Form 20B/21B, GSTIN, and credit terms</small>
</div>
<div className="d-flex gap-2">
<div className="input-group input-group-sm" style={{ width: 250 }}>
<span className="input-group-text bg-white border-end-0"><i className="bi bi-search text-muted"></i></span>
<input
type="text"
className="form-control border-start-0"
placeholder="Search distributor, GST..."
value={supplierSearch}
onChange={(e) => setSupplierSearch(e.target.value)}
/>
{supplierSearch && (
<button className="btn btn-outline-secondary" type="button" onClick={() => setSupplierSearch('')}>
<i className="bi bi-x"></i>
</button>
)}
</div>
<button className="btn btn-primary btn-sm px-3" onClick={openAddSupplier}>
<i className="bi bi-plus-circle me-1"></i> Add New Supplier
</button>
</div>
</div>
<div className="table-responsive">
<table className="table table-pos mb-0">
<table className="table table-pos align-middle mb-0">
<thead>
<tr>
<th>#</th>
<th>Supplier Name</th>
<th style={{ width: 45 }}>#</th>
<th>Distributor Name</th>
<th>DL Form 20B / 21B</th>
<th>GSTIN</th>
<th>GSTIN &amp; PAN</th>
<th>Phone / Contact</th>
<th>Credit Terms</th>
<th>Address / Bank</th>
<th style={{ width: 90 }} className="text-end pe-3">Actions</th>
</tr>
</thead>
<tbody>
{suppliers.map((s, i) => (
<tr key={s.supplier_id}>
<td>{i + 1}</td>
<td className="fw-bold">{s.supplier_name}</td>
<td>{s.dl_no_20b || 'N/A'} / {s.dl_no_21b || 'N/A'}</td>
<td><code>{s.gstin}</code></td>
<td>{s.phone} ({s.contact_person})</td>
<td>{s.credit_days} Days</td>
{filteredSuppliers.length === 0 ? (
<tr>
<td colSpan="8" className="text-center py-4 text-muted">
<i className="bi bi-truck fs-2 d-block mb-1 text-secondary"></i>
{suppliers.length === 0
? 'No distributors registered yet. Click "Add New Supplier" to create one.'
: 'No suppliers match your search query.'}
</td>
</tr>
))}
) : (
filteredSuppliers.map((s, i) => (
<tr key={s.supplier_id}>
<td className="fw-semibold text-secondary">{i + 1}</td>
<td>
<div className="fw-bold text-dark fs-6">{s.supplier_name}</div>
{s.email && <small className="text-muted"><i className="bi bi-envelope me-1"></i>{s.email}</small>}
</td>
<td>
<div className="small"><strong>20B:</strong> {s.dl_no_20b || <span className="text-muted">N/A</span>}</div>
<div className="small"><strong>21B:</strong> {s.dl_no_21b || <span className="text-muted">N/A</span>}</div>
</td>
<td>
<div><code className="text-primary fw-bold">{s.gstin || 'N/A'}</code></div>
{s.pan_no && <small className="text-muted d-block">PAN: {s.pan_no}</small>}
</td>
<td>
<div className="fw-semibold text-dark">{s.phone || 'N/A'}</div>
{s.contact_person && <small className="text-muted"><i className="bi bi-person me-1"></i>{s.contact_person}</small>}
</td>
<td>
<span className="badge bg-light text-dark border px-2 py-1">
<i className="bi bi-calendar-check me-1 text-primary"></i>{s.credit_days || 30} Days
</span>
</td>
<td style={{ maxWidth: 220 }}>
{s.address && <div className="small text-truncate" title={s.address}><i className="bi bi-geo-alt me-1 text-muted"></i>{s.address}</div>}
{s.bank_details && <div className="small text-muted text-truncate" title={s.bank_details}><i className="bi bi-bank me-1"></i>{s.bank_details}</div>}
{!s.address && !s.bank_details && <span className="text-muted small">-</span>}
</td>
<td className="text-end pe-3">
<div className="btn-group btn-group-sm">
<button
className="btn btn-outline-primary py-0.5 px-2"
title="Edit Supplier"
onClick={() => openEditSupplier(s)}
>
<i className="bi bi-pencil-square"></i>
</button>
<button
className="btn btn-outline-danger py-0.5 px-2"
title="Remove Supplier"
onClick={() => handleDeleteSupplier(s.supplier_id, s.supplier_name)}
>
<i className="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
{/* Supplier Add / Edit Modal */}
{showSupplierModal && (
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(15, 23, 42, 0.75)' }} tabIndex="-1">
<div className="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div className="modal-content border-0 shadow-lg">
<div className="modal-header bg-dark text-white py-2.5 px-3 d-flex justify-content-between align-items-center">
<div className="d-flex align-items-center gap-2">
<div className="bg-primary text-white rounded p-1.5 d-flex align-items-center justify-content-center" style={{ width: 32, height: 32 }}>
<i className="bi bi-truck fs-5"></i>
</div>
<div>
<h6 className="modal-title mb-0 fw-bold">
{supplierForm.supplier_id ? 'Edit Supplier / Distributor Details' : 'Add New Drug Distributor & Supplier'}
</h6>
<small className="text-light opacity-75">Statutory Drug Licenses, GSTIN, contact &amp; credit terms</small>
</div>
</div>
<button type="button" className="btn-close btn-close-white" onClick={() => setShowSupplierModal(false)}></button>
</div>
<form onSubmit={handleSaveSupplier}>
<div className="modal-body p-3 bg-light">
<div className="card border-0 shadow-sm p-3 mb-3 bg-white">
<h6 className="fw-bold text-primary mb-2" style={{ fontSize: '13px' }}>
<i className="bi bi-building me-1"></i> Basic Distributor Information
</h6>
<div className="row g-2">
<div className="col-md-8">
<label className="form-label small fw-bold mb-1">Distributor / Agency Name <span className="text-danger">*</span></label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. Apex Healthcare Distributors Pvt Ltd"
value={supplierForm.supplier_name}
onChange={(e) => setSupplierForm({ ...supplierForm, supplier_name: e.target.value })}
required
autoFocus
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">Contact Person</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. Ramesh Gupta"
value={supplierForm.contact_person}
onChange={(e) => setSupplierForm({ ...supplierForm, contact_person: e.target.value })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">Phone / Mobile No.</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="+91 9811002233"
value={supplierForm.phone}
onChange={(e) => setSupplierForm({ ...supplierForm, phone: e.target.value })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">Email Address</label>
<input
type="email"
className="form-control form-control-sm"
placeholder="sales@distributor.com"
value={supplierForm.email}
onChange={(e) => setSupplierForm({ ...supplierForm, email: e.target.value })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">Credit Terms</label>
<div className="input-group input-group-sm">
<input
type="number"
className="form-control form-control-sm"
min="0"
placeholder="30"
value={supplierForm.credit_days}
onChange={(e) => setSupplierForm({ ...supplierForm, credit_days: e.target.value })}
/>
<span className="input-group-text bg-light">Days</span>
</div>
</div>
</div>
</div>
<div className="card border-0 shadow-sm p-3 mb-3 bg-white">
<h6 className="fw-bold text-primary mb-2" style={{ fontSize: '13px' }}>
<i className="bi bi-shield-check me-1"></i> Indian Statutory Drug Licenses &amp; GSTIN
</h6>
<div className="row g-2">
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">Drug License Form 20-B (Allopathic)</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. DL-20B-DEL-98441"
value={supplierForm.dl_no_20b}
onChange={(e) => setSupplierForm({ ...supplierForm, dl_no_20b: e.target.value })}
/>
</div>
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">Drug License Form 21-B (Schedule C &amp; C1)</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="e.g. DL-21B-DEL-98442"
value={supplierForm.dl_no_21b}
onChange={(e) => setSupplierForm({ ...supplierForm, dl_no_21b: e.target.value })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">GSTIN (15-Digit)</label>
<input
type="text"
className="form-control form-control-sm text-uppercase"
maxLength="15"
placeholder="07AACCA1234F1Z9"
value={supplierForm.gstin}
onChange={(e) => setSupplierForm({ ...supplierForm, gstin: e.target.value.toUpperCase() })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">PAN No. (10-Digit)</label>
<input
type="text"
className="form-control form-control-sm text-uppercase"
maxLength="10"
placeholder="AACCA1234F"
value={supplierForm.pan_no}
onChange={(e) => setSupplierForm({ ...supplierForm, pan_no: e.target.value.toUpperCase() })}
/>
</div>
<div className="col-md-4">
<label className="form-label small fw-bold mb-1">State Code (GST)</label>
<input
type="text"
className="form-control form-control-sm"
placeholder="07 (Delhi)"
value={supplierForm.state_code}
onChange={(e) => setSupplierForm({ ...supplierForm, state_code: e.target.value })}
/>
</div>
</div>
</div>
<div className="card border-0 shadow-sm p-3 bg-white">
<h6 className="fw-bold text-primary mb-2" style={{ fontSize: '13px' }}>
<i className="bi bi-geo-alt me-1"></i> Address &amp; Banking Details
</h6>
<div className="row g-2">
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">Full Postal Address</label>
<textarea
className="form-control form-control-sm"
rows="2"
placeholder="Shop / Office No, Market, City, Pincode"
value={supplierForm.address}
onChange={(e) => setSupplierForm({ ...supplierForm, address: e.target.value })}
></textarea>
</div>
<div className="col-md-6">
<label className="form-label small fw-bold mb-1">Bank Account &amp; Settlement Details</label>
<textarea
className="form-control form-control-sm"
rows="2"
placeholder="Bank Name, A/c Number, IFSC Code, UPI ID"
value={supplierForm.bank_details}
onChange={(e) => setSupplierForm({ ...supplierForm, bank_details: e.target.value })}
></textarea>
</div>
</div>
</div>
</div>
<div className="modal-footer py-2 px-3 bg-white d-flex justify-content-between">
<button
type="button"
className="btn btn-outline-secondary btn-sm"
onClick={() => setShowSupplierModal(false)}
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary btn-sm px-4"
disabled={isSavingSupplier}
>
{isSavingSupplier ? (
<>
<span className="spinner-border spinner-border-sm me-1" role="status"></span>
Saving...
</>
) : (
<>
<i className="bi bi-check2-circle me-1"></i>
{supplierForm.supplier_id ? 'Update Supplier' : 'Save Supplier'}
</>
)}
</button>
</div>
</form>
</div>
</div>
</div>
)}
</div>
);
}