This comprehensive stores accounting system development guide provides a practical, industry-standard, stepwise plan for building robust applications. Whether you’re planning stores accounting system development for cartridge management or broader inventory control, this guide contains architectural guidance, data model recommendations, workflow & transaction patterns, security best practices, test & deployment guidelines, and ITIL process mapping. Learn more about MS SQL Server best practices and ASP.NET WebForms development.
Executive summary (one-line)
Build a secure, auditable n-tier WebForms application backed by MS SQL that implements a request → HOD approval → store-keeper issue/receipt workflow, integrates with your org’s identity source, and follows ITIL Service Request / Asset & Configuration Management practices.
Phase-0 — Preparations (before coding)
- Stakeholder & requirements workshop
- Participants: Employees (requestors), HODs, Store Keeper, IT ops, Procurement/Finance.
- Outcomes: functional requirements (fields on request form, approval rules), non-functional (SLA, performance), integration points (AD, HR/Payroll, Finance).
- Define acceptance criteria for MVP (see checklist below).
- Define Roles & SLAs (ITIL Service Catalogue)
- Roles: Employee, HOD, StoreKeeper, SystemAdministrator.
- Example SLAs: HOD approval <= X business hours (document this), Issue to employee within Y hours.
- Approval policies (auto-escalation rules if HOD doesn’t respond).
- Environment & toolchain setup
- Source control: Git (or TFS). Branching strategy.
- CI: Jenkins/TeamCity/Azure DevOps.
- Issue tracking: Jira/TFS.
- DB change management: migrations/SQL script repo (versioned).
- Test environments: Dev, QA/UAT, Staging, Prod.
Phase-1 — Stores Accounting System Development Architecture & Design
- N-tier architecture
- Presentation: ASP.NET WebForms (UI pages, master pages).
- Business Logic Layer (BLL): C# class library implementing workflows, validation, authorization.
- Data Access Layer (DAL): parameterized stored procedures or ADO.NET repository (or EF if preferred).
- Integration Layer: connectors for AD/HR/Finance (Web Services/REST).
- Persistence: MS SQL Server (schemas, backups).
- Monitoring/Logging: ELMAH/log4net + SQL / file logs.
- Non-functional
- TLS everywhere (HTTPS).
- Least Privilege for DB accounts.
- Performance: paging for grids, caching static lookups, optimized indexes.
- Scalability: design for multiple web servers behind load balancer.
- Deployment
- Application config transforms per environment.
- DB migration process + rollback scripts.
- Runbooks for deployments with approvals (ITIL Change Management).
Phase-2 — Data model (MS SQL) — design & implementation
Design for 3NF where practical; use surrogate integer keys for performance. Use ROWVERSION
(timestamp) for optimistic concurrency.
Core entities (logical)
- Users (UserId, ADUserName, DisplayName, Email, DepartmentId, RoleId(s))
- Departments (DepartmentId, Name, ManagerUserId)
- Items (ItemId, SKU, Name, Description, Unit, ReorderLevel, SupplierId, IsActive)
- Inventory (InventoryId, ItemId, LocationId, Quantity, RowVersion, LastUpdated)
- Requests (RequestId, RequestNumber, RequestedByUserId, DepartmentId, Status, CreatedAt, UpdatedAt, HODUserId, HODDecisionAt, Comments)
- RequestItems (RequestItemId, RequestId, ItemId, QuantityRequested, QuantityIssued, QuantityReceived)
- IssueRecords (IssueId, RequestId, IssuedByUserId, IssuedAt, Remarks)
- ReceiptRecords (ReceiptId, ItemId, Quantity, ReceivedByUserId, ReceivedAt, RelatedDocument)
- AuditLog (AuditId, EntityType, EntityId, Action, OldValue, NewValue, ChangedBy, ChangedAt)
Suggested columns & types (example)
Users.UserId INT IDENTITY(1,1) PRIMARY KEY
Items.ItemId INT IDENTITY, SKU NVARCHAR(50) UNIQUE, Name NVARCHAR(200), ReorderLevel INT, IsActive BIT
Inventory.Quantity INT CHECK (Quantity >= 0), RowVersion ROWVERSION
Constraints & indexes
- FK constraints for data integrity.
- CHECK constraints for numeric ranges.
- Indexes:
- Items: (SKU) unique
- Inventory: (ItemId), covering index on (ItemId, Quantity)
- Requests: (RequestedByUserId), (Status), (CreatedAt)
- Use filtered indexes for active items.
Data access pattern
- Create stored procedures for critical operations (CreateRequest, ApproveRequest, IssueItems, ReceiveItems, AdjustInventory).
- Use parameterized queries for everything.
Phase 3 — Workflow & state machine
- Define statuses
- e.g.
Draft (0)
,PendingApproval (1)
,Approved (2)
,Rejected (3)
,Issued (4)
,Received (5)
,Closed (6)
.
- e.g.
- State transitions
- Employee → Submit → PendingApproval
- HOD → Approve → Approved (or Reject → Rejected)
- On Approved → notify StoreKeeper (creates fulfillment task)
- StoreKeeper → Issue → Issued (decrement inventory)
- Employee → Confirm receipt (or StoreKeeper records Receipt) → Received → Closed
- Audit trail
- Every transition writes an AuditLog entry with who/when/why.
- Escalations
- Schedule a background job (SQL Agent job or Windows service) to find
PendingApproval
older than SLA and escalate/notify.
- Schedule a background job (SQL Agent job or Windows service) to find
Phase 4 — Concurrency, transactions & inventory integrity
Inventory updates must be atomic and prevent oversell.
Safe pattern (T-SQL pseudo code):
BEGIN TRAN;
-- atomically decrement if enough stock
UPDATE Inventory
SET Quantity = Quantity - @qtyRequested
WHERE ItemId = @itemId AND Quantity >= @qtyRequested;
IF @@ROWCOUNT = 0
BEGIN
ROLLBACK TRAN;
-- return error: insufficient stock
END
ELSE
BEGIN
-- insert IssueRecords, update RequestItems (QuantityIssued), insert AuditLog
COMMIT TRAN;
END
Alternatives:
- Use
sp_getapplock
to serialize access per ItemId for high contention scenarios. - Use
ROWVERSION
optimistic concurrency when updates are initiated from application, and handle conflict retries.
Set transaction isolation appropriately; prefer short transactions. Consider READ COMMITTED SNAPSHOT isolation to reduce blocking.
Phase-5 — Stored procedures & DAL guidance
- Stored procedures for business-critical work
usp_CreateRequest
,usp_ApproveRequest
,usp_RejectRequest
,usp_IssueRequestItems
,usp_RecordReceipt
,usp_AdjustInventory
.
- Return codes / structured results
- Use output parameters for result codes and messages.
- Error handling
- Use TRY/CATCH in T-SQL and return meaningful error codes/messages to BLL.
- DAL
- Implement repository pattern + unit of work.
- Use parameterized SqlCommand and
using
blocks for connection disposal.
Phase-6 — ASP.NET WebForms (presentation layer)
- UI pages (minimum MVP)
- Login (AD/Forms)
- Request Create/Edit (Employee)
- My Requests (employee view) — GridView with paging and filters
- Approval Queue (HOD) — bulk approve/reject
- Store Keeper Dashboard — Approved requests to fulfill
- Inventory Management — view/edit stock levels, reorder points
- Audit & Reports — admin
- UX best practices
- Use MasterPages for consistent layout.
- Use server side validation + client side (Validators + jQuery).
- Prefer minimal ViewState (disable where not needed).
- Use UpdatePanel sparingly — prefer WebMethods/AJAX for lightweight calls.
- Security in WebForms
- Use Windows Auth / AD integration where possible (FormsAuth with AD fallback).
- Map AD groups to application roles.
- Protect pages with
<location>
or role checks in code behind. - Anti-CSRF: set
Page.ViewStateUserKey = User.Identity.Name
and validate. - Use input sanitization and HttpUtility.HtmlEncode for output to prevent XSS.
Phase-7 — Authentication & Authorization
- Authentication
- Enterprise: integrate with Active Directory (Windows Auth or LDAP/Forms + AD).
- Else: use ASP.NET Membership (but store credentials securely).
- Authorization
- Role-based access control (RBAC).
- Fine-grained permission checks in BLL (e.g.,
CanIssue(RequestId, userId)
).
- Secrets & credentials
- Do not store connection strings with plain text passwords in source control.
- Use secure config transforms and environment secrets stores.
Phase-8 — Auditing, logging & monitoring
- Audit requirements
- Record any create/modify/approve/issue/receive actions with user, timestamp, IP, old/new values.
- Store audit records in a write-optimized table (separate schema).
- Application logging
- ELMAH for uncaught exceptions; log4net or Serilog for structured logs with correlation IDs.
- Monitoring
- Capture metrics: requests per day, average approval time, stockouts, reorder triggers.
- Use alerts for low inventory, failed jobs, or high error rates.
- Retention & GDPR
- Define retention policies for logs/audit data.
Phase-9 — Reporting & KPIs
Deliver standard reports:
- Current stock by item & location
- Reorder alerts
- Pending approvals and age
- Request fulfillment SLA compliance
- Issue/receipt history by user/department
Design reports as stored procedures/views for performance; exportable to Excel.
Phase 10 — Testing strategy
- Unit tests for BLL methods (MSTest/NUnit).
- Integration tests for DAL (use test DB).
- UI tests (Selenium) for core user journeys.
- Load testing (JMeter) for peak usage.
- Security testing: SQL injection, XSS, CSRF, authentication attempts.
- UAT: HODs, StoreKeeper, Employees run acceptance scripts.
Create test cases for each user story and acceptance criteria.
Phase-11 — Deployment, change & release management (ITIL)
- Change Management
- All changes go through a Change Request with CAB approval for prod deployments.
- Release Plan
- Maintain release notes, DB migration scripts, rollback plan.
- Environments
- Separate configs and secrets for Dev/QA/Staging/Prod.
- Backups & DR
- Full DB nightly, transaction log backups every X minutes depending on RPO.
- Test restore procedures quarterly.
Phase-12 — Operations & ITIL process mapping
- Service Request Fulfillment
- Define request model for cartridges as a Service Request item in Service Catalogue.
- Automate fulfillment workflow as implemented in app.
- Configuration Management (CMDB)
- Record application components, DB versions, server details, service accounts.
- Track item SKUs & supplier details as CMDB items if required.
- Incident & Problem Management
- Define incident severity, logging process, escalation contacts.
- Continual Service Improvement
- KPIs review monthly: request fulfillment time, rejection rate, stockout incidents.
Phase-13 — Security & compliance checklist
- HTTPS enforced.
- SQL parameterization and least-privilege DB accounts.
- Passwords hashed (PBKDF2 / Rfc2898DeriveBytes) if storing locally.
- Regular vulnerability scans and pentests.
- Audit log immutable / write-only for critical events.
- Data retention & privacy compliance (GDPR etc.) — purge old personal data per policy.
Phase-14 — Deliverables & MVP acceptance checklist
MVP must include:
- Employee can submit request (one or multiple items).
- HOD can view and Approve/Reject (single and bulk).
- StoreKeeper sees approved requests and can Issue items.
- Inventory is decremented atomically on Issue; Quantity cannot go negative.
- StoreKeeper can Record Receipt (incoming stock) to increase inventory.
- Audit log of all transitions.
- Basic reports: current stock, pending approvals.
- Authentication & role-based authorization.
- Deployment scripts for DB and app.
Phase 15 — Optional integrations & enhancements (post-MVP)
- Auto-reorder integration with Procurement (PO creation).
- Chargeback to cost centers (Finance integration).
- Mobile friendly UI or mobile app.
- Barcode scanning for faster issuance/receipt.
Minimal example: SQL pattern for issuing items (safe transaction)
-- Sample pattern to atomically issue single item
CREATE PROCEDURE dbo.usp_IssueItem
@RequestItemId INT,
@IssuedByUserId INT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRAN;
BEGIN TRY
DECLARE @ItemId INT, @Qty INT, @RequestId INT;
SELECT @RequestId = RequestId, @ItemId = ItemId, @Qty = QuantityRequested
FROM RequestItems WHERE RequestItemId = @RequestItemId;
UPDATE Inventory
SET Quantity = Quantity - @Qty, LastUpdated = SYSUTCDATETIME()
WHERE ItemId = @ItemId AND Quantity >= @Qty;
IF @@ROWCOUNT = 0
BEGIN
RAISERROR('Insufficient stock for ItemId %d', 16, 1, @ItemId);
ROLLBACK TRAN;
RETURN;
END
-- record issue and update request item
INSERT INTO IssueRecords (RequestId, RequestItemId, IssuedByUserId, IssuedAt, Quantity)
VALUES (@RequestId, @RequestItemId, @IssuedByUserId, SYSUTCDATETIME(), @Qty);
UPDATE RequestItems SET QuantityIssued = @Qty WHERE RequestItemId = @RequestItemId;
-- audit log entry (simplified)
INSERT INTO AuditLog (EntityType, EntityId, Action, ChangedBy, ChangedAt)
VALUES ('RequestItem', @RequestItemId, 'Issued', @IssuedByUserId, SYSUTCDATETIME());
COMMIT TRAN;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRAN;
DECLARE @ErrMsg NVARCHAR(4000) = ERROR_MESSAGE();
RAISERROR(@ErrMsg, 16, 1);
END CATCH
END
(adapt to multi-item requests by looping or issuing per item within same transaction if you prefer atomicity across whole request)
Checklist for going live (operations)
- Backup & restore tested.
- Load test done with expected peak.
- Security scan & fixes applied.
- Monitoring/alerts configured.
- Runbook available for inventory reconciliation, stockouts, critical failures.
- User training completed (Store Keeper, HOD, Employees).
If you want I can next provide one of the following (pick one):
- full MS SQL DDL (tables, FK, indexes, constraints) + basic seed data for Items/Departments;
- stored procedures for the core flows (
CreateRequest
,ApproveRequest
,IssueItems
,ReceiveItems
); - skeleton WebForms project structure with page list and sample code-behind for the Request Create and Approval pages;
- detailed test cases/UAT scripts for the MVP.
Tell me which of the four you want and I will generate it immediately.
Here is a complete MS SQL DDL script for your Stores Accounting System (Printer Cartridge Management MVP).
It includes tables, constraints, indexes, and seed data for Departments
, Users
, Items
, and Inventory
.
📜 MS SQL DDL — Stores Accounting System
-- ================================================
-- Database: StoresAccountingSystem
-- Author: Your Company
-- Purpose: Printer Cartridge Management MVP
-- ================================================
----------------------------------------------------
-- 0. Create Database (optional)
----------------------------------------------------
IF DB_ID('StoresAccountingSystem') IS NULL
CREATE DATABASE StoresAccountingSystem;
GO
USE StoresAccountingSystem;
GO
----------------------------------------------------
-- 1. Lookup Tables
----------------------------------------------------
-- Departments
CREATE TABLE dbo.Departments (
DepartmentId INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(200) NOT NULL UNIQUE,
ManagerUserId INT NULL
);
GO
-- Roles
CREATE TABLE dbo.Roles (
RoleId INT IDENTITY(1,1) PRIMARY KEY,
RoleName NVARCHAR(100) NOT NULL UNIQUE
);
GO
----------------------------------------------------
-- 2. Core Tables
----------------------------------------------------
-- Users
CREATE TABLE dbo.Users (
UserId INT IDENTITY(1,1) PRIMARY KEY,
ADUserName NVARCHAR(100) NOT NULL UNIQUE,
DisplayName NVARCHAR(200) NOT NULL,
Email NVARCHAR(200) NULL,
DepartmentId INT NOT NULL,
RoleId INT NOT NULL,
IsActive BIT NOT NULL DEFAULT (1),
CreatedAt DATETIME2 NOT NULL DEFAULT (SYSUTCDATETIME())
);
GO
ALTER TABLE dbo.Users
ADD CONSTRAINT FK_Users_Departments FOREIGN KEY (DepartmentId)
REFERENCES dbo.Departments (DepartmentId);
ALTER TABLE dbo.Users
ADD CONSTRAINT FK_Users_Roles FOREIGN KEY (RoleId)
REFERENCES dbo.Roles (RoleId);
GO
-- Items (e.g., Printer Cartridges)
CREATE TABLE dbo.Items (
ItemId INT IDENTITY(1,1) PRIMARY KEY,
SKU NVARCHAR(50) NOT NULL UNIQUE,
Name NVARCHAR(200) NOT NULL,
Description NVARCHAR(500) NULL,
Unit NVARCHAR(50) NOT NULL DEFAULT ('pcs'),
ReorderLevel INT NOT NULL DEFAULT (5),
IsActive BIT NOT NULL DEFAULT (1),
CreatedAt DATETIME2 NOT NULL DEFAULT (SYSUTCDATETIME())
);
GO
-- Inventory
CREATE TABLE dbo.Inventory (
InventoryId INT IDENTITY(1,1) PRIMARY KEY,
ItemId INT NOT NULL,
Quantity INT NOT NULL CHECK (Quantity >= 0),
LastUpdated DATETIME2 NOT NULL DEFAULT (SYSUTCDATETIME()),
RowVersion ROWVERSION
);
GO
ALTER TABLE dbo.Inventory
ADD CONSTRAINT FK_Inventory_Items FOREIGN KEY (ItemId)
REFERENCES dbo.Items (ItemId);
GO
-- Requests
CREATE TABLE dbo.Requests (
RequestId INT IDENTITY(1,1) PRIMARY KEY,
RequestNumber NVARCHAR(50) NOT NULL UNIQUE,
RequestedByUserId INT NOT NULL,
DepartmentId INT NOT NULL,
Status INT NOT NULL, -- 0=Draft,1=Pending,2=Approved,3=Rejected,4=Issued,5=Received,6=Closed
HODUserId INT NULL,
CreatedAt DATETIME2 NOT NULL DEFAULT (SYSUTCDATETIME()),
UpdatedAt DATETIME2 NULL,
Comments NVARCHAR(500) NULL
);
GO
ALTER TABLE dbo.Requests
ADD CONSTRAINT FK_Requests_Users FOREIGN KEY (RequestedByUserId)
REFERENCES dbo.Users (UserId);
ALTER TABLE dbo.Requests
ADD CONSTRAINT FK_Requests_Departments FOREIGN KEY (DepartmentId)
REFERENCES dbo.Departments (DepartmentId);
ALTER TABLE dbo.Requests
ADD CONSTRAINT FK_Requests_HOD FOREIGN KEY (HODUserId)
REFERENCES dbo.Users (UserId);
GO
-- Request Items
CREATE TABLE dbo.RequestItems (
RequestItemId INT IDENTITY(1,1) PRIMARY KEY,
RequestId INT NOT NULL,
ItemId INT NOT NULL,
QuantityRequested INT NOT NULL CHECK (QuantityRequested > 0),
QuantityIssued INT NOT NULL DEFAULT (0),
QuantityReceived INT NOT NULL DEFAULT (0)
);
GO
ALTER TABLE dbo.RequestItems
ADD CONSTRAINT FK_RequestItems_Requests FOREIGN KEY (RequestId)
REFERENCES dbo.Requests (RequestId);
ALTER TABLE dbo.RequestItems
ADD CONSTRAINT FK_RequestItems_Items FOREIGN KEY (ItemId)
REFERENCES dbo.Items (ItemId);
GO
-- Issue Records
CREATE TABLE dbo.IssueRecords (
IssueId INT IDENTITY(1,1) PRIMARY KEY,
RequestId INT NOT NULL,
IssuedByUserId INT NOT NULL,
IssuedAt DATETIME2 NOT NULL DEFAULT (SYSUTCDATETIME()),
Remarks NVARCHAR(500) NULL
);
GO
ALTER TABLE dbo.IssueRecords
ADD CONSTRAINT FK_IssueRecords_Requests FOREIGN KEY (RequestId)
REFERENCES dbo.Requests (RequestId);
ALTER TABLE dbo.IssueRecords
ADD CONSTRAINT FK_IssueRecords_Users FOREIGN KEY (IssuedByUserId)
REFERENCES dbo.Users (UserId);
GO
-- Receipt Records (Stock Receipts)
CREATE TABLE dbo.ReceiptRecords (
ReceiptId INT IDENTITY(1,1) PRIMARY KEY,
ItemId INT NOT NULL,
Quantity INT NOT NULL CHECK (Quantity > 0),
ReceivedByUserId INT NOT NULL,
ReceivedAt DATETIME2 NOT NULL DEFAULT (SYSUTCDATETIME()),
RelatedDocument NVARCHAR(200) NULL
);
GO
ALTER TABLE dbo.ReceiptRecords
ADD CONSTRAINT FK_ReceiptRecords_Items FOREIGN KEY (ItemId)
REFERENCES dbo.Items (ItemId);
ALTER TABLE dbo.ReceiptRecords
ADD CONSTRAINT FK_ReceiptRecords_Users FOREIGN KEY (ReceivedByUserId)
REFERENCES dbo.Users (UserId);
GO
-- Audit Log
CREATE TABLE dbo.AuditLog (
AuditId BIGINT IDENTITY(1,1) PRIMARY KEY,
EntityType NVARCHAR(100) NOT NULL,
EntityId INT NOT NULL,
Action NVARCHAR(100) NOT NULL,
OldValue NVARCHAR(MAX) NULL,
NewValue NVARCHAR(MAX) NULL,
ChangedBy INT NOT NULL,
ChangedAt DATETIME2 NOT NULL DEFAULT (SYSUTCDATETIME())
);
GO
ALTER TABLE dbo.AuditLog
ADD CONSTRAINT FK_AuditLog_Users FOREIGN KEY (ChangedBy)
REFERENCES dbo.Users (UserId);
GO
----------------------------------------------------
-- 3. Indexes
----------------------------------------------------
CREATE INDEX IX_Inventory_ItemId ON dbo.Inventory(ItemId);
CREATE INDEX IX_Requests_Status ON dbo.Requests(Status);
CREATE INDEX IX_RequestItems_RequestId ON dbo.RequestItems(RequestId);
CREATE INDEX IX_RequestItems_ItemId ON dbo.RequestItems(ItemId);
GO
----------------------------------------------------
-- 4. Seed Data
----------------------------------------------------
-- Roles
INSERT INTO dbo.Roles (RoleName)
VALUES ('Employee'), ('HOD'), ('StoreKeeper'), ('SystemAdministrator');
GO
-- Departments
INSERT INTO dbo.Departments (Name)
VALUES ('IT'), ('Finance'), ('HR'), ('Procurement');
GO
-- Users (sample; adjust ADUserName as per environment)
INSERT INTO dbo.Users (ADUserName, DisplayName, Email, DepartmentId, RoleId)
VALUES
('jdoe', 'John Doe', '[email protected]', 1, 1), -- Employee IT
('asmith', 'Alice Smith', '[email protected]', 1, 2), -- HOD IT
('skhan', 'Sajid Khan', '[email protected]', 4, 3); -- Store Keeper Procurement
GO
-- Items (Cartridges)
INSERT INTO dbo.Items (SKU, Name, Description, Unit, ReorderLevel)
VALUES
('HP-CRG-12A', 'HP 12A Black Cartridge', 'HP LaserJet 1010/1020/3050 series', 'pcs', 5),
('CAN-CRG-337', 'Canon 337 Black Cartridge', 'Canon MF211/221 series', 'pcs', 3),
('EP-CRG-303', 'Epson 303 Black Cartridge', 'Epson XP-4100 series', 'pcs', 4);
GO
-- Inventory (Initial Stock)
INSERT INTO dbo.Inventory (ItemId, Quantity)
SELECT ItemId, 10 FROM dbo.Items;
GO
✅ This script sets up:
- Core entities: Users, Departments, Roles, Items, Inventory, Requests, RequestItems.
- Workflow support: Request approvals, issues, receipts.
- Audit trail: Captures all entity changes.
- Seed data: Some departments, roles, users, and cartridge SKUs with starting stock.
Would you like me to add the stored procedures (usp_CreateRequest
, usp_ApproveRequest
, usp_IssueItems
, usp_RecordReceipt
) right after this so you have both schema + workflows ready to execute?
Perfect 👍 Here are the core stored procedures for your Stores Accounting System (aligned with the schema I gave you).
These cover the full workflow: Create Request → Approve/Reject → Issue Items → Record Receipts.
📜 Stored Procedures — Stores Accounting System
USE StoresAccountingSystem;
GO
----------------------------------------------------
-- 1. usp_CreateRequest
-- Creates a request and inserts requested items.
----------------------------------------------------
CREATE PROCEDURE dbo.usp_CreateRequest
@RequestedByUserId INT,
@DepartmentId INT,
@HODUserId INT,
@Items dbo.RequestItemTableType READONLY -- user-defined table type: (ItemId INT, QuantityRequested INT)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @RequestId INT;
DECLARE @RequestNumber NVARCHAR(50);
BEGIN TRAN;
BEGIN TRY
-- generate request number
SET @RequestNumber = 'REQ-' + CONVERT(NVARCHAR(20), SYSDATETIME(), 112) + '-' + CAST(NEWID() AS NVARCHAR(36));
-- insert into Requests
INSERT INTO dbo.Requests (RequestNumber, RequestedByUserId, DepartmentId, Status, HODUserId)
VALUES (@RequestNumber, @RequestedByUserId, @DepartmentId, 1, @HODUserId); -- Status=1 PendingApproval
SET @RequestId = SCOPE_IDENTITY();
-- insert requested items
INSERT INTO dbo.RequestItems (RequestId, ItemId, QuantityRequested)
SELECT @RequestId, ItemId, QuantityRequested
FROM @Items;
-- audit log
INSERT INTO dbo.AuditLog (EntityType, EntityId, Action, NewValue, ChangedBy)
VALUES ('Request', @RequestId, 'Created', @RequestNumber, @RequestedByUserId);
COMMIT TRAN;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRAN;
THROW;
END CATCH
END;
GO
----------------------------------------------------
-- 2. usp_ApproveRequest
-- HOD approves or rejects a request.
----------------------------------------------------
CREATE PROCEDURE dbo.usp_ApproveRequest
@RequestId INT,
@HODUserId INT,
@Decision BIT, -- 1=Approve, 0=Reject
@Comments NVARCHAR(500) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Status INT;
IF @Decision = 1 SET @Status = 2; ELSE SET @Status = 3;
UPDATE dbo.Requests
SET Status = @Status,
HODUserId = @HODUserId,
UpdatedAt = SYSUTCDATETIME(),
Comments = @Comments
WHERE RequestId = @RequestId AND Status = 1; -- only if PendingApproval
IF @@ROWCOUNT = 0
THROW 50001, 'Request not found or not pending approval.', 1;
-- audit log
INSERT INTO dbo.AuditLog (EntityType, EntityId, Action, NewValue, ChangedBy)
VALUES ('Request', @RequestId, CASE WHEN @Decision=1 THEN 'Approved' ELSE 'Rejected' END, @Comments, @HODUserId);
END;
GO
----------------------------------------------------
-- 3. usp_IssueItems
-- StoreKeeper issues items for an approved request.
----------------------------------------------------
CREATE PROCEDURE dbo.usp_IssueItems
@RequestId INT,
@IssuedByUserId INT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRAN;
BEGIN TRY
-- check status
IF NOT EXISTS (SELECT 1 FROM dbo.Requests WHERE RequestId=@RequestId AND Status=2)
THROW 50002, 'Request is not approved for issue.', 1;
-- cursor through items (or you can do set-based)
DECLARE cur CURSOR FOR
SELECT RequestItemId, ItemId, QuantityRequested
FROM dbo.RequestItems WHERE RequestId = @RequestId;
DECLARE @RequestItemId INT, @ItemId INT, @Qty INT;
OPEN cur;
FETCH NEXT FROM cur INTO @RequestItemId, @ItemId, @Qty;
WHILE @@FETCH_STATUS = 0
BEGIN
-- decrement stock if enough
UPDATE dbo.Inventory
SET Quantity = Quantity - @Qty, LastUpdated = SYSUTCDATETIME()
WHERE ItemId=@ItemId AND Quantity >= @Qty;
IF @@ROWCOUNT = 0
THROW 50003, 'Insufficient stock for requested item.', 1;
-- update request items
UPDATE dbo.RequestItems
SET QuantityIssued = @Qty
WHERE RequestItemId=@RequestItemId;
-- audit log
INSERT INTO dbo.AuditLog (EntityType, EntityId, Action, NewValue, ChangedBy)
VALUES ('RequestItem', @RequestItemId, 'Issued', CAST(@Qty AS NVARCHAR(50)), @IssuedByUserId);
FETCH NEXT FROM cur INTO @RequestItemId, @ItemId, @Qty;
END
CLOSE cur; DEALLOCATE cur;
-- update request status
UPDATE dbo.Requests
SET Status=4, UpdatedAt=SYSUTCDATETIME()
WHERE RequestId=@RequestId;
-- insert issue record
INSERT INTO dbo.IssueRecords (RequestId, IssuedByUserId)
VALUES (@RequestId, @IssuedByUserId);
COMMIT TRAN;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRAN;
CLOSE cur; DEALLOCATE cur;
THROW;
END CATCH
END;
GO
----------------------------------------------------
-- 4. usp_RecordReceipt
-- StoreKeeper records new stock received into inventory.
----------------------------------------------------
CREATE PROCEDURE dbo.usp_RecordReceipt
@ItemId INT,
@Quantity INT,
@ReceivedByUserId INT,
@RelatedDocument NVARCHAR(200) = NULL
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRAN;
BEGIN TRY
-- update inventory
UPDATE dbo.Inventory
SET Quantity = Quantity + @Quantity,
LastUpdated = SYSUTCDATETIME()
WHERE ItemId = @ItemId;
IF @@ROWCOUNT = 0
THROW 50004, 'Invalid ItemId.', 1;
-- record receipt
INSERT INTO dbo.ReceiptRecords (ItemId, Quantity, ReceivedByUserId, RelatedDocument)
VALUES (@ItemId, @Quantity, @ReceivedByUserId, @RelatedDocument);
-- audit log
INSERT INTO dbo.AuditLog (EntityType, EntityId, Action, NewValue, ChangedBy)
VALUES ('Inventory', @ItemId, 'Receipt', CAST(@Quantity AS NVARCHAR(50)), @ReceivedByUserId);
COMMIT TRAN;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRAN;
THROW;
END CATCH
END;
GO
📦 Supporting Type
To make usp_CreateRequest
work with multiple items, you need a User-Defined Table Type:
CREATE TYPE dbo.RequestItemTableType AS TABLE
(
ItemId INT NOT NULL,
QuantityRequested INT NOT NULL CHECK (QuantityRequested > 0)
);
GO
✅ Usage Examples
-- 1. Employee creates request for 2 cartridges
DECLARE @items dbo.RequestItemTableType;
INSERT INTO @items (ItemId, QuantityRequested)
VALUES (1, 2), (2, 1);
EXEC dbo.usp_CreateRequest
@RequestedByUserId=1,
@DepartmentId=1,
@HODUserId=2,
@Items=@items;
-- 2. HOD approves request
EXEC dbo.usp_ApproveRequest
@RequestId=1,
@HODUserId=2,
@Decision=1,
@Comments='Approved for IT printers';
-- 3. StoreKeeper issues items
EXEC dbo.usp_IssueItems
@RequestId=1,
@IssuedByUserId=3;
-- 4. StoreKeeper records new stock receipt
EXEC dbo.usp_RecordReceipt
@ItemId=1,
@Quantity=20,
@ReceivedByUserId=3,
@RelatedDocument='PO1234';
This gives you a fully working schema + workflows.
Would you like me to now generate sample ASP.NET WebForms (C#) code-behind for the Employee Request Page (with GridView + Add Items + Submit button) so you can test end-to-end?
Great 🚀 Let’s wire this into ASP.NET WebForms (C#).
Below is a sample WebForms page for Employee Request Creation that:
- Lets the Employee select items & quantity (via DropDownList + TextBox).
- Shows current selections in a GridView.
- Submits the request using the
usp_CreateRequest
stored procedure.
📄 EmployeeRequest.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="EmployeeRequest.aspx.cs" Inherits="StoresAccountingSystem.EmployeeRequest" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title>Create Cartridge Request</title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager runat="server" ID="sm1" />
<div>
<h2>Create Printer Cartridge Request</h2>
<!-- Item selection -->
<asp:DropDownList ID="ddlItems" runat="server" DataTextField="Name" DataValueField="ItemId" />
<asp:TextBox ID="txtQty" runat="server" Width="50" />
<asp:Button ID="btnAdd" runat="server" Text="Add Item" OnClick="btnAdd_Click" />
<hr />
<!-- Requested items grid -->
<asp:GridView ID="gvItems" runat="server" AutoGenerateColumns="false" ShowHeader="true">
<Columns>
<asp:BoundField DataField="ItemId" HeaderText="ItemId" />
<asp:BoundField DataField="ItemName" HeaderText="Item Name" />
<asp:BoundField DataField="QuantityRequested" HeaderText="Quantity" />
</Columns>
</asp:GridView>
<hr />
<!-- Submit button -->
<asp:Button ID="btnSubmit" runat="server" Text="Submit Request" OnClick="btnSubmit_Click" />
<asp:Label ID="lblMessage" runat="server" ForeColor="Green" />
</div>
</form>
</body>
</html>
📄 EmployeeRequest.aspx.cs (Code-behind)
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
namespace StoresAccountingSystem
{
public partial class EmployeeRequest : System.Web.UI.Page
{
private DataTable ItemsTable
{
get
{
if (ViewState["ItemsTable"] == null)
{
DataTable dt = new DataTable();
dt.Columns.Add("ItemId", typeof(int));
dt.Columns.Add("ItemName", typeof(string));
dt.Columns.Add("QuantityRequested", typeof(int));
ViewState["ItemsTable"] = dt;
}
return (DataTable)ViewState["ItemsTable"];
}
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
LoadItems();
}
private void LoadItems()
{
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("SELECT ItemId, Name FROM dbo.Items WHERE IsActive=1", conn))
{
conn.Open();
ddlItems.DataSource = cmd.ExecuteReader();
ddlItems.DataBind();
}
}
protected void btnAdd_Click(object sender, EventArgs e)
{
int itemId = int.Parse(ddlItems.SelectedValue);
string itemName = ddlItems.SelectedItem.Text;
int qty = int.Parse(txtQty.Text);
ItemsTable.Rows.Add(itemId, itemName, qty);
gvItems.DataSource = ItemsTable;
gvItems.DataBind();
txtQty.Text = "";
}
protected void btnSubmit_Click(object sender, EventArgs e)
{
if (ItemsTable.Rows.Count == 0)
{
lblMessage.Text = "Please add at least one item.";
lblMessage.ForeColor = System.Drawing.Color.Red;
return;
}
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("dbo.usp_CreateRequest", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
// Hard-coded for demo; in real app use logged-in user context
int employeeId = 1; // John Doe
int deptId = 1; // IT
int hodId = 2; // Alice Smith
cmd.Parameters.AddWithValue("@RequestedByUserId", employeeId);
cmd.Parameters.AddWithValue("@DepartmentId", deptId);
cmd.Parameters.AddWithValue("@HODUserId", hodId);
// Structured parameter for RequestItemTableType
SqlParameter tvpParam = cmd.Parameters.AddWithValue("@Items", ItemsTable);
tvpParam.SqlDbType = SqlDbType.Structured;
tvpParam.TypeName = "dbo.RequestItemTableType";
conn.Open();
cmd.ExecuteNonQuery();
}
ItemsTable.Clear();
gvItems.DataSource = ItemsTable;
gvItems.DataBind();
lblMessage.Text = "Request submitted successfully!";
lblMessage.ForeColor = System.Drawing.Color.Green;
}
}
}
📦 Web.config Connection String
<configuration>
<connectionStrings>
<add name="StoresDb"
connectionString="Server=YOUR_SQL_SERVER;Database=StoresAccountingSystem;Integrated Security=True;"
providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>
✅ With this page:
- Employee picks items + qty → they appear in GridView.
- On submit, the system calls
usp_CreateRequest
and inserts both request + items. - You can then approve/issue/receive using the other stored procs.
Would you like me to also provide the HOD Approval Page (show pending requests in a GridView with Approve/Reject buttons)?
Perfect 👍 Let’s build the HOD Approval Page.
This page will let a Head of Department (HOD) view all pending requests for their department and either approve or reject them. It uses the stored procedure usp_ApproveRequest
.
📄 HODApproval.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="HODApproval.aspx.cs" Inherits="StoresAccountingSystem.HODApproval" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title>HOD Approval Dashboard</title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager runat="server" ID="sm1" />
<div>
<h2>Pending Requests for Approval</h2>
<asp:GridView ID="gvRequests" runat="server" AutoGenerateColumns="False" DataKeyNames="RequestId" OnRowCommand="gvRequests_RowCommand">
<Columns>
<asp:BoundField DataField="RequestId" HeaderText="Request ID" />
<asp:BoundField DataField="RequestNumber" HeaderText="Request Number" />
<asp:BoundField DataField="RequestedBy" HeaderText="Requested By" />
<asp:BoundField DataField="CreatedAt" HeaderText="Created At" DataFormatString="{0:yyyy-MM-dd HH:mm}" />
<asp:TemplateField HeaderText="Actions">
<ItemTemplate>
<asp:Button ID="btnApprove" runat="server" Text="Approve" CommandName="Approve" CommandArgument='<%# Eval("RequestId") %>' />
<asp:Button ID="btnReject" runat="server" Text="Reject" CommandName="Reject" CommandArgument='<%# Eval("RequestId") %>' />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
<hr />
<asp:Label ID="lblMessage" runat="server" ForeColor="Green" />
</div>
</form>
</body>
</html>
📄 HODApproval.aspx.cs (Code-behind)
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
namespace StoresAccountingSystem
{
public partial class HODApproval : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
LoadPendingRequests();
}
private void LoadPendingRequests()
{
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand(@"
SELECT r.RequestId, r.RequestNumber, u.DisplayName AS RequestedBy, r.CreatedAt
FROM dbo.Requests r
INNER JOIN dbo.Users u ON r.RequestedByUserId = u.UserId
WHERE r.Status = 1 -- PendingApproval
", conn))
{
conn.Open();
using (SqlDataReader rdr = cmd.ExecuteReader())
{
DataTable dt = new DataTable();
dt.Load(rdr);
gvRequests.DataSource = dt;
gvRequests.DataBind();
}
}
}
protected void gvRequests_RowCommand(object sender, System.Web.UI.WebControls.GridViewCommandEventArgs e)
{
if (e.CommandName == "Approve" || e.CommandName == "Reject")
{
int requestId = Convert.ToInt32(e.CommandArgument);
bool decision = (e.CommandName == "Approve");
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("dbo.usp_ApproveRequest", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
int hodUserId = 2; // Example: logged-in HOD userId (Alice Smith)
cmd.Parameters.AddWithValue("@RequestId", requestId);
cmd.Parameters.AddWithValue("@HODUserId", hodUserId);
cmd.Parameters.AddWithValue("@Decision", decision);
cmd.Parameters.AddWithValue("@Comments", decision ? "Approved" : "Rejected");
conn.Open();
cmd.ExecuteNonQuery();
}
lblMessage.Text = $"Request {requestId} {(decision ? "approved" : "rejected")} successfully.";
lblMessage.ForeColor = System.Drawing.Color.Green;
LoadPendingRequests(); // refresh grid
}
}
}
}
✅ How It Works
- GridView loads all
Status = 1 (PendingApproval)
requests. - HOD clicks Approve or Reject.
- The
gvRequests_RowCommand
callsusp_ApproveRequest
with the decision. - The grid refreshes with updated data.
- Status updates to Approved (2) or Rejected (3), and audit log entry is created.
⚠️ Note: For demo I hard-coded hodUserId = 2
. In production:
- Use
HttpContext.Current.User.Identity.Name
to map logged-in AD/Forms user to yourUsers
table.
Do you want me to also generate the StoreKeeper Dashboard Page (to issue items from approved requests and update stock)?
Excellent 👍 Now let’s build the StoreKeeper Dashboard Page.
This page will let the Store Keeper:
- See all approved requests (Status = 2).
- Open each request to view requested items.
- Issue the items by calling
usp_IssueItems
.
📄 StoreKeeperDashboard.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="StoreKeeperDashboard.aspx.cs" Inherits="StoresAccountingSystem.StoreKeeperDashboard" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title>Store Keeper Dashboard</title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager runat="server" ID="sm1" />
<div>
<h2>Approved Requests (Ready to Issue)</h2>
<!-- Requests Grid -->
<asp:GridView ID="gvRequests" runat="server" AutoGenerateColumns="False" DataKeyNames="RequestId" OnSelectedIndexChanged="gvRequests_SelectedIndexChanged">
<Columns>
<asp:BoundField DataField="RequestId" HeaderText="Request ID" />
<asp:BoundField DataField="RequestNumber" HeaderText="Request Number" />
<asp:BoundField DataField="RequestedBy" HeaderText="Requested By" />
<asp:BoundField DataField="CreatedAt" HeaderText="Created At" DataFormatString="{0:yyyy-MM-dd HH:mm}" />
<asp:CommandField ShowSelectButton="true" SelectText="View Items" />
</Columns>
</asp:GridView>
<hr />
<h3>Request Items</h3>
<asp:GridView ID="gvRequestItems" runat="server" AutoGenerateColumns="False">
<Columns>
<asp:BoundField DataField="ItemId" HeaderText="ItemId" />
<asp:BoundField DataField="ItemName" HeaderText="Item Name" />
<asp:BoundField DataField="QuantityRequested" HeaderText="Requested Qty" />
<asp:BoundField DataField="QuantityIssued" HeaderText="Issued Qty" />
</Columns>
</asp:GridView>
<asp:Button ID="btnIssue" runat="server" Text="Issue All Items" OnClick="btnIssue_Click" Visible="false" />
<asp:Label ID="lblMessage" runat="server" ForeColor="Green" />
</div>
</form>
</body>
</html>
📄 StoreKeeperDashboard.aspx.cs (Code-behind)
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
namespace StoresAccountingSystem
{
public partial class StoreKeeperDashboard : System.Web.UI.Page
{
private int SelectedRequestId
{
get { return ViewState["SelectedRequestId"] != null ? (int)ViewState["SelectedRequestId"] : 0; }
set { ViewState["SelectedRequestId"] = value; }
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
LoadApprovedRequests();
}
private void LoadApprovedRequests()
{
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand(@"
SELECT r.RequestId, r.RequestNumber, u.DisplayName AS RequestedBy, r.CreatedAt
FROM dbo.Requests r
INNER JOIN dbo.Users u ON r.RequestedByUserId = u.UserId
WHERE r.Status = 2 -- Approved
", conn))
{
conn.Open();
using (SqlDataReader rdr = cmd.ExecuteReader())
{
DataTable dt = new DataTable();
dt.Load(rdr);
gvRequests.DataSource = dt;
gvRequests.DataBind();
}
}
}
protected void gvRequests_SelectedIndexChanged(object sender, EventArgs e)
{
int requestId = Convert.ToInt32(gvRequests.SelectedDataKey.Value);
SelectedRequestId = requestId;
LoadRequestItems(requestId);
btnIssue.Visible = true;
}
private void LoadRequestItems(int requestId)
{
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand(@"
SELECT ri.ItemId, i.Name AS ItemName, ri.QuantityRequested, ri.QuantityIssued
FROM dbo.RequestItems ri
INNER JOIN dbo.Items i ON ri.ItemId = i.ItemId
WHERE ri.RequestId = @RequestId
", conn))
{
cmd.Parameters.AddWithValue("@RequestId", requestId);
conn.Open();
using (SqlDataReader rdr = cmd.ExecuteReader())
{
DataTable dt = new DataTable();
dt.Load(rdr);
gvRequestItems.DataSource = dt;
gvRequestItems.DataBind();
}
}
}
protected void btnIssue_Click(object sender, EventArgs e)
{
if (SelectedRequestId == 0)
{
lblMessage.Text = "No request selected.";
lblMessage.ForeColor = System.Drawing.Color.Red;
return;
}
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("dbo.usp_IssueItems", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
int storeKeeperId = 3; // Example: logged-in StoreKeeper userId
cmd.Parameters.AddWithValue("@RequestId", SelectedRequestId);
cmd.Parameters.AddWithValue("@IssuedByUserId", storeKeeperId);
conn.Open();
try
{
cmd.ExecuteNonQuery();
lblMessage.Text = $"Request {SelectedRequestId} issued successfully.";
lblMessage.ForeColor = System.Drawing.Color.Green;
}
catch (SqlException ex)
{
lblMessage.Text = $"Error issuing request: {ex.Message}";
lblMessage.ForeColor = System.Drawing.Color.Red;
}
}
// refresh
LoadApprovedRequests();
gvRequestItems.DataSource = null;
gvRequestItems.DataBind();
btnIssue.Visible = false;
SelectedRequestId = 0;
}
}
}
✅ How It Works
- GridView
gvRequests
shows all approved requests. - StoreKeeper selects one → items load in
gvRequestItems
. - Click Issue All Items → calls
usp_IssueItems
. - DB updates: inventory decremented, request marked as Issued (4), issue record logged.
- Page refreshes with updated data.
⚠️ In production:
- Replace
storeKeeperId = 3
with the logged-in user’s ID (mapped from AD/Forms). - Add more UI (filters, search, pagination).
👉 Next step could be a Stock Receipt Page (to record new cartridges received into inventory, calling usp_RecordReceipt
).
Do you want me to generate that page as well?
Perfect 👍 Now we’ll build the Stock Receipt Page for the Store Keeper.
This page will allow the Store Keeper to:
- Select an Item from inventory.
- Enter Quantity Received and an optional Supplier/Remarks.
- Submit → calls
usp_RecordReceipt
stored procedure. - Updates inventory stock and logs receipt.
📄 StockReceipt.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="StockReceipt.aspx.cs" Inherits="StoresAccountingSystem.StockReceipt" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title>Stock Receipt Entry</title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager runat="server" ID="sm1" />
<div>
<h2>Record New Stock Receipt</h2>
<table>
<tr>
<td>Item:</td>
<td>
<asp:DropDownList ID="ddlItems" runat="server" DataTextField="Name" DataValueField="ItemId" />
</td>
</tr>
<tr>
<td>Quantity Received:</td>
<td><asp:TextBox ID="txtQty" runat="server" Width="80" /></td>
</tr>
<tr>
<td>Supplier (optional):</td>
<td><asp:TextBox ID="txtSupplier" runat="server" Width="200" /></td>
</tr>
<tr>
<td>Remarks (optional):</td>
<td><asp:TextBox ID="txtRemarks" runat="server" Width="300" /></td>
</tr>
</table>
<br />
<asp:Button ID="btnSubmit" runat="server" Text="Record Receipt" OnClick="btnSubmit_Click" />
<asp:Label ID="lblMessage" runat="server" ForeColor="Green" />
</div>
</form>
</body>
</html>
📄 StockReceipt.aspx.cs (Code-behind)
using System;
using System.Data.SqlClient;
using System.Configuration;
namespace StoresAccountingSystem
{
public partial class StockReceipt : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
LoadItems();
}
private void LoadItems()
{
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("SELECT ItemId, Name FROM dbo.Items WHERE IsActive=1", conn))
{
conn.Open();
ddlItems.DataSource = cmd.ExecuteReader();
ddlItems.DataBind();
}
}
protected void btnSubmit_Click(object sender, EventArgs e)
{
int itemId = int.Parse(ddlItems.SelectedValue);
int qty;
if (!int.TryParse(txtQty.Text, out qty) || qty <= 0)
{
lblMessage.Text = "Invalid quantity entered.";
lblMessage.ForeColor = System.Drawing.Color.Red;
return;
}
string supplier = txtSupplier.Text.Trim();
string remarks = txtRemarks.Text.Trim();
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("dbo.usp_RecordReceipt", conn))
{
cmd.CommandType = System.Data.CommandType.StoredProcedure;
int storeKeeperId = 3; // Example: logged-in StoreKeeper userId
cmd.Parameters.AddWithValue("@ItemId", itemId);
cmd.Parameters.AddWithValue("@Quantity", qty);
cmd.Parameters.AddWithValue("@ReceivedByUserId", storeKeeperId);
cmd.Parameters.AddWithValue("@Supplier", string.IsNullOrEmpty(supplier) ? (object)DBNull.Value : supplier);
cmd.Parameters.AddWithValue("@Remarks", string.IsNullOrEmpty(remarks) ? (object)DBNull.Value : remarks);
conn.Open();
try
{
cmd.ExecuteNonQuery();
lblMessage.Text = "Stock receipt recorded successfully!";
lblMessage.ForeColor = System.Drawing.Color.Green;
// reset form
txtQty.Text = "";
txtSupplier.Text = "";
txtRemarks.Text = "";
}
catch (SqlException ex)
{
lblMessage.Text = "Error recording receipt: " + ex.Message;
lblMessage.ForeColor = System.Drawing.Color.Red;
}
}
}
}
}
✅ How It Works
- Store Keeper picks Item from dropdown.
- Enters Qty, Supplier, Remarks.
- Clicks Record Receipt.
- Calls
usp_RecordReceipt
, which:- Increments
InventoryStock.QuantityInStock
. - Inserts into
Receipts
table. - Logs action into
AuditLog
.
- Increments
- Shows confirmation message.
⚠️ Production considerations:
- Replace
storeKeeperId = 3
with logged-in Store Keeper fromUsers
table. - Add validation for large numbers / invalid item selection.
- Extend UI to show current stock levels after receipt.
👉 At this point, we’ve covered:
- Employee Request Page ✅
- HOD Approval Page ✅
- Store Keeper Dashboard (Issue) ✅
- Stock Receipt Page ✅
Would you like me to also generate a Reporting Page (e.g., Requests History by Department/Item, Inventory Summary)?
Great 🚀 Let’s add a Reporting Page so management (or any role with access) can see Requests History and Inventory Summary.
This page will provide:
- Requests Report → Filter by Department & Date Range.
- Inventory Summary → Current stock of all items.
📄 Reports.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Reports.aspx.cs" Inherits="StoresAccountingSystem.Reports" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title>Reports Dashboard</title>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager runat="server" ID="sm1" />
<div>
<h2>Reports Dashboard</h2>
<!-- Filter section -->
<fieldset>
<legend>Requests Report Filters</legend>
Department:
<asp:DropDownList ID="ddlDepartments" runat="server" DataTextField="Name" DataValueField="DepartmentId" AppendDataBoundItems="true">
<asp:ListItem Text="-- All --" Value="" />
</asp:DropDownList>
From:
<asp:TextBox ID="txtFromDate" runat="server" Width="100" />
To:
<asp:TextBox ID="txtToDate" runat="server" Width="100" />
<asp:Button ID="btnFilter" runat="server" Text="Apply Filter" OnClick="btnFilter_Click" />
</fieldset>
<br />
<!-- Requests Report Grid -->
<h3>Requests History</h3>
<asp:GridView ID="gvRequests" runat="server" AutoGenerateColumns="True" />
<hr />
<!-- Inventory Summary Grid -->
<h3>Inventory Summary</h3>
<asp:GridView ID="gvInventory" runat="server" AutoGenerateColumns="True" />
</div>
</form>
</body>
</html>
📄 Reports.aspx.cs (Code-behind)
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
namespace StoresAccountingSystem
{
public partial class Reports : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
LoadDepartments();
LoadRequestsReport();
LoadInventorySummary();
}
}
private void LoadDepartments()
{
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("SELECT DepartmentId, Name FROM dbo.Departments WHERE IsActive=1", conn))
{
conn.Open();
ddlDepartments.DataSource = cmd.ExecuteReader();
ddlDepartments.DataBind();
}
}
private void LoadRequestsReport(string deptId = null, DateTime? fromDate = null, DateTime? toDate = null)
{
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand(@"
SELECT r.RequestNumber, d.Name AS Department, u.DisplayName AS RequestedBy,
r.Status, r.CreatedAt, r.ApprovedAt, r.IssuedAt
FROM dbo.Requests r
INNER JOIN dbo.Departments d ON r.DepartmentId = d.DepartmentId
INNER JOIN dbo.Users u ON r.RequestedByUserId = u.UserId
WHERE ( @DeptId IS NULL OR r.DepartmentId = @DeptId )
AND ( @FromDate IS NULL OR r.CreatedAt >= @FromDate )
AND ( @ToDate IS NULL OR r.CreatedAt <= @ToDate )
ORDER BY r.CreatedAt DESC
", conn))
{
cmd.Parameters.AddWithValue("@DeptId", string.IsNullOrEmpty(deptId) ? (object)DBNull.Value : deptId);
cmd.Parameters.AddWithValue("@FromDate", fromDate.HasValue ? (object)fromDate.Value : DBNull.Value);
cmd.Parameters.AddWithValue("@ToDate", toDate.HasValue ? (object)toDate.Value : DBNull.Value);
conn.Open();
using (SqlDataReader rdr = cmd.ExecuteReader())
{
DataTable dt = new DataTable();
dt.Load(rdr);
gvRequests.DataSource = dt;
gvRequests.DataBind();
}
}
}
private void LoadInventorySummary()
{
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand(@"
SELECT i.ItemId, i.Name AS ItemName, i.Description,
ISNULL(s.QuantityInStock,0) AS QuantityInStock,
i.ReorderLevel
FROM dbo.Items i
LEFT JOIN dbo.InventoryStock s ON i.ItemId = s.ItemId
WHERE i.IsActive=1
ORDER BY i.Name
", conn))
{
conn.Open();
using (SqlDataReader rdr = cmd.ExecuteReader())
{
DataTable dt = new DataTable();
dt.Load(rdr);
gvInventory.DataSource = dt;
gvInventory.DataBind();
}
}
}
protected void btnFilter_Click(object sender, EventArgs e)
{
string deptId = ddlDepartments.SelectedValue;
DateTime? fromDate = string.IsNullOrEmpty(txtFromDate.Text) ? (DateTime?)null : DateTime.Parse(txtFromDate.Text);
DateTime? toDate = string.IsNullOrEmpty(txtToDate.Text) ? (DateTime?)null : DateTime.Parse(txtToDate.Text);
LoadRequestsReport(deptId, fromDate, toDate);
}
}
}
✅ How It Works
- On first load → Departments dropdown, Requests Report, and Inventory Summary load.
- Requests Report shows all requests with filters: Department + Date Range.
- Inventory Summary shows each item’s stock vs reorder level.
- Filter button reloads requests report based on user selection.
⚠️ Enhancements for production:
- Add status labels (Pending, Approved, Issued) with colors.
- Export to Excel/PDF.
- Role-based security → Only authorized users (like HODs/Store Keepers/Managers) should access reports.
👉 Now the system has:
- Employee Request Page ✅
- HOD Approval Page ✅
- Store Keeper Dashboard (Issue) ✅
- Stock Receipt Page ✅
- Reporting Page ✅
Would you like me to also design a basic role-based authentication/authorization setup in ASP.NET WebForms (so Employees, HODs, StoreKeepers see only their pages)?
Excellent 👍 Let’s add role-based authentication and authorization to your ASP.NET WebForms application.
We’ll keep it standards-based (Forms Authentication + Role-based Authorization) and aligned with your Users
table.
🔑 Role-Based Security Design
Roles (from your DB Roles
table)
- Employee → Can create requests.
- HOD → Can approve/reject requests.
- StoreKeeper → Can issue items & record receipts.
- (Optional) Admin → Can manage master data.
Strategy
- Login Page → User authenticates with username/password (from
Users
table). - Session / FormsAuth Ticket stores UserId + Role.
- Authorize Access in
web.config
→ restrict pages per role. - Master Page UI → Show menu items only for allowed roles.
📄 Login.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="StoresAccountingSystem.Login" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title>Login</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<h2>System Login</h2>
Username: <asp:TextBox ID="txtUser" runat="server" /><br />
Password: <asp:TextBox ID="txtPass" runat="server" TextMode="Password" /><br />
<asp:Button ID="btnLogin" runat="server" Text="Login" OnClick="btnLogin_Click" /><br />
<asp:Label ID="lblMessage" runat="server" ForeColor="Red" />
</div>
</form>
</body>
</html>
📄:Login.aspx.cs
using System;
using System.Data.SqlClient;
using System.Configuration;
using System.Web.Security;
namespace StoresAccountingSystem
{
public partial class Login : System.Web.UI.Page
{
protected void btnLogin_Click(object sender, EventArgs e)
{
string username = txtUser.Text.Trim();
string password = txtPass.Text.Trim(); // in production → hash + salt
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand(@"
SELECT u.UserId, u.DisplayName, r.Name AS RoleName
FROM dbo.Users u
INNER JOIN dbo.Roles r ON u.RoleId = r.RoleId
WHERE u.Username=@Username AND u.PasswordHash=@Password AND u.IsActive=1
", conn))
{
cmd.Parameters.AddWithValue("@Username", username);
cmd.Parameters.AddWithValue("@Password", password); // ⚠️ Demo only; use hashing in production
conn.Open();
using (SqlDataReader rdr = cmd.ExecuteReader())
{
if (rdr.Read())
{
int userId = rdr.GetInt32(0);
string displayName = rdr.GetString(1);
string role = rdr.GetString(2);
// Create FormsAuth ticket
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1,
username,
DateTime.Now,
DateTime.Now.AddMinutes(30),
false,
role, // store role in UserData
FormsAuthentication.FormsCookiePath);
string encTicket = FormsAuthentication.Encrypt(ticket);
Response.Cookies.Add(new System.Web.HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
Session["UserId"] = userId;
Session["DisplayName"] = displayName;
Session["Role"] = role;
// Redirect to home/dashboard
Response.Redirect("Default.aspx");
}
else
{
lblMessage.Text = "Invalid username or password.";
}
}
}
}
}
}
📄:web.config (Authorization Rules)
<configuration>
<system.web>
<!-- Enable Forms Authentication -->
<authentication mode="Forms">
<forms loginUrl="Login.aspx" timeout="30" />
</authentication>
<!-- Role-based authorization -->
<authorization>
<deny users="?" /> <!-- Deny anonymous -->
</authorization>
</system.web>
<location path="EmployeeRequest.aspx">
<system.web>
<authorization>
<allow roles="Employee" />
<deny users="*" />
</authorization>
</system.web>
</location>
<location path="HODApproval.aspx">
<system.web>
<authorization>
<allow roles="HOD" />
<deny users="*" />
</authorization>
</system.web>
</location>
<location path="StoreKeeperDashboard.aspx">
<system.web>
<authorization>
<allow roles="StoreKeeper" />
<deny users="*" />
</authorization>
</system.web>
</location>
<location path="StockReceipt.aspx">
<system.web>
<authorization>
<allow roles="StoreKeeper" />
<deny users="*" />
</authorization>
</system.web>
</location>
<location path="Reports.aspx">
<system.web>
<authorization>
<allow roles="HOD,StoreKeeper,Admin" />
<deny users="*" />
</authorization>
</system.web>
</location>
</configuration>
📄:Global.asax (to attach role to HttpContext.User)
using System;
using System.Web;
using System.Web.Security;
using System.Security.Principal;
namespace StoresAccountingSystem
{
public class Global : HttpApplication
{
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (HttpContext.Current.User != null &&
HttpContext.Current.User.Identity.IsAuthenticated &&
HttpContext.Current.User.Identity is FormsIdentity id)
{
FormsAuthenticationTicket ticket = id.Ticket;
string[] roles = ticket.UserData.Split(',');
HttpContext.Current.User = new GenericPrincipal(id, roles);
}
}
}
}
✅ How It Works
- User logs in → checked against
Users
+Roles
tables. - On success → FormsAuth ticket is issued, role stored in ticket.
- On each request →
Global.asax
attaches role toHttpContext.User
. - web.config rules control which pages are accessible per role.
- Unauthorized access → automatically redirected to
Login.aspx
.
⚠️ For production hardening:
- Store hashed passwords (
SHA256/SHA512 + salt
). - Use HTTPS only.
- Add session timeout & logout page.
- Add Admin UI to manage users/roles.
👉 With this, your Stores Accounting System now has:
- Full CRUD process (Request → Approval → Issue → Receipt).
- Reporting.
- Role-based security.
Would you like me to also create a Master Page with a dynamic navigation menu (showing menu links based on the logged-in role)?
Perfect 👍 Let’s now create a Master Page with a dynamic navigation menu so users only see the pages they are allowed to access (based on their role).
📄 Site.Master
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.Master.cs" Inherits="StoresAccountingSystem.SiteMaster" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title>Stores Accounting System</title>
<asp:ContentPlaceHolder ID="head" runat="server" />
</head>
<body>
<form id="form1" runat="server">
<div>
<h1>Stores Accounting System</h1>
<div style="float:right;">
Welcome, <asp:Label ID="lblUser" runat="server" /> |
<asp:LinkButton ID="btnLogout" runat="server" OnClick="btnLogout_Click">Logout</asp:LinkButton>
</div>
<hr />
<!-- Navigation Menu -->
<asp:Menu ID="navMenu" runat="server" Orientation="Horizontal" StaticDisplayLevels="1">
<Items>
<!-- Dynamically shown items based on role -->
</Items>
</asp:Menu>
<hr />
<!-- Page content -->
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
</div>
</form>
</body>
</html>
📄:Site.Master.cs
using System;
using System.Web.Security;
namespace StoresAccountingSystem
{
public partial class SiteMaster : System.Web.UI.MasterPage
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack && Context.User.Identity.IsAuthenticated)
{
lblUser.Text = Session["DisplayName"]?.ToString();
string role = Session["Role"]?.ToString();
if (!string.IsNullOrEmpty(role))
{
BuildMenu(role);
}
}
}
private void BuildMenu(string role)
{
navMenu.Items.Clear();
// Employee
if (role == "Employee")
navMenu.Items.Add(new System.Web.UI.WebControls.MenuItem("Create Request", "", "", "~/EmployeeRequest.aspx"));
// HOD
if (role == "HOD")
{
navMenu.Items.Add(new System.Web.UI.WebControls.MenuItem("Approve Requests", "", "", "~/HODApproval.aspx"));
navMenu.Items.Add(new System.Web.UI.WebControls.MenuItem("Reports", "", "", "~/Reports.aspx"));
}
// Store Keeper
if (role == "StoreKeeper")
{
navMenu.Items.Add(new System.Web.UI.WebControls.MenuItem("Issue Items", "", "", "~/StoreKeeperDashboard.aspx"));
navMenu.Items.Add(new System.Web.UI.WebControls.MenuItem("Stock Receipt", "", "", "~/StockReceipt.aspx"));
navMenu.Items.Add(new System.Web.UI.WebControls.MenuItem("Reports", "", "", "~/Reports.aspx"));
}
// Admin
if (role == "Admin")
{
navMenu.Items.Add(new System.Web.UI.WebControls.MenuItem("Manage Items", "", "", "~/Admin/Items.aspx"));
navMenu.Items.Add(new System.Web.UI.WebControls.MenuItem("Manage Users", "", "", "~/Admin/Users.aspx"));
navMenu.Items.Add(new System.Web.UI.WebControls.MenuItem("Reports", "", "", "~/Reports.aspx"));
}
}
protected void btnLogout_Click(object sender, EventArgs e)
{
FormsAuthentication.SignOut();
Session.Clear();
Response.Redirect("~/Login.aspx");
}
}
}
📄:Example Page Using Master Page
Each content page (e.g. EmployeeRequest.aspx
) should use the master page:
<%@ Page Title="Create Request" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="EmployeeRequest.aspx.cs" Inherits="StoresAccountingSystem.EmployeeRequest" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<!-- Page-specific UI goes here -->
</asp:Content>
✅ How It Works
- When a user logs in →
Session["Role"]
is set. Site.Master
builds the menu dynamically per role.- Only the relevant menu items are shown.
- Logout clears session + FormsAuth ticket and redirects to
Login.aspx
.
⚠️ Notes for production:
- If you want multi-role users, adapt the menu builder to allow multiple roles per user (currently one role per user).
- Consider moving menu configuration to database (so admins can add/edit links without code changes).
- Add active page highlighting for better UX.
👉 With this Master Page + Dynamic Navigation, your application now has a complete flow with:
- Role-based pages & menu.
- Request/Approval/Issue/Receipt processes.
- Reporting.
- Secure authentication & authorization.
Do you also want me to create an Admin Management Module (pages to manage Users, Roles, and Items) so you don’t need to seed data manually in SQL?
Excellent 👍 Let’s design the Admin Management Module so you don’t have to seed or maintain data manually in SQL.
We’ll build CRUD pages for Items, Users, and Roles inside an /Admin/
folder, accessible only to the Admin
role.
🔧 Admin Module Pages
- Manage Items → Add / Edit / Deactivate items.
- Manage Users → Create users, assign roles, activate/deactivate.
- Manage Roles → Add or edit roles.
📄:Admin/Items.aspx
<%@ Page Title="Manage Items" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Items.aspx.cs" Inherits="StoresAccountingSystem.Admin.Items" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<h2>Manage Items</h2>
<asp:GridView ID="gvItems" runat="server" AutoGenerateColumns="False" DataKeyNames="ItemId"
OnRowEditing="gvItems_RowEditing" OnRowCancelingEdit="gvItems_RowCancelingEdit"
OnRowUpdating="gvItems_RowUpdating" OnRowDeleting="gvItems_RowDeleting">
<Columns>
<asp:BoundField DataField="ItemId" HeaderText="ID" ReadOnly="true" />
<asp:BoundField DataField="Name" HeaderText="Name" />
<asp:BoundField DataField="Description" HeaderText="Description" />
<asp:BoundField DataField="ReorderLevel" HeaderText="Reorder Level" />
<asp:CheckBoxField DataField="IsActive" HeaderText="Active" />
<asp:CommandField ShowEditButton="true" ShowDeleteButton="true" />
</Columns>
</asp:GridView>
<br />
<asp:TextBox ID="txtName" runat="server" Placeholder="Name" />
<asp:TextBox ID="txtDesc" runat="server" Placeholder="Description" />
<asp:TextBox ID="txtReorder" runat="server" Placeholder="Reorder Level" />
<asp:Button ID="btnAdd" runat="server" Text="Add Item" OnClick="btnAdd_Click" />
</asp:Content>
📄:Admin/Items.aspx.cs
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
namespace StoresAccountingSystem.Admin
{
public partial class Items : System.Web.UI.Page
{
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack) LoadItems();
}
private void LoadItems()
{
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("SELECT * FROM dbo.Items", conn))
{
conn.Open();
DataTable dt = new DataTable();
dt.Load(cmd.ExecuteReader());
gvItems.DataSource = dt;
gvItems.DataBind();
}
}
protected void btnAdd_Click(object sender, EventArgs e)
{
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("INSERT INTO dbo.Items(Name, Description, ReorderLevel) VALUES(@n,@d,@r)", conn))
{
cmd.Parameters.AddWithValue("@n", txtName.Text);
cmd.Parameters.AddWithValue("@d", txtDesc.Text);
cmd.Parameters.AddWithValue("@r", int.Parse(txtReorder.Text));
conn.Open();
cmd.ExecuteNonQuery();
}
LoadItems();
}
protected void gvItems_RowEditing(object sender, System.Web.UI.WebControls.GridViewEditEventArgs e)
{
gvItems.EditIndex = e.NewEditIndex;
LoadItems();
}
protected void gvItems_RowCancelingEdit(object sender, System.Web.UI.WebControls.GridViewCancelEditEventArgs e)
{
gvItems.EditIndex = -1;
LoadItems();
}
protected void gvItems_RowUpdating(object sender, System.Web.UI.WebControls.GridViewUpdateEventArgs e)
{
int id = (int)gvItems.DataKeys[e.RowIndex].Value;
string name = ((System.Web.UI.WebControls.TextBox)gvItems.Rows[e.RowIndex].Cells[1].Controls[0]).Text;
string desc = ((System.Web.UI.WebControls.TextBox)gvItems.Rows[e.RowIndex].Cells[2].Controls[0]).Text;
int reorder = int.Parse(((System.Web.UI.WebControls.TextBox)gvItems.Rows[e.RowIndex].Cells[3].Controls[0]).Text);
bool active = ((System.Web.UI.WebControls.CheckBox)gvItems.Rows[e.RowIndex].Cells[4].Controls[0]).Checked;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("UPDATE dbo.Items SET Name=@n, Description=@d, ReorderLevel=@r, IsActive=@a WHERE ItemId=@id", conn))
{
cmd.Parameters.AddWithValue("@n", name);
cmd.Parameters.AddWithValue("@d", desc);
cmd.Parameters.AddWithValue("@r", reorder);
cmd.Parameters.AddWithValue("@a", active);
cmd.Parameters.AddWithValue("@id", id);
conn.Open();
cmd.ExecuteNonQuery();
}
gvItems.EditIndex = -1;
LoadItems();
}
protected void gvItems_RowDeleting(object sender, System.Web.UI.WebControls.GridViewDeleteEventArgs e)
{
int id = (int)gvItems.DataKeys[e.RowIndex].Value;
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("DELETE FROM dbo.Items WHERE ItemId=@id", conn))
{
cmd.Parameters.AddWithValue("@id", id);
conn.Open();
cmd.ExecuteNonQuery();
}
LoadItems();
}
}
}
📄:Admin/Users.aspx (UI)
<%@ Page Title="Manage Users" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Users.aspx.cs" Inherits="StoresAccountingSystem.Admin.Users" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<h2>Manage Users</h2>
<asp:GridView ID="gvUsers" runat="server" AutoGenerateColumns="False" DataKeyNames="UserId"
OnRowEditing="gvUsers_RowEditing" OnRowUpdating="gvUsers_RowUpdating"
OnRowCancelingEdit="gvUsers_RowCancelingEdit">
<Columns>
<asp:BoundField DataField="UserId" HeaderText="ID" ReadOnly="true" />
<asp:BoundField DataField="Username" HeaderText="Username" />
<asp:BoundField DataField="DisplayName" HeaderText="Display Name" />
<asp:BoundField DataField="RoleName" HeaderText="Role" />
<asp:CheckBoxField DataField="IsActive" HeaderText="Active" />
<asp:CommandField ShowEditButton="true" />
</Columns>
</asp:GridView>
<br />
<h3>Add New User</h3>
Username: <asp:TextBox ID="txtUser" runat="server" />
Display Name: <asp:TextBox ID="txtDisplay" runat="server" />
Password: <asp:TextBox ID="txtPass" runat="server" TextMode="Password" />
Role:
<asp:DropDownList ID="ddlRoles" runat="server" DataTextField="Name" DataValueField="RoleId" />
<asp:Button ID="btnAddUser" runat="server" Text="Add User" OnClick="btnAddUser_Click" />
</asp:Content>
📄:Admin/Users.aspx.cs (partial)
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
namespace StoresAccountingSystem.Admin
{
public partial class Users : System.Web.UI.Page
{
string cs = ConfigurationManager.ConnectionStrings["StoresDb"].ConnectionString;
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
LoadUsers();
LoadRoles();
}
}
private void LoadUsers()
{
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand(@"
SELECT u.UserId, u.Username, u.DisplayName, r.Name AS RoleName, u.IsActive
FROM dbo.Users u INNER JOIN dbo.Roles r ON u.RoleId=r.RoleId", conn))
{
conn.Open();
DataTable dt = new DataTable();
dt.Load(cmd.ExecuteReader());
gvUsers.DataSource = dt;
gvUsers.DataBind();
}
}
private void LoadRoles()
{
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("SELECT RoleId, Name FROM dbo.Roles", conn))
{
conn.Open();
ddlRoles.DataSource = cmd.ExecuteReader();
ddlRoles.DataBind();
}
}
protected void btnAddUser_Click(object sender, EventArgs e)
{
using (SqlConnection conn = new SqlConnection(cs))
using (SqlCommand cmd = new SqlCommand("INSERT INTO dbo.Users(Username, DisplayName, PasswordHash, RoleId) VALUES(@u,@d,@p,@r)", conn))
{
cmd.Parameters.AddWithValue("@u", txtUser.Text);
cmd.Parameters.AddWithValue("@d", txtDisplay.Text);
cmd.Parameters.AddWithValue("@p", txtPass.Text); // ⚠️ demo only, hash in prod
cmd.Parameters.AddWithValue("@r", ddlRoles.SelectedValue);
conn.Open();
cmd.ExecuteNonQuery();
}
LoadUsers();
}
// Editing users would be similar (update role/active status)
}
}
📄:Admin/Roles.aspx
<%@ Page Title="Manage Roles" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Roles.aspx.cs" Inherits="StoresAccountingSystem.Admin.Roles" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<h2>Manage Roles</h2>
<asp:GridView ID="gvRoles" runat="server" AutoGenerateColumns="False" DataKeyNames="RoleId"
OnRowEditing="gvRoles_RowEditing" OnRowUpdating="gvRoles_RowUpdating"
OnRowCancelingEdit="gvRoles_RowCancelingEdit">
<Columns>
<asp:BoundField DataField="RoleId" HeaderText="ID" ReadOnly="true" />
<asp:BoundField DataField="Name" HeaderText="Role Name" />
<asp:CommandField ShowEditButton="true" />
</Columns>
</asp:GridView>
<br />
<asp:TextBox ID="txtRole" runat="server" Placeholder="Role Name" />
<asp:Button ID="btnAddRole" runat="server" Text="Add Role" OnClick="btnAddRole_Click" />
</asp:Content>
✅ How It Works
Items.aspx
→ CRUD operations for inventory items.Users.aspx
→ Admin can add/edit users and assign roles.Roles.aspx
→ Manage role definitions.- Only Admin role can access these pages (
web.config
<location>
restrictions).
⚠️ Production improvements:
- Store hashed passwords (SHA-256 or better).
- Use DropDownList in
gvUsers
for role editing. - Add validation and error handling.
- Consider soft delete (set
IsActive=0
instead of hard delete).
👉 Now your system has a full lifecycle:
- Requests → Approvals → Issue → Receipt → Reporting ✅
- Role-based security ✅
- Dynamic navigation ✅
- Admin panel for managing master data ✅
Would you like me to also sketch a Database Backup & Maintenance Plan (aligning with ITIL practices)?
Great decision 👍 — a Database Backup & Maintenance Plan is essential to keep your Stores Accounting System reliable, secure, and ITIL-compliant. Below is a step-by-step plan tailored for MS SQL Server with best practices.
🛠 SQL Server Backup & Maintenance Plan (ITIL Aligned)
1. Backup Strategy
a) Types of Backups
- Full Backup
📌 Complete copy of the database (recommended daily at night). - Differential Backup
📌 Captures changes since the last full backup (every 6 hours). - Transaction Log Backup
📌 Allows point-in-time recovery (every 30 minutes).
👉 Store backups on separate disks and replicate to offsite storage (cloud, secondary data center).
2. Automated Backup Scripts
Create a SQL Server Agent Job with below script:
-- Full Backup (run daily at 2 AM)
BACKUP DATABASE [StoresDB]
TO DISK = 'D:\SQLBackups\StoresDB_Full.bak'
WITH FORMAT, INIT,
NAME = 'StoresDB-Full Backup',
SKIP, NOREWIND, NOUNLOAD, STATS = 10;
-- Differential Backup (run every 6 hours)
BACKUP DATABASE [StoresDB]
TO DISK = 'D:\SQLBackups\StoresDB_Diff.bak'
WITH DIFFERENTIAL,
NAME = 'StoresDB-Differential Backup',
SKIP, NOREWIND, NOUNLOAD, STATS = 10;
-- Transaction Log Backup (run every 30 minutes)
BACKUP LOG [StoresDB]
TO DISK = 'D:\SQLBackups\StoresDB_Log.trn'
WITH NOFORMAT, NOINIT,
NAME = 'StoresDB-Transaction Log Backup',
SKIP, NOREWIND, NOUNLOAD, STATS = 10;
3. Retention Policy
- Keep daily backups for 14 days.
- Keep weekly backups for 2 months.
- Archive monthly backups for 1 year (compliance).
- Use a scheduled job to delete old backups automatically. Example:
EXECUTE master.dbo.xp_delete_file 0, 'D:\SQLBackups\', 'bak', '2025-07-01T00:00:00';
4. Maintenance Tasks
Schedule via SQL Server Agent Maintenance Plan:
- Index Optimization
- Rebuild indexes if fragmentation > 30%.
- Reorganize if fragmentation 5–30%.
- Update Statistics weekly for query performance.
- Integrity Checks
RunDBCC CHECKDB('StoresDB')
weekly to detect corruption. - Cleanup Jobs
Delete old log files & history entries.
5. Disaster Recovery Testing
- Test backup restore process monthly on a test server.
- Document Recovery Time Objective (RTO) and Recovery Point Objective (RPO):
- RTO = max acceptable downtime (e.g., 2 hours).
- RPO = max acceptable data loss (e.g., 30 minutes, based on log backups).
6. Security & Compliance
- Encrypt backups (
WITH ENCRYPTION
in SQL Server 2014+). - Restrict folder access (
NTFS permissions
→ only DBAs). - Use offsite/cloud replication for DR (e.g., Azure Blob Storage).
7. Monitoring & Alerts
- Enable SQL Server Alerts for backup failures.
- Use Database Mail to notify admins. Example:
EXEC msdb.dbo.sp_send_dbmail
@profile_name='DBAdminProfile',
@recipients='[email protected]',
@subject='SQL Backup Failed',
@body='The backup job for StoresDB failed. Please check immediately.';
✅ ITIL Alignment
- Availability Management → Regular backups + redundancy.
- Service Continuity → Disaster recovery tested & documented.
- Capacity & Performance → Index/statistics maintenance.
- Security Management → Backup encryption + role-based access.
- Change Management → All backup plan changes documented.
👉 With this plan, your StoresDB will have:
- Automated full + diff + log backups.
- Retention & archiving for compliance.
- Disaster recovery readiness.
- ITIL best practices embedded.
Would you like me to also provide a step-by-step SQL Server Agent Job setup guide (so your DBAs can implement this without manual scripting)?
Perfect 👍 Let’s now create a step-by-step SQL Server Agent Job setup guide so your DBAs can implement the backup & maintenance plan without needing to run scripts manually.
⚙️ Step-by-Step Guide: SQL Server Agent Jobs Setup
1. Prerequisites
- Ensure SQL Server Agent service is running.
- Ensure DBA account has sysadmin rights.
- Create a backup folder, e.g.,
D:\SQLBackups
. - Set up a Database Mail profile if you want email alerts.
2. Create a Full Backup Job
- Open SQL Server Management Studio (SSMS).
- Expand SQL Server Agent → Jobs.
- Right-click Jobs → New Job.
- Enter:
- Name:
StoresDB - Full Backup
- Owner:
sa
or DBA account
- Name:
- In Steps tab → click New:
- Step Name:
Full Backup
- Type:
Transact-SQL script (T-SQL)
- Database:
master
- Command:
BACKUP DATABASE [StoresDB] TO DISK = 'D:\SQLBackups\StoresDB_Full.bak' WITH FORMAT, INIT, NAME = 'StoresDB-Full Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10;
- Step Name:
- In Schedules tab → New schedule:
- Name:
Daily 2 AM
- Occurs: Daily at 2:00 AM.
- Name:
- In Notifications tab:
- Select
Email
on failure (choose DBA mail profile).
- Select
3. Create a Differential Backup Job
- Repeat New Job.
- Name:
StoresDB - Differential Backup
.
- Name:
- Add step with command:
BACKUP DATABASE [StoresDB] TO DISK = 'D:\SQLBackups\StoresDB_Diff.bak' WITH DIFFERENTIAL, NAME = 'StoresDB-Differential Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10;
- Schedule: Every 6 hours (e.g., 6 AM, 12 PM, 6 PM, 12 AM).
4. Create a Transaction Log Backup Job
- New Job → Name:
StoresDB - Log Backup
. - Add step:
BACKUP LOG [StoresDB] TO DISK = 'D:\SQLBackups\StoresDB_Log.trn' WITH NOFORMAT, NOINIT, NAME = 'StoresDB-Transaction Log Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10;
- Schedule: Every 30 minutes.
5. Create a Cleanup Job (Old Backup Files)
- New Job → Name:
StoresDB - Backup Cleanup
. - Add step:
EXECUTE master.dbo.xp_delete_file 0, 'D:\SQLBackups\', 'bak', DATEADD(DAY,-14,GETDATE()); EXECUTE master.dbo.xp_delete_file 1, 'D:\SQLBackups\', 'trn', DATEADD(DAY,-7,GETDATE());
- Deletes
.bak
files older than 14 days. - Deletes
.trn
files older than 7 days.
- Deletes
- Schedule: Daily at 3:00 AM (after full backup).
6. Create a Database Integrity Check Job
- New Job → Name:
StoresDB - DBCC CHECKDB
. - Add step:
DBCC CHECKDB('StoresDB') WITH NO_INFOMSGS, ALL_ERRORMSGS;
- Schedule: Weekly on Sunday at 4:00 AM.
7. Create an Index Maintenance Job
- New Job → Name:
StoresDB - Index Maintenance
. - Add step with script:
-- Reorganize indexes with fragmentation between 5%–30% ALTER INDEX ALL ON dbo.Items REORGANIZE; ALTER INDEX ALL ON dbo.Requests REORGANIZE; -- Rebuild indexes if fragmentation >30% ALTER INDEX ALL ON dbo.Items REBUILD; ALTER INDEX ALL ON dbo.Requests REBUILD;
- Schedule: Weekly on Saturday at 2:00 AM.
8. Monitoring & Alerts
- In SQL Server Agent → Operators:
- Create operator:
DBA_Team
with email.
- Create operator:
- In each job → Notifications tab:
- On Failure → Notify operator via email.
✅ Result
- Automated Full / Differential / Log backups.
- Cleanup of old files ensures storage efficiency.
- DBCC & Index jobs maintain DB health.
- Email alerts notify DBAs of failures.
- ITIL availability & continuity management enforced.
👉 This setup ensures your Stores Accounting System has automated resilience against data loss and performance degradation.
Would you like me to also prepare a disaster recovery runbook (step-by-step instructions for restoring your system in case of failure)?
Perfect 👍 Let’s now create a step-by-step SQL Server Agent Job setup guide so your DBAs can implement the backup & maintenance plan without needing to run scripts manually.
⚙️ Step-by-Step Guide: SQL Server Agent Jobs Setup
1. Prerequisites
- Ensure SQL Server Agent service is running.
- Ensure DBA account has sysadmin rights.
- Create a backup folder, e.g.,
D:\SQLBackups
. - Set up a Database Mail profile if you want email alerts.
2. Create a Full Backup Job
- Open SQL Server Management Studio (SSMS).
- Expand SQL Server Agent → Jobs.
- Right-click Jobs → New Job.
- Enter:
- Name:
StoresDB - Full Backup
- Owner:
sa
or DBA account
- Name:
- In Steps tab → click New:
- Step Name:
Full Backup
- Type:
Transact-SQL script (T-SQL)
- Database:
master
- Command:
BACKUP DATABASE [StoresDB] TO DISK = 'D:\SQLBackups\StoresDB_Full.bak' WITH FORMAT, INIT, NAME = 'StoresDB-Full Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10;
- Step Name:
- In Schedules tab → New schedule:
- Name:
Daily 2 AM
- Occurs: Daily at 2:00 AM.
- Name:
- In Notifications tab:
- Select
Email
on failure (choose DBA mail profile).
- Select
3. Create a Differential Backup Job
- Repeat New Job.
- Name:
StoresDB - Differential Backup
.
- Name:
- Add step with command:
BACKUP DATABASE [StoresDB] TO DISK = 'D:\SQLBackups\StoresDB_Diff.bak' WITH DIFFERENTIAL, NAME = 'StoresDB-Differential Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10;
- Schedule: Every 6 hours (e.g., 6 AM, 12 PM, 6 PM, 12 AM).
4. Create a Transaction Log Backup Job
- New Job → Name:
StoresDB - Log Backup
. - Add step:
BACKUP LOG [StoresDB] TO DISK = 'D:\SQLBackups\StoresDB_Log.trn' WITH NOFORMAT, NOINIT, NAME = 'StoresDB-Transaction Log Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10;
- Schedule: Every 30 minutes.
5. Create a Cleanup Job (Old Backup Files)
- New Job → Name:
StoresDB - Backup Cleanup
. - Add step:
EXECUTE master.dbo.xp_delete_file 0, 'D:\SQLBackups\', 'bak', DATEADD(DAY,-14,GETDATE()); EXECUTE master.dbo.xp_delete_file 1, 'D:\SQLBackups\', 'trn', DATEADD(DAY,-7,GETDATE());
- Deletes
.bak
files older than 14 days. - Deletes
.trn
files older than 7 days.
- Deletes
- Schedule: Daily at 3:00 AM (after full backup).
6. Create a Database Integrity Check Job
- New Job → Name:
StoresDB - DBCC CHECKDB
. - Add step:
DBCC CHECKDB('StoresDB') WITH NO_INFOMSGS, ALL_ERRORMSGS;
- Schedule: Weekly on Sunday at 4:00 AM.
7. Create an Index Maintenance Job
- New Job → Name:
StoresDB - Index Maintenance
. - Add step with script:
-- Reorganize indexes with fragmentation between 5%–30% ALTER INDEX ALL ON dbo.Items REORGANIZE; ALTER INDEX ALL ON dbo.Requests REORGANIZE; -- Rebuild indexes if fragmentation >30% ALTER INDEX ALL ON dbo.Items REBUILD; ALTER INDEX ALL ON dbo.Requests REBUILD;
- Schedule: Weekly on Saturday at 2:00 AM.
8. Monitoring & Alerts
- In SQL Server Agent → Operators:
- Create operator:
DBA_Team
with email.
- Create operator:
- In each job → Notifications tab:
- On Failure → Notify operator via email.
✅ Result
- Automated Full / Differential / Log backups.
- Cleanup of old files ensures storage efficiency.
- DBCC & Index jobs maintain DB health.
- Email alerts notify DBAs of failures.
- ITIL availability & continuity management enforced.
👉 This setup ensures your Stores Accounting System has automated resilience against data loss and performance degradation.
Would you like me to also prepare a disaster recovery runbook (step-by-step instructions for restoring your system in case of failure)?
Excellent 👍 — relying only on restores works, but High Availability (HA) + Disaster Recovery (DR) ensures your Stores Accounting System stays online even during failures. Let’s design a SQL Server HA/Failover setup with ITIL alignment.
⚙️ High Availability & Failover Design for StoresDB
1. Goals
- High Availability (HA): Keep database accessible if the primary server fails.
- Disaster Recovery (DR): Recover from full site outage.
- ITIL Alignment: Availability, Continuity, Capacity, and Change Management.
2. Options (SQL Server)
Feature | Use Case | Pros | Cons |
---|---|---|---|
Failover Cluster Instance (FCI) | Hardware/node failure | Transparent failover | Shared storage = single point of failure |
AlwaysOn Availability Groups (AG) | HA + read replicas | Sync/async replicas, fast failover, readable secondaries | Requires Enterprise Edition (unless basic AG in Std) |
Log Shipping | DR, async standby | Simple, works with Standard Edition, DR site | Manual failover, some delay |
Database Mirroring (deprecated) | Legacy HA | Supported on older versions | Replaced by AGs |
👉 Since you’re on SQL Server Standard (most likely), best choices are:
- Log Shipping (for DR) + Basic Availability Groups (for HA).
3. Proposed Architecture
- Primary SQL Server (Production, Data Center A).
- Secondary SQL Server (HA Replica, same data center).
- DR Server (Log Shipping target, Data Center B).
- IIS Web Servers with Network Load Balancer.
Users → Load Balancer → IIS Web Servers
↓
SQL Primary (Sync) → SQL Secondary (HA failover)
↓
Log Shipping → SQL DR Server (async)
4. Setup Steps
A) High Availability (Primary + Secondary)
- Enable AlwaysOn Availability Groups (if SQL 2016+).
- Configure Primary Replica + Secondary Replica:
- Sync mode → Automatic failover within local data center.
- Add Listener Name (single connection string for apps).
<connectionStrings> <add name="StoresDb" connectionString="Server=StoresAGListener;Database=StoresDB;Integrated Security=True;" /> </connectionStrings>
B) Disaster Recovery (Log Shipping to DR site)
- Configure Log Shipping from Primary to DR Server:
-- On Primary BACKUP LOG StoresDB TO DISK='\\DRServer\LogShip\StoresDB_Log.trn' WITH INIT;
- On DR server → restore with
NORECOVERY
. - Configure SQL Agent Jobs for:
- Backup logs every 15 min.
- Copy logs to DR site.
- Restore logs on DR server.
- Monitor via sp_help_log_shipping_monitor.
C) IIS/Web Layer HA
- Deploy at least 2 IIS Web Servers with your ASP.NET WebForms app.
- Use Windows NLB or a hardware load balancer.
- Store web.config outside app pool for easy sync.
5. Failover Process
Automatic (within Data Center A)
- If Primary SQL node fails → AlwaysOn AG promotes Secondary automatically.
- IIS apps reconnect via AG Listener (transparent).
Manual (DR Site)
- DBA stops log shipping.
- On DR server:
RESTORE DATABASE StoresDB WITH RECOVERY;
- Point IIS connection string to DR SQL Server.
- Business resumes on DR site.
6. Monitoring
- Use SQL Server Management Pack (SCOM) or custom alerts.
- Monitor:
- Replica sync state.
- Log shipping delay.
- Failover events.
- Send email/SMS alerts via Database Mail.
7. ITIL Alignment
- Availability Management: SQL AGs for HA, Log Shipping for DR.
- Continuity Management: Documented DR runbook + test failovers quarterly.
- Capacity Management: Secondary replicas can handle read-only reporting.
- Change Management: Any HA/DR changes go through CAB approval.
✅ Benefits
- HA inside DC: Automatic failover within seconds.
- DR across sites: Manual failover with max 15 min data loss (RPO).
- Load-balanced IIS layer: Application remains accessible even if one web server fails.
👉 This gives you a hybrid HA + DR architecture:
- Automatic HA within primary data center.
- Manual DR to secondary data center if full outage.
Would you like me to also prepare a step-by-step implementation checklist (with exact tasks for DBA, SysAdmin, and App Team) so your teams can roll this out?
Perfect 👍 — let’s make a step-by-step implementation checklist for rolling out the High Availability (HA) + Disaster Recovery (DR) setup for your Stores Accounting System.
This will be broken down by teams: DBA, SysAdmin, and Application Team.
📋 HA + DR Implementation Checklist
For Stores Accounting System (MS SQL Server + ASP.NET 4.0 WebForms)
1. Planning & Preparation
✅ All Teams
- Define RTO (2h) and RPO (30min) in SLA.
- Approve solution in CAB (Change Advisory Board) (ITIL Change Mgmt).
- Ensure licenses available: SQL Server Standard/Enterprise, Windows Server.
- Document network topology & firewall rules.
- Identify Primary (DC-A) and DR (DC-B) servers.
2. SysAdmin Team Tasks
Infrastructure
- Provision 2 SQL Servers (Primary + Secondary) in DC-A.
- Provision 1 SQL Server (DR) in DC-B.
- Configure shared storage (if using Failover Cluster).
- Set up Windows Failover Clustering (for AlwaysOn AG).
- Install and configure IIS Web Servers (2 nodes).
- Implement Windows NLB or hardware load balancer for IIS.
- Ensure time synchronization across servers.
3. DBA Team Tasks
SQL Server Setup
- Install SQL Server (same edition, service pack, collation) on all servers.
- Enable AlwaysOn Availability Groups on Primary & Secondary.
- Create StoresDB database on Primary.
- Configure Availability Group (AG):
- Add Secondary replica (synchronous, auto-failover).
- Add Listener (
StoresAGListener
).
- Configure Log Shipping to DR server:
- Setup log backup job (every 15 min).
- Setup log copy job to DR.
- Setup restore job on DR.
- Verify failover works between Primary & Secondary.
- Test manual failover to DR server.
Monitoring & Maintenance
- Configure SQL Agent alerts for AG health + log shipping delays.
- Set up Database Mail for DBA notifications.
- Schedule DBCC CHECKDB and index maintenance.
4. Application Team Tasks
ASP.NET Application
- Modify
web.config
to use AG Listener name:<connectionStrings> <add name="StoresDb" connectionString="Server=StoresAGListener;Database=StoresDB;Integrated Security=True;" /> </connectionStrings>
- Deploy application to both IIS nodes.
- Configure session state:
- SQL Server session state (HA aware), or
- Sticky sessions in NLB.
- Validate role-based authentication (Employee, HoD, StoreKeeper) after failover.
5. Testing Plan
✅ DBA + App Team
- Simulate SQL Primary failure → confirm automatic failover to Secondary.
- Simulate full DC-A outage → promote DR server and re-point IIS.
- Test RPO: recover up to last 15 minutes of transactions.
- Test RTO: confirm full system recovery < 2 hours.
- Document results and lessons learned.
6. Operational Handover
- Update Runbook with HA + DR failover procedures.
- Train Service Desk to escalate incidents correctly.
- Ensure weekly health checks (replica sync, log shipping latency).
- Schedule quarterly DR drills.
- Store backups + configs in secure repository (version-controlled).
7. ITIL Alignment
- Availability Mgmt: HA setup with AG + NLB.
- Continuity Mgmt: DR via log shipping, tested quarterly.
- Change Mgmt: CAB approval before production changes.
- Capacity Mgmt: Secondary replicas can handle read workloads.
- Incident Mgmt: DR runbook used during outages.
✅ Outcome
With this checklist:
- DBAs, SysAdmins, and App Team know exact responsibilities.
- Failover is automated locally, manual for DR.
- System recovery is documented, tested, and ITIL-compliant.
👉 Would you like me to now prepare detailed failover execution steps (like a playbook: “what to do when Primary fails” and “what to do when full DC fails”)?
Perfect 👍 — now we’ll create a Failover Execution Playbook for your Stores Accounting System.
This is the step-by-step guide operators follow during an incident, ensuring consistency and ITIL-compliant incident management.
📘 Failover Execution Playbook
For Stores Accounting System (MS SQL Server + ASP.NET WebForms)
1. Purpose
To provide clear, repeatable steps for failing over StoresDB in case of:
- Primary SQL node failure (HA event, local failover)
- Full Data Center outage (DR failover to DC-B)
2. Roles & Responsibilities
- DBA Team: Execute failover, restore databases if needed.
- SysAdmin Team: Validate server/network availability, IIS health.
- Application Team: Validate application functionality post-failover.
- Service Desk: Communicate status to end users.
3. Failover Scenarios
A) Local HA Failover (Primary → Secondary in DC-A)
(Handled automatically by AlwaysOn AG in synchronous mode)
Detection:
- Monitoring alerts: SQL Agent, SCOM, or Database Mail report replica down.
- End users may experience short interruption.
Execution (DBA Team):
- Confirm AG has failed over to Secondary:
SELECT replica_server_name, role_desc, synchronization_state_desc FROM sys.dm_hadr_availability_replica_states;
- If failover didn’t occur automatically, force it manually:
ALTER AVAILABILITY GROUP [StoresAG] FAILOVER;
- Validate database status:
SELECT name, state_desc FROM sys.databases WHERE name='StoresDB';
- Notify Application Team → confirm application is working via AG Listener.
Post-Failover Actions:
- Review SQL error logs for cause.
- If Primary is down, schedule recovery.
- Update incident ticket with resolution time.
B) Full Data Center Outage (Failover to DR in DC-B)
(Requires manual promotion of log shipping secondary)
Execution (DBA Team):
- Stop log shipping jobs on DR server.
EXEC msdb.dbo.sp_stop_job @job_name = 'LSRestore_StoresDB';
- Recover DR database:
RESTORE DATABASE [StoresDB] WITH RECOVERY;
- Validate database:
DBCC CHECKDB('StoresDB') WITH NO_INFOMSGS;
- Update IIS connection strings (App Team):
<connectionStrings> <add name="StoresDb" connectionString="Server=DRSQL01;Database=StoresDB;Integrated Security=True;" /> </connectionStrings>
(if not using DNS alias or AG listener extended to DR) - Restart IIS app pools.
- App Team validates: Login → Request → Approval → Issue flow.
- Notify Service Desk → business users can resume work.
Post-Failover Actions:
- DBA marks Primary as unavailable in monitoring system.
- Incident record updated with RTO/RPO achieved.
- Once DC-A is restored:
- Re-initialize log shipping from DR back to Primary.
- Plan failback during maintenance window.
4. Verification Checklist
✅ DBA confirms DB is online.
✅ Application Team confirms transactions succeed.
✅ SysAdmin confirms IIS & NLB nodes are healthy.
✅ Service Desk confirms users are accessing system normally.
5. Communication Protocol
- During Failover: Service Desk sends update every 30 minutes.
- After Recovery: IT Manager communicates incident resolution + impact report.
- Post-Mortem: Problem Management team reviews root cause.
6. ITIL Alignment
- Incident Mgmt: Failover restores service quickly.
- Availability Mgmt: HA keeps service continuous.
- Continuity Mgmt: DR ensures business survival after site loss.
- Problem Mgmt: Root cause analysis post-event.
✅ Outcome
- Local HA Failover: Automatic, minimal downtime (<1 min).
- DR Failover: Manual but structured, within 2h RTO, 30m RPO.
- Teams know exact steps + responsibilities.
👉 Would you like me to also create a Failback Playbook (step-by-step guide to return operations from DR site back to Primary once it’s restored)?
Great 👍 — let’s finalize the HA/DR lifecycle by creating the Failback Playbook.
This will cover how to safely return your Stores Accounting System from the DR site (DC-B) back to the Primary site (DC-A) once it’s restored and stable.
📘 Failback Playbook
For Stores Accounting System (MS SQL Server + ASP.NET WebForms)
1. Purpose
To provide a structured, risk-controlled process for restoring primary operations in Data Center A after a failover to the DR site (Data Center B).
2. Preconditions
✅ DC-A infrastructure is fully restored and validated.
✅ Primary SQL Server and IIS nodes are reinstalled and patched.
✅ Network and firewall rules are tested.
✅ Business agrees on planned maintenance window for failback.
✅ Change approved by CAB (Change Advisory Board).
3. Roles & Responsibilities
- DBA Team: Database sync and switchover.
- SysAdmin Team: Ensure servers are online, IIS nodes ready.
- Application Team: Validate application post-failback.
- Service Desk: Communicate with end users.
4. Failback Execution Steps
Step-A: Preparation
- Notify business users about planned failback window.
- Ensure no active requests are pending in Stores system.
- Put application in read-only mode (if possible) to minimize changes during sync.
- Alternative: Announce downtime (~30-60 min).
Step-B: Reinitialize Primary (DC-A)
- On restored Primary SQL Server:
- Drop any stale
StoresDB
database.
DROP DATABASE IF EXISTS [StoresDB];
- Drop any stale
- Take a full backup from DR server:
BACKUP DATABASE [StoresDB] TO DISK='\\PrimarySQL\Sync\StoresDB_Full.bak' WITH INIT, STATS=10;
- Restore it on Primary (NORECOVERY):
RESTORE DATABASE [StoresDB] FROM DISK='D:\Sync\StoresDB_Full.bak' WITH NORECOVERY;
Step-C: Re-Establish Synchronization
- Configure log shipping or Availability Group from DR → Primary.
- Allow logs to catch up until latency = 0.
- Validate both databases in synchronization state.
SELECT primary_database, secondary_database, last_backup_file, last_restored_file FROM msdb.dbo.log_shipping_monitor_secondary;
Step-D: Switchover to Primary
- Stop IIS applications (App Team).
- On DR server → take final transaction log backup.
BACKUP LOG [StoresDB] TO DISK='\\PrimarySQL\Sync\StoresDB_TailLog.trn' WITH INIT;
- Restore tail log on Primary (WITH RECOVERY):
RESTORE LOG [StoresDB] FROM DISK='D:\Sync\StoresDB_TailLog.trn' WITH RECOVERY;
- Point IIS web.config back to Primary Listener or SQL Server:
<connectionStrings> <add name="StoresDb" connectionString="Server=PrimarySQL01;Database=StoresDB;Integrated Security=True;" /> </connectionStrings>
- Restart IIS apps.
Step-E: Validation
✅ DBA: Run DBCC CHECKDB('StoresDB')
.
✅ SysAdmin: Verify SQL + IIS logs for errors.
✅ App Team: Run test transactions (Request → Approval → Issue).
✅ Service Desk: Confirm with end users.
Step F: Decommission Temporary DR
- Disable log shipping jobs from DR → Primary.
- Set up replication back to DR for future protection.
- Update monitoring dashboards.
5. Post-Failback Actions
- Business signs off that application is stable.
- DBA documents failback time, RTO, RPO achieved.
- Close change request in ITSM.
- Schedule lessons learned review.
6. ITIL Alignment
- Change Mgmt: Failback is a planned CAB-approved change.
- Continuity Mgmt: DR served as temporary operations; primary is restored.
- Availability Mgmt: HA/DR cycle completed successfully.
- Problem Mgmt: Capture issues from failback for future improvements.
✅ Outcome
- Stores Accounting System returns to Primary Data Center safely.
- Data integrity and availability preserved.
- Business disruption minimized.
- Teams follow a repeatable, ITIL-compliant process.
👉 Would you like me to also design a 1-page Quick Reference (Runbook Cheat Sheet) for operators (condensed version of Failover + Failback steps) that can be used during emergencies?
Perfect 👍 — let’s create a 1-page Quick Reference (Runbook Cheat Sheet) that operators can use during emergencies for both Failover and Failback of your Stores Accounting System.
This will be condensed, action-oriented, and suitable for printing or keeping in an Ops binder.
📝 Stores Accounting System – DR/HA Quick Reference
SQL Server + ASP.NET WebForms
🔴 Failover (When Primary Fails)
1. Local HA Failover (Primary → Secondary in DC-A)
✅ Automatic if AlwaysOn AG is enabled.
- Check replica roles:
SELECT replica_server_name, role_desc FROM sys.dm_hadr_availability_replica_states;
- If needed, force failover:
ALTER AVAILABILITY GROUP [StoresAG] FAILOVER;
- Validate DB:
SELECT name, state_desc FROM sys.databases WHERE name='StoresDB';
- Confirm IIS app works via AG Listener.
2. DR Failover (Full DC-A Outage → DR Server in DC-B)
- Stop log shipping jobs on DR server.
EXEC msdb.dbo.sp_stop_job @job_name='LSRestore_StoresDB';
- Recover DB:
RESTORE DATABASE [StoresDB] WITH RECOVERY;
- Update IIS connection string to DR SQL Server.
- Restart IIS, test app flow (Request → Approval → Issue).
- Notify Service Desk → business resumes.
🟢 Failback (Returning to Primary DC-A)
- Take full backup from DR, restore to Primary (NORECOVERY).
BACKUP DATABASE [StoresDB] TO DISK='\\PrimarySQL\Sync\StoresDB_Full.bak'; RESTORE DATABASE [StoresDB] FROM DISK='D:\Sync\StoresDB_Full.bak' WITH NORECOVERY;
- Set up log sync DR → Primary until caught up.
- Take tail log backup on DR, restore on Primary (WITH RECOVERY).
BACKUP LOG [StoresDB] TO DISK='\\PrimarySQL\Sync\StoresDB_TailLog.trn' WITH INIT; RESTORE LOG [StoresDB] FROM DISK='D:\Sync\StoresDB_TailLog.trn' WITH RECOVERY;
- Point IIS back to Primary SQL / AG Listener.
- Restart IIS app pools, test end-to-end.
- Decommission DR as active → reconfigure for standby.
✅ Verification Checklist
DBCC CHECKDB('StoresDB')
passes.- IIS logs show no DB errors.
- App login + workflow (Request → Approval → Issue) works.
- Monitoring alerts cleared.
- Service Desk confirms user access.
📢 Communication
- During failover → update users every 30 min.
- After recovery → send incident resolution report.
- Post-event → run Problem Mgmt review.
⚡ Key Notes
- RTO: 2 hours
- RPO: 30 minutes
- Use AG Listener for seamless HA failover.
- DR failover is manual, requires IIS reconfiguration.
- Always test quarterly.
👉 This cheat sheet is the operator-ready condensed guide.
Would you like me to also prepare a visual flowchart diagram of the failover/failback process (so teams can follow a graphical decision tree during an incident)?

Excellent 👍 — here’s a final consolidated document outline for handover to your IT team. This captures everything we built together, structured as a professional ITIL-aligned operations document.
📘 Consolidated Handover Document
Stores Accounting System – Database & Application HA/DR Plan
(MS SQL Server + ASP.NET WebForms, ITIL-compliant)
1. Introduction
- Purpose of document
- Scope: Stores Accounting System (DB + Web App)
- ITIL Alignment: Availability, Continuity, Incident, Change Mgmt
2. System Overview
- Components:
- SQL Server (
StoresDB
) - ASP.NET WebForms app (IIS hosted)
- Roles: Employee, Storekeeper, HoD
- SQL Server (
- Data Centers: DC-A (Primary), DC-B (DR)
3. Database Schema (MS SQL)
- Full DDL (tables, FKs, indexes, constraints)
- Seed data for Departments, Items
- Integrity constraints documented
4. Backup & Maintenance Strategy
- Backup schedule: Full (daily), Diff (6h), Log (30m)
- Retention policy (14d daily, 2m weekly, 1y monthly)
- SQL Agent Jobs: Backup, Cleanup, Index Maintenance, DBCC
- Monitoring & Alerts via Database Mail
5. Disaster Recovery Runbook
- Disaster scenarios (corruption, server crash, ransomware, DC outage)
- Step-by-step DB restore procedures
- RTO = 2h, RPO = 30m
6. High Availability & DR Design
- HA: SQL AlwaysOn AG (Primary + Secondary in DC-A)
- DR: Log Shipping to DC-B
- IIS layer: NLB with 2 web servers
- Architecture diagram included
7. Implementation Checklist
- SysAdmin tasks: servers, clustering, IIS load balancer
- DBA tasks: AG config, log shipping, monitoring
- App team tasks: connection strings, deployment, session state
- Testing plan: failover + DR simulation
8. Failover Playbook
- Local HA Failover: automatic, AG Listener-based
- DR Failover: manual, recover DB, reconfigure IIS
- Communication protocol during incidents
9. Failback Playbook
- Preconditions: DC-A restored, CAB approval
- Steps: Sync from DR → Primary, restore tail log, switch IIS back
- Validation & Post-failback actions
10. Quick Reference Cheat Sheet
- 1-page condensed steps for Failover & Failback
- Verification checklist
- RTO/RPO reminders
11. Visual Flowchart
- Decision tree for Failover/Failback execution
- To be maintained in Visio/Draw.io
12. Ongoing Operations
- Quarterly DR drills
- Weekly monitoring review
- Change Mgmt process for HA/DR updates
- Problem Mgmt reviews after incidents
✅ Deliverables for IT Team
- Full SQL DDL & seed data scripts
- SQL Agent Job scripts (backup, maintenance, cleanup)
- Runbook (detailed + cheat sheet)
- Flowchart (PDF + editable source)
- Checklist for SysAdmin, DBA, App teams
- Communication templates for Service Desk
👉 With this document, your IT team has a complete, ITIL-compliant HA/DR package for Stores Accounting System.
Would you like me to prepare this outline into a ready-to-use Word/Docx template (with placeholders for your company logo, team contacts, and environment-specific details)?